From 268c41c9121d3a90f054a3a4e598e8890d6fbaef Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Sat, 20 Apr 2024 13:05:53 +0800 Subject: [PATCH] Add DuckDB sequences Include sequences in data source definition. --- TODO.md | 5 + .../core/test/features/decorators.test.ts | 1 + packages/data-sources/duckdb/src/index.ts | 92 ++++++++++++++---- packages/examples/duckdb/.gitignore | 2 + packages/examples/duckdb/src/index.ts | 3 +- packages/examples/duckdb/test.db | Bin 274432 -> 0 bytes packages/examples/duckdb/test.db.wal | Bin 836 -> 0 bytes 7 files changed, 82 insertions(+), 21 deletions(-) delete mode 100644 packages/examples/duckdb/test.db delete mode 100644 packages/examples/duckdb/test.db.wal diff --git a/TODO.md b/TODO.md index c1c4e50..13a05d6 100644 --- a/TODO.md +++ b/TODO.md @@ -1,12 +1,16 @@ - [ ] Integrate with other data stores - [ ] SQLite - [ ] PostgreSQL + - [ ] DuckDB - [X] Access control with resources - [ ] Custom definitions - [ ] Middlewares - [ ] Request decorators - [ ] Status messages - [ ] Response bodies (in case of error messages) + - [ ] Resource operations + - [ ] Single item + - [ ] Collection - [ ] Content negotiation - [X] Language - [X] Charset @@ -32,3 +36,4 @@ - [ ] Swagger docs plugin - [ ] Plugin support - [ ] Add option to reject content negotiation params with `406 Not Acceptable` +- [ ] Add `fast-check` for property-based checking diff --git a/packages/core/test/features/decorators.test.ts b/packages/core/test/features/decorators.test.ts index 09d8b2e..f1824ec 100644 --- a/packages/core/test/features/decorators.test.ts +++ b/packages/core/test/features/decorators.test.ts @@ -71,6 +71,7 @@ describe('decorators', () => { resolve(); }); + // TODO add .inject() method server.listen({ port: PORT }); diff --git a/packages/data-sources/duckdb/src/index.ts b/packages/data-sources/duckdb/src/index.ts index 37a3f65..a25fba7 100644 --- a/packages/data-sources/duckdb/src/index.ts +++ b/packages/data-sources/duckdb/src/index.ts @@ -5,9 +5,38 @@ import assert from 'assert'; type ID = number; +interface DuckDbDataSourceBase< + ID, + Schema extends v.BaseSchema = v.BaseSchema, + CurrentName extends string = string, + CurrentRouteName extends string = string, + Data extends object = v.Output, +> extends DataSource { + resource?: Resource; + + db?: Database; +} + export const AutoincrementIdConfig = { - // TODO - generationStrategy: async () => {}, + // TODO add options: https://duckdb.org/docs/sql/statements/create_sequence + generationStrategy: async (dataSourceRaw: DataSource) => { + const dataSource = dataSourceRaw as DuckDbDataSourceBase; + assert(typeof dataSource.db !== 'undefined'); + assert(typeof dataSource.resource !== 'undefined'); + const idAttr = dataSource.resource.state.shared.get('idAttr'); + assert(typeof idAttr === 'string'); + + const con = await dataSource.db.connect(); + const stmt = await con.prepare(` + SELECT nextval('${dataSource.resource.state.routeName}_sequence') as ${idAttr}; + `); + const [v] = await stmt.all(); + return v[idAttr]; + }, schema: v.number(), serialize: (v: unknown) => v?.toString() ?? '', deserialize: (v: string) => Number(v), @@ -18,8 +47,8 @@ export class DuckDbDataSource< CurrentName extends string = string, CurrentRouteName extends string = string, Data extends object = v.Output, -> implements DataSource { - private resource?: Resource implements DuckDbDataSourceBase { + resource?: Resource | undefined; @@ -42,7 +72,7 @@ export class DuckDbDataSource< const idSchema = idConfig.schema as v.BaseSchema; - this.db = await Database.create('test.db'); + this.db = await Database.create(this.path); const clause = `CREATE TABLE IF NOT EXISTS ${this.resource.state.routeName}`; const resourceSchema = this.resource.schema as unknown as v.ObjectSchema; const tableSchema = Object.entries(resourceSchema.entries) @@ -51,7 +81,16 @@ export class DuckDbDataSource< return [columnName, columnDef.type].join(' '); }) .join(','); - const sql = `${clause} (${idAttr} ${idSchema.type},${tableSchema});`; + let sequenceSql = ''; + let defaultValue = ''; + let idType = 'STRING'; + if (idSchema.type === 'number') { + // TODO support more sequence statements: https://duckdb.org/docs/sql/statements/create_sequence + sequenceSql = `CREATE SEQUENCE IF NOT EXISTS ${this.resource.state.routeName}_sequence START 1;`; + defaultValue = `DEFAULT nextval('${this.resource.state.routeName}_sequence')`; + idType = 'INTEGER'; + } + const sql = `${sequenceSql}${clause} (${idAttr} ${idType} ${defaultValue},${tableSchema});`; const con = await this.db.connect(); const stmt = await con.prepare(sql); await stmt.run(); @@ -77,7 +116,8 @@ export class DuckDbDataSource< this.resource = resource as any; } - async getMultiple() { + async getMultiple(query) { + // TODO translate query to SQL statements assert(typeof this.db !== 'undefined'); assert(typeof this.resource !== 'undefined'); @@ -90,7 +130,8 @@ export class DuckDbDataSource< } async newId() { - const idConfig = this.resource?.state.shared.get('idConfig') as ResourceIdConfig; + assert(typeof this.resource !== 'undefined'); + const idConfig = this.resource.state.shared.get('idConfig') as ResourceIdConfig; assert(typeof idConfig !== 'undefined'); const theNewId = await idConfig.generationStrategy(this); @@ -107,7 +148,18 @@ export class DuckDbDataSource< const idAttr = this.resource.state.shared.get('idAttr'); assert(typeof idAttr === 'string'); - const { [idAttr]: _, ...effectiveData } = data as Record; + let theId: any; + const { [idAttr]: dataId } = data as Record; + if (typeof dataId !== 'undefined') { + theId = idConfig.deserialize((data as Record)[idAttr]); + } else { + const newId = await this.newId(); + theId = idConfig.deserialize(newId.toString()); + } + const effectiveData = { + ...data, + } as Record; + effectiveData[idAttr] = theId; const clause = `INSERT INTO ${this.resource.state.routeName}`; const keys = Object.keys(effectiveData).join(','); @@ -115,14 +167,14 @@ export class DuckDbDataSource< const sql = `${clause} (${keys}) VALUES (${values});`; const con = await this.db.connect(); const stmt = await con.prepare(sql); - await stmt.all(); + await stmt.run(); const newData = { ...effectiveData }; return newData as Data; } - async getTotalCount() { + async getTotalCount(query) { assert(typeof this.db !== 'undefined'); assert(typeof this.resource !== 'undefined'); @@ -131,10 +183,10 @@ export class DuckDbDataSource< const con = await this.db.connect(); const stmt = await con.prepare(` - SELECT COUNT(*) FROM ?; - `, this.resource.state.routeName); + SELECT COUNT(*) as c FROM ${this.resource.state.routeName}; + `); const [data] = await stmt.all(); - return data as unknown as number; + return data['c'] as unknown as number; } async getById(id: ID) { @@ -148,11 +200,11 @@ export class DuckDbDataSource< const stmt = await con.prepare(` SELECT * FROM ${this.resource.state.routeName} WHERE ${idAttr} = ${JSON.stringify(id).replace(/"/g, "'")}; `); - const data = await stmt.all(); - return data as Data; + const [data = null] = await stmt.all(); + return data as Data | null; } - async getSingle() { + async getSingle(query) { assert(typeof this.db !== 'undefined'); assert(typeof this.resource !== 'undefined'); @@ -160,8 +212,8 @@ export class DuckDbDataSource< const stmt = await con.prepare(` SELECT * FROM ${this.resource.state.routeName} LIMIT 1; `); - const [data] = await stmt.all(); - return data as Data; + const [data = null] = await stmt.all(); + return data as Data | null; } async delete(id: ID) { diff --git a/packages/examples/duckdb/.gitignore b/packages/examples/duckdb/.gitignore index 53992de..b9f3b75 100644 --- a/packages/examples/duckdb/.gitignore +++ b/packages/examples/duckdb/.gitignore @@ -105,3 +105,5 @@ dist .tern-port .npmrc +*.db +*.db.wal diff --git a/packages/examples/duckdb/src/index.ts b/packages/examples/duckdb/src/index.ts index 228446a..9d76edf 100644 --- a/packages/examples/duckdb/src/index.ts +++ b/packages/examples/duckdb/src/index.ts @@ -27,8 +27,9 @@ const app = application({ .resource(Post); const backend = app.createBackend({ - dataSource: new DuckDbDataSource(), + dataSource: new DuckDbDataSource('test.db'), }) + .showTotalItemCountOnGetCollection() .throwsErrorOnDeletingNotFound(); const server = backend.createHttpServer({ diff --git a/packages/examples/duckdb/test.db b/packages/examples/duckdb/test.db deleted file mode 100644 index 72cf332774af795852ea8d7d7d2b07de2df31bd2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 274432 zcmeI)&2AD=6ae4@LTp@`#%Nsl1`QZvT(~gNg^As|=*9+_;U{UKk*RCnz_|4-d<#(f z1{z<%g|5e;1I3z>xTyvE-OK{cJ#+7wZx+lUg!+x0@#f)+%_q+vKYJ8^H(ocknvJc- z(&W31m(6<*?kCOW(i9T}2oNAZfB*pk1PBlyK!CtM6sUile8_IU{ZTo4&nt^~t<2as zJ5iJX0RjXF5FkK+009C72oP9Zfm#3mZC+vi$C$?>^#2PBuLuwzK!5-N0t5&UAV7e? zdI`M$^7CQu=+5E1?N6fCj%pH>B&wab-XC;^ul8G6e>m73wR`D)s~<7V{Vl0h1P3SK z;>78ZMlDN6StsVRHi73uIRm-)5?ryu4wf2VHSw(riUN=Ow1PBlyK!5-N0t5&UAVA>!1d5Kl zTD0v+L>iUjzIr_Eu*)%tlOO$mjQA9l&*gUj$Ejtr|L*3~o$c-OtIx^=2oNAZfB*pk z1PH9OK-oKJDpvX?DejUA%zOJwdigG!009C72oP950go^1ch6KzfB*pk7bvjogLCme ze`RssXNebBs;vnSAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U MAV7csf&WzC7gOuV#Q*>R diff --git a/packages/examples/duckdb/test.db.wal b/packages/examples/duckdb/test.db.wal deleted file mode 100644 index becce1bb8eef4f60d655a3ad8394f3c5b517b3e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 836 zcmYdcNJ?d3`u|^q0RlX~Rz7{|&~ZG4K{AzrB{wlMFO7k70^A;{IXU?XB^jwjssF)% b6>8w~f3-#WZ*AjJ7*a-EG8!gBEu8=WYZWA0