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 72cf332..0000000 Binary files a/packages/examples/duckdb/test.db and /dev/null differ diff --git a/packages/examples/duckdb/test.db.wal b/packages/examples/duckdb/test.db.wal deleted file mode 100644 index becce1b..0000000 Binary files a/packages/examples/duckdb/test.db.wal and /dev/null differ