Working api.

This commit is contained in:
aminecmi 2022-10-01 14:19:28 +02:00
parent 1b8e514476
commit c7b0bcde49
26 changed files with 418 additions and 187 deletions

View File

@ -18,6 +18,8 @@ dependencies {
implementation("io.javalin:javalin:4.6.4")
implementation("org.slf4j:slf4j-simple:2.0.1")
implementation("com.squareup.sqldelight:sqlite-driver:1.5.3")
implementation("com.fasterxml.jackson.core:jackson-databind:2.13.4")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4")
}
tasks.test {

View File

@ -1,27 +1,24 @@
package controller
import io.javalin.apibuilder.CrudHandler
import fr.louveauAmine.cvApi.dao.CVApiDB
import io.javalin.http.Context
import view.EducationYear
import view.toEntity
import view.toView
class EducationYearController : CrudHandler {
override fun create(ctx: Context) {
TODO("Not yet implemented")
class EducationYearController(private val database: CVApiDB) {
fun create(ctx: Context) {
database.transaction {
val bodyAsClass = ctx.bodyAsClass(EducationYear::class.java)
database.educationYearQueries.insertEducationYear(bodyAsClass.toEntity())
val executeAsOne =
database.educationYearQueries.oneEducationYear(bodyAsClass.endDate, bodyAsClass.school).executeAsOne()
ctx.json(executeAsOne.toView())
}
}
override fun delete(ctx: Context, resourceId: String) {
TODO("Not yet implemented")
fun delete(ctx: Context) {
val bodyAsClass = ctx.bodyAsClass(EducationYear::class.java)
database.educationYearQueries.deleteEducationYear(bodyAsClass.endDate, bodyAsClass.school)
}
override fun getAll(ctx: Context) {
TODO("Not yet implemented")
}
override fun getOne(ctx: Context, resourceId: String) {
TODO("Not yet implemented")
}
override fun update(ctx: Context, resourceId: String) {
TODO("Not yet implemented")
}
}

View File

@ -1,27 +1,23 @@
package controller
import fr.louveauAmine.cvApi.dao.CVApiDB
import io.javalin.apibuilder.CrudHandler
import io.javalin.http.Context
import view.*
class InterestController : CrudHandler {
override fun create(ctx: Context) {
TODO("Not yet implemented")
class InterestController(val database: CVApiDB) {
fun create(ctx: Context) {
database.transaction {
val bodyAsClass = ctx.bodyAsClass(Interest::class.java)
database.interestsQueries.insertInterest(bodyAsClass.toEntity())
val executeAsOne =
database.interestsQueries.oneInterest(bodyAsClass.theme).executeAsOne()
ctx.json(executeAsOne.toView())
}
}
override fun delete(ctx: Context, resourceId: String) {
TODO("Not yet implemented")
fun delete(ctx: Context) {
val bodyAsClass = ctx.bodyAsClass(DeleteInterest::class.java)
database.interestsQueries.deleteInterest(bodyAsClass.theme)
}
override fun getAll(ctx: Context) {
TODO("Not yet implemented")
}
override fun getOne(ctx: Context, resourceId: String) {
TODO("Not yet implemented")
}
override fun update(ctx: Context, resourceId: String) {
TODO("Not yet implemented")
}
}

View File

@ -1,27 +1,23 @@
package controller
import fr.louveauAmine.cvApi.dao.CVApiDB
import io.javalin.apibuilder.CrudHandler
import io.javalin.http.Context
import view.*
class JobController : CrudHandler {
override fun create(ctx: Context) {
TODO("Not yet implemented")
class JobController(val database: CVApiDB) {
fun create(ctx: Context) {
database.transaction {
val bodyAsClass = ctx.bodyAsClass(Job::class.java)
database.jobsQueries.insertJob(bodyAsClass.toEntity())
val executeAsOne =
database.jobsQueries.oneJob(bodyAsClass.start, bodyAsClass.end, bodyAsClass.company).executeAsOne()
ctx.json(executeAsOne.toView())
}
}
override fun delete(ctx: Context, resourceId: String) {
TODO("Not yet implemented")
fun delete(ctx: Context) {
val bodyAsClass = ctx.bodyAsClass(DeleteJob::class.java)
database.jobsQueries.deleteJob(bodyAsClass.start, bodyAsClass.end, bodyAsClass.company)
}
override fun getAll(ctx: Context) {
TODO("Not yet implemented")
}
override fun getOne(ctx: Context, resourceId: String) {
TODO("Not yet implemented")
}
override fun update(ctx: Context, resourceId: String) {
TODO("Not yet implemented")
}
}

View File

@ -1,27 +1,23 @@
package controller
import fr.louveauAmine.cvApi.dao.CVApiDB
import io.javalin.apibuilder.CrudHandler
import io.javalin.http.Context
import view.*
class LanguageController : CrudHandler {
override fun create(ctx: Context) {
TODO("Not yet implemented")
class LanguageController(val database: CVApiDB) {
fun create(ctx: Context) {
database.transaction {
val bodyAsClass = ctx.bodyAsClass(Language::class.java)
database.languagesQueries.insertLang(bodyAsClass.toEntity())
val executeAsOne =
database.languagesQueries.oneLang(bodyAsClass.lang).executeAsOne()
ctx.json(executeAsOne.toView())
}
}
override fun delete(ctx: Context, resourceId: String) {
TODO("Not yet implemented")
fun delete(ctx: Context) {
val bodyAsClass = ctx.bodyAsClass(DeleteLanguage::class.java)
database.languagesQueries.deleteLang(bodyAsClass.lang)
}
override fun getAll(ctx: Context) {
TODO("Not yet implemented")
}
override fun getOne(ctx: Context, resourceId: String) {
TODO("Not yet implemented")
}
override fun update(ctx: Context, resourceId: String) {
TODO("Not yet implemented")
}
}

View File

@ -1,7 +1,23 @@
package controller
class MeController {
fun getMe() {
TODO("Not yet implemented")
import fr.louveauAmine.cvApi.dao.*
import io.javalin.http.Context
import view.*
class MeController(private val database: CVApiDB) {
fun getMe(ctx: Context) {
ctx.json(
Me(
profile = database.profileQueries.profiles().executeAsOne().toView(),
interests = database.interestsQueries.interests().executeAsList().map { it.toView() },
jobs = database.jobsQueries.jobs().executeAsList().map { it.toView() },
sideProjects = database.sideProjectsQueries.sideProjects().executeAsList().map { it.toView() }.groupBy { it.category },
educationYears = database.educationYearQueries.educationYears().executeAsList().map { it.toView() },
languages = database.languagesQueries.langs().executeAsList().map { it.toView() },
tags = database.tagsQueries.tags().executeAsList().map { it.toView() }
)
)
}
}

View File

@ -1,27 +1,39 @@
package controller
import fr.louveauAmine.cvApi.dao.CVApiDB
import io.javalin.apibuilder.CrudHandler
import io.javalin.http.Context
import view.*
class ProfileController : CrudHandler {
override fun create(ctx: Context) {
TODO("Not yet implemented")
class ProfileController(val database: CVApiDB) {
fun create(ctx: Context) {
database.transaction {
val bodyAsClass = ctx.bodyAsClass(Profile::class.java)
database.profileQueries.insertProfile(bodyAsClass.toEntity())
val executeAsOne =
database.profileQueries.oneProfile(bodyAsClass.name).executeAsOne()
ctx.json(executeAsOne.toView())
}
}
override fun delete(ctx: Context, resourceId: String) {
TODO("Not yet implemented")
}
override fun getAll(ctx: Context) {
TODO("Not yet implemented")
}
override fun getOne(ctx: Context, resourceId: String) {
TODO("Not yet implemented")
}
override fun update(ctx: Context, resourceId: String) {
TODO("Not yet implemented")
fun update(ctx: Context) {
val bodyAsClass = ctx.bodyAsClass(UpdateProfile::class.java)
val existing = database.profileQueries.oneProfile(bodyAsClass.name).executeAsOne()
database.profileQueries.updateProfile(
bodyAsClass.newName ?: existing.name,
bodyAsClass.title ?: existing.title,
bodyAsClass.quote ?: existing.quote,
bodyAsClass.picture ?: existing.picture,
bodyAsClass.email ?: existing.email,
bodyAsClass.phone ?: existing.phone,
bodyAsClass.linkedin ?: existing.linkedin,
bodyAsClass.source ?: existing.source,
bodyAsClass.name
)
val executeAsOne =
database.profileQueries.oneProfile(bodyAsClass.newName ?: existing.name).executeAsOne()
ctx.json(executeAsOne.toView())
}
}

View File

@ -1,27 +1,26 @@
package controller
import fr.louveauAmine.cvApi.dao.CVApiDB
import io.javalin.apibuilder.CrudHandler
import io.javalin.http.Context
import view.DeleteSideProject
import view.SideProject
import view.toEntity
import view.toView
class SideProjectController : CrudHandler {
override fun create(ctx: Context) {
TODO("Not yet implemented")
class SideProjectController(val database: CVApiDB) {
fun create(ctx: Context) {
database.transaction {
val bodyAsClass = ctx.bodyAsClass(SideProject::class.java)
database.sideProjectsQueries.insertSideProject(bodyAsClass.toEntity())
val executeAsOne =
database.sideProjectsQueries.oneSideProject(bodyAsClass.description, bodyAsClass.category).executeAsOne()
ctx.json(executeAsOne.toView())
}
}
override fun delete(ctx: Context, resourceId: String) {
TODO("Not yet implemented")
fun delete(ctx: Context) {
val bodyAsClass = ctx.bodyAsClass(DeleteSideProject::class.java)
database.sideProjectsQueries.deleteSideProject(bodyAsClass.description, bodyAsClass.category)
}
override fun getAll(ctx: Context) {
TODO("Not yet implemented")
}
override fun getOne(ctx: Context, resourceId: String) {
TODO("Not yet implemented")
}
override fun update(ctx: Context, resourceId: String) {
TODO("Not yet implemented")
}
}

View File

@ -1,27 +1,26 @@
package controller
import fr.louveauAmine.cvApi.dao.CVApiDB
import io.javalin.apibuilder.CrudHandler
import io.javalin.http.Context
import view.DeleteTag
import view.Tag
import view.toEntity
import view.toView
class TagController : CrudHandler {
override fun create(ctx: Context) {
TODO("Not yet implemented")
class TagController(val database: CVApiDB) {
fun create(ctx: Context) {
database.transaction {
val bodyAsClass = ctx.bodyAsClass(Tag::class.java)
database.tagsQueries.insertTag(bodyAsClass.toEntity())
val executeAsOne =
database.tagsQueries.oneTag(bodyAsClass.name, bodyAsClass.category).executeAsOne()
ctx.json(executeAsOne.toView())
}
}
override fun delete(ctx: Context, resourceId: String) {
TODO("Not yet implemented")
fun delete(ctx: Context) {
val bodyAsClass = ctx.bodyAsClass(DeleteTag::class.java)
database.tagsQueries.deleteTag(bodyAsClass.name, bodyAsClass.category)
}
override fun getAll(ctx: Context) {
TODO("Not yet implemented")
}
override fun getOne(ctx: Context, resourceId: String) {
TODO("Not yet implemented")
}
override fun update(ctx: Context, resourceId: String) {
TODO("Not yet implemented")
}
}

View File

@ -10,11 +10,6 @@ import io.javalin.core.security.RouteRole
enum class ApiRole : RouteRole { PUBLIC, BASIC_AUTHED }
fun main() {
val driver: SqlDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
CVApiDB.Schema.create(driver)
val database = CVApiDB(driver)
database.profileQueries.insertProfile(PROFILE("Amine Louveau", "truc", "tata", "prout", null, null, null, null))
val app = Javalin.create{
it.accessManager { handler, ctx, permittedRoles ->
@ -29,22 +24,72 @@ fun main() {
}.start(7070)
val driver: SqlDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY + "cvapi.db")
CVApiDB.Schema.create(driver)
val database = CVApiDB(driver)
val meController = MeController(database)
val educationController = EducationYearController(database)
val interestController = InterestController(database)
val jobController = JobController(database)
val languageController = LanguageController(database)
val profileController = ProfileController(database)
val sideProjectController = SideProjectController(database)
val tagController = TagController(database)
app.routes {
ApiBuilder.path("api") {
ApiBuilder.get("me", {
MeController().getMe()
ApiBuilder.get("me", { ctx ->
meController.getMe(ctx)
}, ApiRole.PUBLIC)
ApiBuilder.post("education", { ctx ->
educationController.create(ctx)
}, ApiRole.BASIC_AUTHED)
ApiBuilder.delete("education", { ctx ->
educationController.delete(ctx)
}, ApiRole.BASIC_AUTHED)
ApiBuilder.crud("education/{education-id}", EducationYearController(), ApiRole.BASIC_AUTHED)
ApiBuilder.crud("interest/{interest-id}", InterestController(), ApiRole.BASIC_AUTHED)
ApiBuilder.crud("job/{job-id}", JobController(), ApiRole.BASIC_AUTHED)
ApiBuilder.crud("language/{lang-id}", LanguageController(), ApiRole.BASIC_AUTHED)
ApiBuilder.crud("profile/{profile-id}", ProfileController(), ApiRole.BASIC_AUTHED)
ApiBuilder.crud("side-project/{side-id}", SideProjectController(), ApiRole.BASIC_AUTHED)
ApiBuilder.crud("tag/{tag-id}", TagController(), ApiRole.BASIC_AUTHED)
ApiBuilder.post("interest", { ctx ->
interestController.create(ctx)
}, ApiRole.BASIC_AUTHED)
ApiBuilder.delete("interest", { ctx ->
interestController.delete(ctx)
}, ApiRole.BASIC_AUTHED)
ApiBuilder.post("job", { ctx ->
jobController.create(ctx)
}, ApiRole.BASIC_AUTHED)
ApiBuilder.delete("job", { ctx ->
jobController.delete(ctx)
}, ApiRole.BASIC_AUTHED)
ApiBuilder.post("language", { ctx ->
languageController.create(ctx)
}, ApiRole.BASIC_AUTHED)
ApiBuilder.delete("language", { ctx ->
languageController.delete(ctx)
}, ApiRole.BASIC_AUTHED)
ApiBuilder.post("side-project", { ctx ->
sideProjectController.create(ctx)
}, ApiRole.BASIC_AUTHED)
ApiBuilder.delete("side-project", { ctx ->
sideProjectController.delete(ctx)
}, ApiRole.BASIC_AUTHED)
ApiBuilder.post("tag", { ctx ->
tagController.create(ctx)
}, ApiRole.BASIC_AUTHED)
ApiBuilder.delete("tag", { ctx ->
tagController.delete(ctx)
}, ApiRole.BASIC_AUTHED)
ApiBuilder.post("profile", { ctx ->
profileController.create(ctx)
}, ApiRole.BASIC_AUTHED)
ApiBuilder.patch("profile", { ctx ->
profileController.update(ctx)
}, ApiRole.BASIC_AUTHED)
}
}
}

View File

@ -0,0 +1,16 @@
package view
import fr.louveauAmine.cvApi.dao.EDUCATIONYEAR
data class EducationYear(
val endDate: String, val school: String, val name: String
)
fun EDUCATIONYEAR.toView(): EducationYear = EducationYear(
endDate = this.endDate, school = this.school, name = this.name
)
fun EducationYear.toEntity(): EDUCATIONYEAR = EDUCATIONYEAR(
endDate = this.endDate, school = this.school, name = this.name
)

View File

@ -0,0 +1,17 @@
package view
import fr.louveauAmine.cvApi.dao.INTEREST
data class Interest(
val theme: String,
val description: String
)
data class DeleteInterest(
val theme: String
)
fun INTEREST.toView(): Interest = Interest(theme = this.theme, description = this.description)
fun Interest.toEntity(): INTEREST = INTEREST(theme = this.theme, description = this.description)

View File

@ -0,0 +1,34 @@
package view
import fr.louveauAmine.cvApi.dao.JOB
data class Job(
val jobTitle: String,
val start: String,
val end: String,
val company: String,
val tasks: String
)
data class DeleteJob(
val start: String,
val end: String,
val company: String
)
fun JOB.toView(): Job = Job(
jobTitle = this.jobTitle,
start = this.start,
end = this.end,
company = this.company,
tasks = this.tasks
)
fun Job.toEntity(): JOB = JOB(
jobTitle = this.jobTitle,
start = this.start,
end = this.end,
company = this.company,
tasks = this.tasks
)

View File

@ -0,0 +1,14 @@
package view
import fr.louveauAmine.cvApi.dao.LANGUAGE
data class Language(
val lang: String,
val level: String
)
data class DeleteLanguage(val lang: String)
fun LANGUAGE.toView(): Language = Language(lang = this.lang, level = this.level)
fun Language.toEntity(): LANGUAGE = LANGUAGE(lang = this.lang, level = this.level)

View File

@ -0,0 +1,3 @@
package view
data class Me(val profile: Profile, val interests: List<Interest>, val jobs: List<Job>, val sideProjects: Map<String, List<SideProject>>, val educationYears: List<EducationYear>, val languages: List<Language>, val tags: List<Tag>)

View File

@ -0,0 +1,49 @@
package view
import fr.louveauAmine.cvApi.dao.PROFILE
data class Profile(
val name: String,
val title: String,
val quote: String,
val picture: String,
val email: String?,
val phone: String?,
val linkedin: String?,
val source: String?
)
data class UpdateProfile(
val name: String,
val newName: String?,
val title: String?,
val quote: String?,
val picture: String?,
val email: String?,
val phone: String?,
val linkedin: String?,
val source: String?
)
fun PROFILE.toView(): Profile = Profile(
name = this.name,
title = this.title,
quote = this.quote,
picture = this.picture,
email = this.email,
phone = this.phone,
linkedin = this.linkedin,
source = this.source
)
fun Profile.toEntity(): PROFILE = PROFILE(
name = this.name,
title = this.title,
quote = this.quote,
picture = this.picture,
email = this.email,
phone = this.phone,
linkedin = this.linkedin,
source = this.source
)

View File

@ -0,0 +1,21 @@
package view
import fr.louveauAmine.cvApi.dao.SIDEPROJECT
data class SideProject(
val description: String,
val title: String?,
val url: String?,
val category: String
)
data class DeleteSideProject(
val description: String,
val category: String
)
fun SIDEPROJECT.toView(): SideProject =
SideProject(description = this.description, title = this.title, url = this.url, category = this.category)
fun SideProject.toEntity(): SIDEPROJECT = SIDEPROJECT(description = this.description, title = this.title, url = this.url, category = this.category)

View File

@ -0,0 +1,19 @@
package view
import fr.louveauAmine.cvApi.dao.TAG
data class Tag(
val name: String,
val workThing: Boolean,
val category: String
)
data class DeleteTag(
val name: String,
val category: String
)
fun TAG.toView(): Tag = Tag(name = this.name, workThing = this.workThing, category = this.category)
fun Tag.toEntity(): TAG = TAG(name = this.name, workThing = this.workThing, category = this.category)

View File

@ -1,15 +1,18 @@
CREATE TABLE EDUCATIONYEAR (
CREATE TABLE IF NOT EXISTS EDUCATIONYEAR (
`endDate` TEXT NOT NULL,
`school` TEXT NOT NULL,
`name` TEXT NOT NULL,
PRIMARY KEY(`endDate`, `school`)
);
CREATE INDEX educationyear_date_school ON EDUCATIONYEAR(`endDate`, `school`);
CREATE INDEX IF NOT EXISTS educationyear_date_school ON EDUCATIONYEAR(`endDate`, `school`);
educationYears:
SELECT * FROM EDUCATIONYEAR;
oneEducationYear:
SELECT * FROM EDUCATIONYEAR WHERE `endDate` = ? AND `school` = ?;
insertEducationYear:
INSERT OR REPLACE INTO EDUCATIONYEAR VALUES ?;

View File

@ -1,14 +1,17 @@
CREATE TABLE INTEREST (
CREATE TABLE IF NOT EXISTS INTEREST (
`theme` TEXT NOT NULL,
`description` TEXT NOT NULL,
PRIMARY KEY (`theme`)
);
CREATE INDEX interest_key ON INTEREST(`theme`);
CREATE INDEX IF NOT EXISTS interest_key ON INTEREST(`theme`);
interests:
SELECT * FROM INTEREST;
oneInterest:
SELECT * FROM INTEREST WHERE `theme` = ?;
insertInterest:
INSERT OR REPLACE INTO INTEREST VALUES ?;

View File

@ -1,4 +1,4 @@
CREATE TABLE JOB (
CREATE TABLE IF NOT EXISTS JOB (
`jobTitle` TEXT NOT NULL,
`start` TEXT NOT NULL,
`end` TEXT NOT NULL,
@ -7,11 +7,14 @@ CREATE TABLE JOB (
PRIMARY KEY (`start`, `end`, `company`)
);
CREATE INDEX job_index ON JOB(`start`, `end`, `company`);
CREATE INDEX IF NOT EXISTS job_index ON JOB(`start`, `end`, `company`);
jobs:
SELECT * FROM JOB;
oneJob:
SELECT * FROM JOB WHERE `start` = ? AND `end` = ? AND `company` = ?;
insertJob:
INSERT OR REPLACE INTO JOB VALUES ?;

View File

@ -1,14 +1,17 @@
CREATE TABLE LANGUAGE (
CREATE TABLE IF NOT EXISTS LANGUAGE (
`lang` TEXT NOT NULL,
`level` TEXT NOT NULL,
PRIMARY KEY(`lang`)
);
CREATE INDEX lang_lang ON LANGUAGE(`lang`);
CREATE INDEX IF NOT EXISTS lang_lang ON LANGUAGE(`lang`);
langs:
SELECT * FROM LANGUAGE;
oneLang:
SELECT * FROM LANGUAGE WHERE `lang` = ?;
insertLang:
INSERT OR REPLACE INTO LANGUAGE VALUES ?;

View File

@ -1,16 +0,0 @@
CREATE TABLE MIGRATION (
`identifier` TEXT NOT NULL,
`done` INTEGER AS Boolean DEFAULT 0 NOT NULL,
PRIMARY KEY (`identifier`)
);
CREATE INDEX migration_index ON MIGRATION(`identifier`);
migrations:
SELECT * FROM MIGRATION;
insertMigration:
INSERT OR REPLACE INTO MIGRATION VALUES ?;
deleteMigration:
DELETE FROM MIGRATION WHERE `identifier` = ?;

View File

@ -1,4 +1,4 @@
CREATE TABLE PROFILE (
CREATE TABLE IF NOT EXISTS PROFILE (
`name` TEXT NOT NULL,
`title` TEXT NOT NULL,
`quote` TEXT NOT NULL,
@ -10,13 +10,16 @@ CREATE TABLE PROFILE (
PRIMARY KEY (`name`)
);
CREATE INDEX profile_name ON PROFILE(`name`);
CREATE INDEX IF NOT EXISTS profile_name ON PROFILE(`name`);
profiles:
SELECT * FROM PROFILE;
oneProfile:
SELECT * FROM PROFILE WHERE `name` = ?;
insertProfile:
INSERT OR REPLACE INTO PROFILE VALUES ?;
deleteProfile:
DELETE FROM PROFILE WHERE `name` = ?;
updateProfile:
UPDATE PROFILE SET `name` = ?, `title` = ?, `quote` = ?, `picture` = ?, `email` = ?, `phone` = ?, `linkedin` = ?, `source` = ? WHERE `name` = ?;

View File

@ -1,20 +1,18 @@
CREATE TABLE SIDEPROJECT (
CREATE TABLE IF NOT EXISTS SIDEPROJECT (
`description` TEXT NOT NULL,
`title` TEXT,
`url` TEXT,
`category` TEXT NOT NULL ,
PRIMARY KEY (`description`, `title`, `url`, `category`)
PRIMARY KEY (`description`, `category`)
);
CREATE INDEX sideproject_index ON SIDEPROJECT(`description`, `title`, `url`, `category`);
CREATE INDEX IF NOT EXISTS sideproject_index ON SIDEPROJECT(`description`, `category`);
sideProjects:
SELECT * FROM SIDEPROJECT;
sideProjectsByCategory:
SELECT * FROM SIDEPROJECT
GROUP BY `category`
ORDER BY COUNT(`category`) DESC;
oneSideProject:
SELECT * FROM SIDEPROJECT WHERE `description` = ? AND `category` = ?;
insertSideProject:
INSERT OR REPLACE INTO SIDEPROJECT VALUES ?;

View File

@ -1,15 +1,18 @@
CREATE TABLE TAG (
CREATE TABLE IF NOT EXISTS TAG (
`name` TEXT NOT NULL,
`workThing` INTEGER AS Boolean DEFAULT 1 NOT NULL,
`category` TEXT NOT NULL,
PRIMARY KEY (`name`)
PRIMARY KEY (`name`, `category`)
);
CREATE INDEX tag_name ON TAG(`name`);
CREATE INDEX IF NOT EXISTS tag_name ON TAG(`name`, `category`);
tags:
SELECT * FROM TAG;
oneTag:
SELECT * FROM TAG WHERE `name` = ? AND `category` = ?;
insertTag:
INSERT OR REPLACE INTO TAG VALUES ?;