From 76857884fbe9d3995c8dc5ca3fb8c9a089ebc303 Mon Sep 17 00:00:00 2001 From: "Marc J. Schmidt" Date: Wed, 10 Jul 2024 17:12:51 +0200 Subject: [PATCH] feat(orm): new selector API, still work in progress 6 --- packages/app/tests/module.spec.ts | 101 ++++++--- packages/example-app/src/views/user-list.tsx | 2 +- packages/injector/tests/injector2.spec.ts | 38 +++- packages/postgres/src/client.ts | 214 +++++++++++++------ packages/postgres/tests/postgres.spec.ts | 8 +- packages/sql/src/migration.ts | 8 +- packages/type/src/serializer.ts | 3 +- 7 files changed, 267 insertions(+), 107 deletions(-) diff --git a/packages/app/tests/module.spec.ts b/packages/app/tests/module.spec.ts index 36ea6cd1b..852ad7918 100644 --- a/packages/app/tests/module.spec.ts +++ b/packages/app/tests/module.spec.ts @@ -2,8 +2,9 @@ import { expect, test } from '@jest/globals'; import { Minimum, MinLength } from '@deepkit/type'; import { provide } from '@deepkit/injector'; import { ServiceContainer } from '../src/service-container.js'; -import { ClassType } from '@deepkit/core'; +import { ClassType, getClassTypeFromInstance } from '@deepkit/core'; import { AppModule, createModule } from '../src/module.js'; +import { App } from '../src/app.js'; class MyModuleConfig { param1!: string & MinLength<5>; @@ -18,9 +19,9 @@ class ModuleService { class MyModule extends createModule({ config: MyModuleConfig, providers: [ - ModuleService + ModuleService, ], - exports: [ModuleService] + exports: [ModuleService], }, 'myModule') { } @@ -71,25 +72,25 @@ function getServiceOnNewServiceContainer(module: AppModule, service: Cla test('import', () => { { expect(() => getServiceOnNewServiceContainer(new MyAppModule, ModuleService)).toThrow( - 'Configuration for module myModule is invalid. Make sure the module is correctly configured. Error: myModule.param1(type): Not a string' + 'Configuration for module myModule is invalid. Make sure the module is correctly configured. Error: myModule.param1(type): Not a string', ); } { expect(() => getServiceOnNewServiceContainer(new MyAppModule({ myModule: { param1: '23' } }), ModuleService)).toThrow( - 'Configuration for module myModule is invalid. Make sure the module is correctly configured. Error: myModule.param1(minLength): Min length is 5' + 'Configuration for module myModule is invalid. Make sure the module is correctly configured. Error: myModule.param1(minLength): Min length is 5', ); } { expect(() => getServiceOnNewServiceContainer(new MyAppModule({ myModule: { param1: '12345' } }), ModuleService)).toThrow( - 'Configuration for module myModule is invalid. Make sure the module is correctly configured. Error: myModule.param2(type): Not a number' + 'Configuration for module myModule is invalid. Make sure the module is correctly configured. Error: myModule.param2(type): Not a number', ); } { expect(() => getServiceOnNewServiceContainer(new MyAppModule({ myModule: { param1: '12345', param2: 55 } }), ModuleService)).toThrow( - 'Configuration for module myModule is invalid. Make sure the module is correctly configured. Error: myModule.param2(minimum): Number needs to be greater than or equal to 100' + 'Configuration for module myModule is invalid. Make sure the module is correctly configured. Error: myModule.param2(minimum): Number needs to be greater than or equal to 100', ); } @@ -114,28 +115,28 @@ test('basic configured', () => { { const myService = getServiceOnNewServiceContainer(createConfiguredApp().configure({ - debug: false + debug: false, }), MyService); expect(myService.isDebug()).toBe(false); } { const myService = getServiceOnNewServiceContainer(createConfiguredApp().configure({ - debug: true + debug: true, }), MyService); expect(myService.isDebug()).toBe(true); } { const myService2 = getServiceOnNewServiceContainer(createConfiguredApp().configure({ - debug: false + debug: false, }), MyService2); expect(myService2.debug).toBe(false); } { const myService2 = getServiceOnNewServiceContainer(createConfiguredApp().configure({ - debug: true + debug: true, }), MyService2); expect(myService2.debug).toBe(true); } @@ -213,7 +214,7 @@ test('same module loaded twice', () => { class ApiModule extends createModule({ config: Config, - providers: [Service] + providers: [Service], }) { } @@ -237,7 +238,7 @@ test('same module loaded twice', () => { imports: [ a, b, - ] + ], }); const serviceContainer = new ServiceContainer(app); @@ -250,19 +251,20 @@ test('same module loaded twice', () => { }); test('interface provider can be exported', () => { - interface Test {} + interface Test { + } - const TEST = {}; + const TEST = {}; - const Test = provide({ useValue: TEST }); + const Test = provide({ useValue: TEST }); - const test = new AppModule({ providers: [Test], exports: [Test] }); + const test = new AppModule({ providers: [Test], exports: [Test] }); - const app = new AppModule({ imports: [test] }); + const app = new AppModule({ imports: [test] }); const serviceContainer = new ServiceContainer(app); - expect(serviceContainer.getInjector(app).get()).toBe(TEST); + expect(serviceContainer.getInjector(app).get()).toBe(TEST); }); test('non-exported providers can not be overwritten', () => { @@ -276,8 +278,8 @@ test('non-exported providers can not be overwritten', () => { const app = new AppModule({ providers: [Overwritten, { provide: SubClass, useClass: Overwritten }], imports: [ - sub - ] + sub, + ], }); const serviceContainer = new ServiceContainer(app); @@ -297,8 +299,8 @@ test('exported providers can not be overwritten', () => { const app = new AppModule({ providers: [Overwritten, { provide: SubClass, useClass: Overwritten }], imports: [ - sub - ] + sub, + ], }); const serviceContainer = new ServiceContainer(app); @@ -345,7 +347,7 @@ test('change config of a imported module dynamically', () => { class DatabaseModule extends createModule({ config: DatabaseConfig, - providers: [Query] + providers: [Query], }) { process() { if (this.config.logging) { @@ -359,7 +361,7 @@ test('change config of a imported module dynamically', () => { } class ApiModule extends createModule({ - config: ApiConfig + config: ApiConfig, }) { imports = [new DatabaseModule({ logging: false })]; @@ -405,7 +407,7 @@ test('scoped injector', () => { } const module = new AppModule({ - providers: [{ provide: Service, scope: 'http' }] + providers: [{ provide: Service, scope: 'http' }], }); const serviceContainer = new ServiceContainer(new AppModule({ imports: [module] })); @@ -457,3 +459,50 @@ test('functional modules', () => { expect(serviceContainer.getInjectorContext().get('title')).toBe('Peter'); }); + +test('configureProvider', () => { + class Service { + } + + class ScopedCounter { + instantiation = new Map(); + + instantiated(service: any) { + this.instantiation.set(service, (this.instantiation.get(service) || 0) + 1); + } + + count(service: any) { + return this.instantiation.get(service) || 0; + } + } + + const module = new AppModule({ + providers: [ + { provide: ScopedCounter, scope: 'http' }, + { provide: Service, scope: 'http' }, + ], + exports: [Service, ScopedCounter], + }); + + const root = new App({ + imports: [module], + }); + + root.configureProvider((service, counter: ScopedCounter) => { + counter.instantiated(getClassTypeFromInstance(service)); + }); + + { + const scope = root.getInjectorContext().createChildScope('http'); + expect(scope.get(ScopedCounter).count(Service)).toBe(0); + const service1 = scope.get(Service); + expect(scope.get(ScopedCounter).count(Service)).toBe(1); + } + + { + const scope = root.getInjectorContext().createChildScope('http'); + expect(scope.get(ScopedCounter).count(Service)).toBe(0); + const service1 = scope.get(Service); + expect(scope.get(ScopedCounter).count(Service)).toBe(1); + } +}); diff --git a/packages/example-app/src/views/user-list.tsx b/packages/example-app/src/views/user-list.tsx index e5a0523b4..9d0ee9e59 100644 --- a/packages/example-app/src/views/user-list.tsx +++ b/packages/example-app/src/views/user-list.tsx @@ -6,7 +6,7 @@ async function Title(props: { title: string }) { } export async function UserList(props: { error?: string }, children: any, database: SQLiteDatabase) { - const users = await database.query(User).select('username', 'created', 'id').find(); + const users = await database.singleQuery(User).find(); return
diff --git a/packages/injector/tests/injector2.spec.ts b/packages/injector/tests/injector2.spec.ts index 38faad522..55f84bf26 100644 --- a/packages/injector/tests/injector2.spec.ts +++ b/packages/injector/tests/injector2.spec.ts @@ -821,18 +821,19 @@ test('configure provider replace', () => { class Service { } - class Replaced extends Service {} + class Replaced extends Service { + } const root = new InjectorModule([Service]); root.configureProvider(service => { return new Replaced; - }, {replace: true}); + }, { replace: true }); const injector = new InjectorContext(root); const service = injector.get(Service); expect(service).toBeInstanceOf(Replaced); -}) +}); test('configure provider additional services scopes', () => { class Service { @@ -1718,3 +1719,34 @@ test('deep config index direct sub class access', () => { const database = injector.get(Database); expect(database.url).toBe('localhost'); }); + +test('scoped instantiations', () => { + class Service { + } + + const subModule = new InjectorModule([ + { provide: Service, scope: 'http' }, + ]).addExport(Service); + + const root = new InjectorModule().addImport(subModule); + const injector = new InjectorContext(root); + + expect(injector.instantiationCount(Service)).toBe(0); + expect(injector.instantiationCount(Service, undefined, 'http')).toBe(0); + + { + const scope = injector.createChildScope('http'); + const cookie1 = scope.get(Service); + + expect(injector.instantiationCount(Service)).toBe(0); + expect(injector.instantiationCount(Service, undefined, 'http')).toBe(1); + } + + { + const scope = injector.createChildScope('http'); + const cookie1 = scope.get(Service); + + expect(injector.instantiationCount(Service)).toBe(0); + expect(injector.instantiationCount(Service, undefined, 'http')).toBe(2); + } +}); diff --git a/packages/postgres/src/client.ts b/packages/postgres/src/client.ts index a49cc6a8e..62557ac16 100644 --- a/packages/postgres/src/client.ts +++ b/packages/postgres/src/client.ts @@ -2,15 +2,21 @@ import { connect, createConnection, Socket } from 'net'; import { arrayRemoveItem, asyncOperation, CompilerContext, decodeUTF8, formatError } from '@deepkit/core'; import { DatabaseError, DatabaseTransaction, SelectorState } from '@deepkit/orm'; import { + getDeepConstructorProperties, getSerializeFunction, getTypeJitContainer, + isAutoIncrementType, isPropertyType, + memberNameToString, ReceiveType, + ReflectionClass, ReflectionKind, resolveReceiveType, Type, TypeClass, TypeObjectLiteral, + TypeProperty, + TypePropertySignature, } from '@deepkit/type'; import { connect as createTLSConnection, TLSSocket } from 'tls'; import { Host, PostgresClientConfig } from './config.js'; @@ -37,14 +43,37 @@ function readUint16BE(data: Uint8Array, offset: number = 0): number { return data[offset + 1] + (data[offset] * 2 ** 8); } -function buildDeserializerForType(type: Type): (message: Uint8Array) => any { +function getPropertyDeserializer(context: CompilerContext, property: TypeProperty | TypePropertySignature, varName: string): string { + switch (property.type.kind) { + case ReflectionKind.string: + return `${varName} = decodeUTF8(data, offset + 4, offset + 4 + length)`; + case ReflectionKind.number: + if (isAutoIncrementType(property.type)) { + return `${varName} = length === 4 ? view.getInt32(offset + 4) : view.getInt64(offset + 4)`; + } + return `${varName} = length === 4 ? view.getFloat32(offset + 4) : view.getFloat64(offset + 4)`; + case ReflectionKind.boolean: + return `${varName} = data[offset + 4] === 1`; + case ReflectionKind.class: + case ReflectionKind.union: + case ReflectionKind.array: + case ReflectionKind.objectLiteral: + return `${varName} = parseJson(decodeUTF8(data, offset + 4 + 1, offset + 4 + length))`; + default: + throw new Error('Unsupported property type ' + property.type); + } +} + +function buildDeserializer(selector: SelectorState): (message: Uint8Array, rows: any[]) => void { + const type = selector.schema.type; if (type.kind !== ReflectionKind.class && type.kind !== ReflectionKind.objectLiteral) { throw new Error('Invalid type for deserialization'); } const context = new CompilerContext(); const lines: string[] = []; - const props: string[] = []; + const inlineProps: string[] = []; + const assignProps: string[] = []; context.set({ DataView, decodeUTF8, @@ -52,56 +81,97 @@ function buildDeserializerForType(type: Type): (message: Uint8Array) => any { parseJson: JSON.parse, }); - for (const property of type.types) { - const varName = context.reserveVariable(); - if (!isPropertyType(property)) continue; - const field = property.type; + const isClass = type.kind === ReflectionKind.class; + const handledPropertiesInConstructor: string[] = []; + const constructorArguments: string[] = []; + const preLines: string[] = []; + const postLines: string[] = []; + const v = context.reserveName('v'); + + const properties = type.types.map(v => [context.reserveVariable(), v]); - if (field.kind === ReflectionKind.number) { - lines.push(` + for (const [varName, property] of properties) { + if (!isPropertyType(property)) continue; + lines.push(` length = readUint32BE(data, offset); - ${varName} = length === 4 ? view.getFloat32(offset + 4) : view.getFloat64(offset + 4); + ${getPropertyDeserializer(context, property, varName)} offset += 4 + length; - `); - } - if (field.kind === ReflectionKind.boolean) { - lines.push(` - ${varName} = data[offset + 4] === 1; - offset += 4 + 1; - `); + `); + + if (isClass) { + assignProps.push(`${v}.${memberNameToString(property.name)} = ${varName};`); + } else { + inlineProps.push(`${String(property.name)}: ${varName}`); } - if (field.kind === ReflectionKind.string) { - lines.push(` - length = readUint32BE(data, offset); - ${varName} = decodeUTF8(data, offset + 4, offset + 4 + length); - offset += 4 + length; - `); + } + + let createObject = ''; + if (isClass) { + const clazz = ReflectionClass.from(type.classType); + const constructor = clazz.getConstructorOrUndefined(); + const classType = context.reserveConst(type.classType); + + if (!clazz.disableConstructor && constructor) { + handledPropertiesInConstructor.push(...getDeepConstructorProperties(type).map(v => String(v.name))); + const parameters = constructor.getParameters(); + for (const parameter of parameters) { + if (!parameter.isProperty()) { + constructorArguments.push('undefined'); + continue; + } + + const property = clazz.getProperty(parameter.getName()); + if (!property) continue; + + //todo handle is skipped + + const entry = properties.find(v => v[1] === property); + if (!entry) continue; + + constructorArguments.push(entry[0]); + } } - if (field.kind === ReflectionKind.class || field.kind === ReflectionKind.union - || field.kind === ReflectionKind.array || field.kind === ReflectionKind.objectLiteral) { - lines.push(` - length = readUint32BE(data, offset); - ${varName} = parseJson(decodeUTF8(data, offset + 4 + 1, offset + 4 + length)); - offset += 4 + length; - `); + + if (clazz.disableConstructor) { + createObject = `Object.create(${classType}.prototype);`; + for (const property of clazz.getProperties()) { + if (property.property.kind !== ReflectionKind.property || property.property.default === undefined) continue; + const defaultFn = context.reserveConst(property.property.default); + createObject += `\n${v}.${memberNameToString(property.name)} = ${defaultFn}.apply(${v});`; + } + } else { + createObject = `new ${classType}(${constructorArguments.join(', ')})`; + // preLines.push(`const oldCheck = typeSettings.unpopulatedCheck; typeSettings.unpopulatedCheck = UnpopulatedCheck.None;`); + // postLines.push(`typeSettings.unpopulatedCheck = oldCheck;`); } - props.push(`${String(property.name)}: ${varName},`); + } else { + createObject = `{${inlineProps.join(', ')}}`; } + const code = ` const view = new DataView(data.buffer, data.byteOffset, data.byteLength); let offset = 1 + 4 + 2; // Skip type, length, and field count let length = 0; + ${lines.join('\n')} - result.push({ - ${props.join('\n')} - }); + ${preLines.join('\n')} + const ${v} = ${createObject}; + ${assignProps.join('\n')} + ${postLines.join('\n')} + + result.push(${v}); `; + + console.log(code); return context.build(code, 'data', 'result'); } -function buildDeserializer(selector: SelectorState): (message: Uint8Array) => any { +function buildDeserializerForType(type: Type): (message: Uint8Array, result: any[]) => void { + if (type.kind !== ReflectionKind.class && type.kind !== ReflectionKind.objectLiteral) { + throw new Error('Invalid type for deserialization'); + } const context = new CompilerContext(); const lines: string[] = []; const props: string[] = []; @@ -112,39 +182,15 @@ function buildDeserializer(selector: SelectorState): (message: Uint8Array) => an parseJson: JSON.parse, }); - for (const field of selector.schema.getProperties()) { + for (const field of type.types) { + if (!isPropertyType(field)) continue; const varName = context.reserveVariable(); - - if (field.type.kind === ReflectionKind.number) { - lines.push(` - length = readUint32BE(data, offset); - ${varName} = length === 4 ? view.getFloat32(offset + 4) : view.getFloat64(offset + 4); - offset += 4 + length; - `); - } - if (field.type.kind === ReflectionKind.boolean) { - lines.push(` - ${varName} = data[offset + 4] === 1; - offset += 4 + 1; - `); - } - if (field.type.kind === ReflectionKind.string) { - lines.push(` - length = readUint32BE(data, offset); - // ${varName} = decodeUTF8(data, offset + 4, offset + 4 + length); - ${varName} = ''; - offset += 4 + length; - `); - } - if (field.type.kind === ReflectionKind.class || field.type.kind === ReflectionKind.union - || field.type.kind === ReflectionKind.array || field.type.kind === ReflectionKind.objectLiteral) { - lines.push(` + lines.push(` length = readUint32BE(data, offset); - ${varName} = parseJson(decodeUTF8(data, offset + 4 + 1, offset + 4 + length)); + ${getPropertyDeserializer(context, field, varName)} offset += 4 + length; - `); - } - props.push(`${field.name}: ${varName},`); + `); + props.push(`${String(field.name)}: ${varName},`); } const code = ` @@ -160,11 +206,21 @@ function buildDeserializer(selector: SelectorState): (message: Uint8Array) => an return context.build(code, 'data', 'result'); } +function buildAutoIncrementDeserializer(type: Type) { + const schema = ReflectionClass.fromType(type); + return buildDeserializerForType({ + kind: ReflectionKind.objectLiteral, + types: [ + schema.getPrimary().property, + ], + }); +} + export class PostgresClientPrepared { created = false; cache?: Buffer; - deserialize: (message: Uint8Array) => any; + deserialize: (message: Uint8Array, rows: any[]) => void; constructor( public client: PostgresClientConnection, @@ -709,7 +765,7 @@ export abstract class Command { // console.log('columns', columns); // for (let i = 0; i < columns; i++) { // const length = readUint32BE(response, offset); - // console.log('column', i, length, Buffer.from(response).toString('hex', offset, offset + length)); + // console.log('column', i, length, Buffer.from(response).toString('hex', offset + 4, offset + 4 + length)); // offset += 4 + length; // } break; @@ -763,8 +819,9 @@ export class InsertCommand extends Command { placeholders.push(values.join(', ')); } + const returning = this.prepared.primaryKey.autoIncrement ? 'RETURNING ' + this.prepared.primaryKey.columnNameEscaped : ''; const sql = `INSERT INTO ${this.prepared.tableNameEscaped} (${names.join(', ')}) - VALUES (${placeholders.join('), (')})`; + VALUES (${placeholders.join('), (')}) ${returning}`; const statement = ''; @@ -774,10 +831,18 @@ export class InsertCommand extends Command { sendExecute('', 0), syncMessage, ]); - await this.sendAndWait(message); - // parse data rows: auto-incremented columns + console.log('insert', sql); + const deserializerAutoIncrement = buildAutoIncrementDeserializer(this.prepared.type); + const rows: any[] = []; + await this.sendAndWait(message, (v) => deserializerAutoIncrement(v, rows)); + console.log('rows', rows); + for (let i = 0; i < this.items.length; i++) { + this.items[i][this.prepared.primaryKey.name] = rows[i][this.prepared.primaryKey.name]; + } + + // parse data rows: auto-incremented columns return 0; } } @@ -800,7 +865,7 @@ export class FindCommand extends Command { : () => { //todo more complex deserializer }; - console.log('new FindCommand'); + console.log('new FindCommand', sql); } setParameters(params: any[]) { @@ -828,7 +893,14 @@ export class FindCommand extends Command { } const rows: any[] = []; - await this.sendAndWait(this.cache, (data) => this.deserialize(data, rows)); + await this.sendAndWait(this.cache, (data) => { + try { + this.deserialize(data, rows); + } catch (e) { + console.log('deserialize error', e); + throw e; + } + }); return rows; } } diff --git a/packages/postgres/tests/postgres.spec.ts b/packages/postgres/tests/postgres.spec.ts index 73ca71387..a8135d44f 100644 --- a/packages/postgres/tests/postgres.spec.ts +++ b/packages/postgres/tests/postgres.spec.ts @@ -2,6 +2,7 @@ import { AutoIncrement, entity, PrimaryKey, Reference } from '@deepkit/type'; import { expect, test } from '@jest/globals'; import pg from 'pg'; import { databaseFactory } from './factory.js'; +import { join } from '@deepkit/orm'; test('count', async () => { const pool = new pg.Pool({ @@ -50,6 +51,7 @@ test('bool and json', async () => { } const m = await database.singleQuery(Model).findOne(); + expect(m).toBeInstanceOf(Model); expect(m).toMatchObject({ flag: true, doc: { flag: true } }); }); @@ -84,10 +86,14 @@ test('join', async () => { users.push(user); } + // await database.persist(...groups); await database.persist(...groups, ...users); { - const users = await database.query(User).find(); + const users = await database.singleQuery(User, (m) => { + const g = join(m.group); + return [m, g]; + }).find(); } }); diff --git a/packages/sql/src/migration.ts b/packages/sql/src/migration.ts index f04ca6314..b27e59923 100644 --- a/packages/sql/src/migration.ts +++ b/packages/sql/src/migration.ts @@ -1,13 +1,13 @@ +import { DatabaseEntityRegistry, DatabaseError } from '@deepkit/orm'; +import { DatabaseModel } from './schema/table.js'; +import { DefaultPlatform } from './platform/default-platform.js'; + /** * Creates (and re-creates already existing) tables in the database. * This is only for testing purposes useful. * * WARNING: THIS DELETES ALL AFFECTED TABLES AND ITS CONTENT. */ -import { DatabaseEntityRegistry, DatabaseError } from '@deepkit/orm'; -import { DatabaseModel } from './schema/table.js'; -import { DefaultPlatform } from './platform/default-platform.js'; - export async function createTables( entityRegistry: DatabaseEntityRegistry, pool: { getConnection(): Promise<{ run(sql: string): Promise; release(): void }> }, diff --git a/packages/type/src/serializer.ts b/packages/type/src/serializer.ts index 661682bd4..a6f2690eb 100644 --- a/packages/type/src/serializer.ts +++ b/packages/type/src/serializer.ts @@ -1165,6 +1165,8 @@ export function serializeObjectLiteral(type: TypeObjectLiteral | TypeClass, stat const constructorArguments: string[] = []; const handledPropertiesInConstructor: string[] = []; const preLines: string[] = []; + const postLines: string[] = []; + if (state.isDeserialization && type.kind === ReflectionKind.class) { const clazz = ReflectionClass.from(type.classType); const constructor = clazz.getConstructorOrUndefined(); @@ -1264,7 +1266,6 @@ export function serializeObjectLiteral(type: TypeObjectLiteral | TypeClass, stat } let createObject = '{}'; - const postLines: string[] = []; if (state.isDeserialization && type.kind === ReflectionKind.class) { const classType = state.compilerContext.reserveConst(type.classType); const clazz = ReflectionClass.from(type.classType);