From 7bab4a965dee662c47bcf20c5b2566857c31d5fa Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Sun, 13 Oct 2024 01:22:16 +0800 Subject: [PATCH 1/3] feat(minato): disposable model.extend --- packages/core/src/database.ts | 27 ++- packages/core/src/model.ts | 24 ++- packages/mongo/tests/index.spec.ts | 3 +- packages/tests/src/index.ts | 30 ++- packages/tests/src/json.ts | 47 ++--- packages/tests/src/model.ts | 296 +++++++++++++++-------------- packages/tests/src/object.ts | 10 +- packages/tests/src/query.ts | 26 +-- packages/tests/src/relation.ts | 238 +++++++++++------------ packages/tests/src/selection.ts | 28 +-- packages/tests/src/transaction.ts | 24 +-- packages/tests/src/update.ts | 60 +++--- 12 files changed, 437 insertions(+), 376 deletions(-) diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts index 4607195a..3145e5af 100644 --- a/packages/core/src/database.ts +++ b/packages/core/src/database.ts @@ -133,6 +133,7 @@ export class Database extends Servi } extend>(name: K, fields: Field.Extension, config: Partial>> = {}) { + const disposables: (() => void)[] = [] let model = this.tables[name] if (!model) { model = this.tables[name] = new Model(name) @@ -142,7 +143,7 @@ export class Database extends Servi this.parseField(field, transformer, undefined, value => field = fields[key] = value) if (typeof field === 'object') field.transformers = transformer }) - model.extend(fields, config) + disposables.push(model.extend(fields, config)) if (makeArray(model.primary).every(key => key in fields)) { defineProperty(model, 'ctx', this.ctx) } @@ -155,10 +156,12 @@ export class Database extends Servi ;(model.fields[key] = Field.parse('expr')).relation = relation if (def.target) { (relmodel.fields[def.target] ??= Field.parse('expr')).relation = inverse + disposables.unshift(() => delete relmodel.fields[def.target!]) } if (relation.type === 'oneToOne' || relation.type === 'manyToOne') { relation.fields.forEach((x, i) => { + if (!model.fields[x]) disposables.unshift(() => delete model.fields[x]) model.fields[x] ??= { ...relmodel.fields[relation.references[i]] } as any if (!relation.required) { model.fields[x]!.nullable = true @@ -171,7 +174,7 @@ export class Database extends Servi const shared = Object.entries(relation.shared).map(([x, y]) => [Relation.buildSharedKey(x, y), model.fields[x]!.deftype] as const) const fields = relation.fields.map(x => [Relation.buildAssociationKey(x, name), model.fields[x]!.deftype] as const) const references = relation.references.map(x => [Relation.buildAssociationKey(x, relation.table), relmodel.fields[x]?.deftype] as const) - this.extend(assocTable as any, { + disposables.push(this.extend(assocTable as any, { ...Object.fromEntries([...shared, ...fields, ...references]), [name]: { type: 'manyToOne', @@ -187,18 +190,32 @@ export class Database extends Servi }, } as any, { primary: [...shared, ...fields, ...references].map(x => x[0]) as any, - }) + })) } }) // use relation field as primary if (Array.isArray(model.primary) || model.fields[model.primary]!.relation) { model.primary = deduplicate(makeArray(model.primary).map(key => model.fields[key]!.relation?.fields || key).flat()) } - model.unique = model.unique.map(keys => typeof keys === 'string' ? model.fields[keys]!.relation?.fields || keys - : keys.map(key => model.fields[key]!.relation?.fields || key).flat()) + model.unique = model.unique.map(keys => { + if (typeof keys === 'string') { + return (model.fields[keys]!.relation?.fields && disposables.unshift(() => model.unique.splice(model.unique.indexOf(keys), 1)), + model.fields[keys]!.relation?.fields || keys) + } else { + const pred = keys.some(key => model.fields[key]!.relation?.fields) + if (pred) { + const newKeys = keys.map(key => model.fields[key]!.relation?.fields || key).flat() + disposables.unshift(() => model.unique.splice(model.unique.indexOf(newKeys), 1)) + return newKeys + } else { + return keys + } + } + }) this.prepareTasks[name] = this.prepare(name) ;(this.ctx as Context).emit('model', name) + return this.ctx.effect(() => () => disposables.splice(0).forEach(dispose => dispose())) } private _parseField(field: any, transformers: Driver.Transformer[] = [], setInitial?: (value) => void, setField?: (value) => void): Type { diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index ebc2bf2e..d9af09c3 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -272,19 +272,25 @@ export class Model { this.foreign = {} } - extend(fields: Field.Extension, config?: Partial): void + extend(fields: Field.Extension, config?: Partial): () => void extend(fields = {}, config: Partial = {}) { - const { primary, autoInc, unique = [], indexes = [], foreign, callback } = config + const { primary, autoInc, unique = [], indexes = [], foreign = {}, callback } = config + const disposables: (() => void)[] = [] this.primary = primary || this.primary this.autoInc = autoInc || this.autoInc - unique.forEach(key => this.unique.includes(key) || this.unique.push(key)) - indexes.map(x => this.parseIndex(x)).forEach(index => (this.indexes.some(ind => deepEqual(ind, index))) || this.indexes.push(index)) - Object.assign(this.foreign, foreign) + unique.forEach(key => this.unique.includes(key) || (this.unique.push(key), disposables.push(() => this.unique.splice(this.unique.indexOf(key), 1)))) + indexes.map(x => this.parseIndex(x)).forEach(index => (this.indexes.some(ind => deepEqual(ind, index))) + || (this.indexes.push(index), disposables.push(() => this.indexes.splice(this.indexes.indexOf(index), 1)))) + Object.keys(foreign).forEach(key => Object.hasOwn(this.foreign, key) + || (this.foreign[key] = foreign[key], disposables.push(() => delete this.foreign[key]))) if (callback) this.migrations.set(callback, Object.keys(fields)) for (const key in fields) { + if (makeArray(this.primary).includes(key)) { + disposables.push(() => delete this.ctx?.get('model')?.tables[this.name]) + } this.fields[key] = Field.parse(fields[key]) this.fields[key].deprecated = !!callback } @@ -297,6 +303,14 @@ export class Model { this.checkIndex(this.primary) this.unique.forEach(index => this.checkIndex(index)) this.indexes.forEach(index => this.checkIndex(index)) + + return () => { + for (const key in fields) { + delete this.fields[key] + } + if (callback) this.migrations.delete(callback) + disposables.splice(0).forEach(dispose => dispose()) + } } private parseIndex(index: MaybeArray | Driver.Index): Driver.Index { diff --git a/packages/mongo/tests/index.spec.ts b/packages/mongo/tests/index.spec.ts index 41dacd6b..01b733f0 100644 --- a/packages/mongo/tests/index.spec.ts +++ b/packages/mongo/tests/index.spec.ts @@ -2,6 +2,7 @@ import { Database } from 'minato' import MongoDriver from '@minatojs/driver-mongo' import test from '@minatojs/tests' import Logger from 'reggol' +import { noop } from 'cosmokit' const logger = new Logger('mongo') @@ -19,7 +20,7 @@ describe('@minatojs/driver-mongo', () => { }) after(async () => { - await database.dropAll() + await database.dropAll().catch(noop) await database.stopAll() logger.level = 2 }) diff --git a/packages/tests/src/index.ts b/packages/tests/src/index.ts index 957b822b..6b183ec7 100644 --- a/packages/tests/src/index.ts +++ b/packages/tests/src/index.ts @@ -1,3 +1,4 @@ +import { Context, ForkScope } from 'cordis' import { Database } from 'minato' import ModelOperations from './model' import QueryOperators from './query' @@ -19,7 +20,7 @@ type UnitOptions = (T extends (database: Database, options?: infer R) => any [K in keyof T as Exclude]?: false | UnitOptions } -type Unit = ((database: Database, options?: UnitOptions) => void) & { +type Unit = ((database: Database | ((arg: T) => Database), options: UnitOptions, fork?: boolean) => void) & { [K in keyof T as Exclude]: Unit } @@ -32,9 +33,20 @@ function setValue(obj: any, path: string, value: any) { } } -function createUnit(target: T, root = false): Unit { +function createUnit(target: T, level = 0): Unit { + const title = target['name'] const test: any = (database: Database, options: any = {}) => { function callback() { + let fork: ForkScope | undefined + if (level === 1) { + fork = database['ctx'].plugin({ + inject: ['model'], + name: title, + apply: () => {}, + }) + database = fork.ctx.model + } + if (typeof target === 'function') { target(database, options) } @@ -43,13 +55,19 @@ function createUnit(target: T, root = false): Unit { if (options[key] === false || Keywords.includes(key)) continue test[key](database, options[key]) } + + if (fork) { + after(async () => { + await database.dropAll() + fork.dispose() + }) + } } process.argv.filter(x => x.startsWith('--+')).forEach(x => setValue(options, x.slice(3), true)) process.argv.filter(x => x.startsWith('---')).forEach(x => setValue(options, x.slice(3), false)) - const title = target['name'] - if (!root && title) { + if (level && title) { describe(title.replace(/(?=[A-Z])/g, ' ').trimStart(), callback) } else { callback() @@ -58,7 +76,7 @@ function createUnit(target: T, root = false): Unit { for (const key in target) { if (Keywords.includes(key)) continue - test[key] = createUnit(target[key]) + test[key] = createUnit(target[key], level + 1) } return test @@ -76,4 +94,4 @@ namespace Tests { export const relation = Relation } -export default createUnit(Tests, true) +export default createUnit(Tests) diff --git a/packages/tests/src/json.ts b/packages/tests/src/json.ts index 896085cc..a22cfa80 100644 --- a/packages/tests/src/json.ts +++ b/packages/tests/src/json.ts @@ -37,32 +37,33 @@ interface Tables { } function JsonTests(database: Database) { - database.extend('foo', { - id: 'unsigned', - value: 'integer', - }) + before(async () => { + console.log(Object.keys(database.tables)) + database.extend('foo', { + id: 'unsigned', + value: 'integer', + }) - database.extend('bar', { - id: 'unsigned', - uid: 'unsigned', - pid: 'unsigned', - value: 'integer', - obj: 'json', - s: 'string', - l: 'list', - }, { - autoInc: true, - }) + database.extend('bar', { + id: 'unsigned', + uid: 'unsigned', + pid: 'unsigned', + value: 'integer', + obj: 'json', + s: 'string', + l: 'list', + }, { + autoInc: true, + }) - database.extend('baz', { - id: 'unsigned', - nums: { - type: 'array', - inner: 'unsigned', - } - }) + database.extend('baz', { + id: 'unsigned', + nums: { + type: 'array', + inner: 'unsigned', + } + }) - before(async () => { await setup(database, 'foo', [ { id: 1, value: 0 }, { id: 2, value: 2 }, diff --git a/packages/tests/src/model.ts b/packages/tests/src/model.ts index bcb82024..9d61559e 100644 --- a/packages/tests/src/model.ts +++ b/packages/tests/src/model.ts @@ -102,167 +102,169 @@ function flatten(type: any, prefix) { } function ModelOperations(database: Database) { - database.define('bigint2', { - type: 'string', - dump: value => isNullable(value) ? value : value.toString(), - load: value => isNullable(value) ? value : BigInt(value), - initial: 123n, - }) - - database.define('custom', { - type: 'string', - dump: value => isNullable(value) ? value : `${value.a}|${value.b}`, - load: value => isNullable(value) ? value : { a: value.split('|')[0], b: +value.split('|')[1] }, - }) + before(() => { + database.define('bigint2', { + type: 'string', + dump: value => isNullable(value) ? value : value.toString(), + load: value => isNullable(value) ? value : BigInt(value), + initial: 123n, + }) - const bnum = database.define({ - type: 'binary', - dump: value => isNullable(value) ? value : toBinary(String(value)), - load: value => isNullable(value) ? value : +Buffer.from(value), - initial: 0, - }) + database.define('custom', { + type: 'string', + dump: value => isNullable(value) ? value : `${value.a}|${value.b}`, + load: value => isNullable(value) ? value : { a: value.split('|')[0], b: +value.split('|')[1] }, + }) - const bstr = database.define({ - type: 'custom', - dump: value => isNullable(value) ? value : { a: value, b: 1 }, - load: value => isNullable(value) ? value : value.a, - initial: 'pooo', - }) + const bnum = database.define({ + type: 'binary', + dump: value => isNullable(value) ? value : toBinary(String(value)), + load: value => isNullable(value) ? value : +Buffer.from(value), + initial: 0, + }) - database.define('recurx', { - type: 'object', - inner: { - id: 'unsigned', - y: 'recury', - }, - }) + const bstr = database.define({ + type: 'custom', + dump: value => isNullable(value) ? value : { a: value, b: 1 }, + load: value => isNullable(value) ? value : value.a, + initial: 'pooo', + }) - database.define('recury', { - type: 'object', - inner: { - id: 'unsigned', - x: 'recurx', - }, - }) + database.define('recurx', { + type: 'object', + inner: { + id: 'unsigned', + y: 'recury', + }, + }) - const baseFields: Field.Extension = { - id: 'unsigned', - text: { - type: 'string', - initial: 'he`l"\'\\lo', - }, - num: { - type: 'integer', - initial: 233, - }, - double: { - type: 'double', - initial: 3.14, - }, - decimal: { - type: 'decimal', - scale: 3, - initial: 12413, - }, - int64: { - type: 'bigint', - initial: 1n, - }, - bool: { - type: 'boolean', - initial: true, - }, - list: { - type: 'list', - initial: ['a`a', 'b"b', 'c\'c', 'd\\d'], - }, - array: 'array', - object: { + database.define('recury', { type: 'object', inner: { - num: 'unsigned', - text: 'string', - json: 'object', - embed: { - type: 'object', - inner: { - bool: { - type: 'boolean', - initial: false, + id: 'unsigned', + x: 'recurx', + }, + }) + + const baseFields: Field.Extension = { + id: 'unsigned', + text: { + type: 'string', + initial: 'he`l"\'\\lo', + }, + num: { + type: 'integer', + initial: 233, + }, + double: { + type: 'double', + initial: 3.14, + }, + decimal: { + type: 'decimal', + scale: 3, + initial: 12413, + }, + int64: { + type: 'bigint', + initial: 1n, + }, + bool: { + type: 'boolean', + initial: true, + }, + list: { + type: 'list', + initial: ['a`a', 'b"b', 'c\'c', 'd\\d'], + }, + array: 'array', + object: { + type: 'object', + inner: { + num: 'unsigned', + text: 'string', + json: 'object', + embed: { + type: 'object', + inner: { + bool: { + type: 'boolean', + initial: false, + }, + int64: 'bigint', + bigint: 'bigint2', + custom: { type: 'custom' }, + bstr: bstr, }, - int64: 'bigint', - bigint: 'bigint2', - custom: { type: 'custom' }, - bstr: bstr, }, }, }, - }, - // dot defined object - 'object2.num': { - type: 'unsigned', - initial: 1, - }, - 'object2.text': { - type: 'string', - initial: '2', - }, - 'object2.embed.bool': { - type: 'boolean', - initial: true, - }, - 'object2.embed.bigint': 'bigint2', - timestamp: { - type: 'timestamp', - initial: new Date('1970-01-01 00:00:00'), - }, - date: { - type: 'date', - initial: new Date('1970-01-01'), - }, - time: { - type: 'time', - initial: new Date('1970-01-01 12:00:00'), - }, - binary: { - type: 'binary', - initial: toBinary('initial buffer') - }, - bigint: 'bigint2', - bnum, - bnum2: { - type: 'binary', - dump: value => isNullable(value) ? value : toBinary(String(value)), - load: value => isNullable(value) ? value : +Buffer.from(value), - initial: 0, - }, - } + // dot defined object + 'object2.num': { + type: 'unsigned', + initial: 1, + }, + 'object2.text': { + type: 'string', + initial: '2', + }, + 'object2.embed.bool': { + type: 'boolean', + initial: true, + }, + 'object2.embed.bigint': 'bigint2', + timestamp: { + type: 'timestamp', + initial: new Date('1970-01-01 00:00:00'), + }, + date: { + type: 'date', + initial: new Date('1970-01-01'), + }, + time: { + type: 'time', + initial: new Date('1970-01-01 12:00:00'), + }, + binary: { + type: 'binary', + initial: toBinary('initial buffer') + }, + bigint: 'bigint2', + bnum, + bnum2: { + type: 'binary', + dump: value => isNullable(value) ? value : toBinary(String(value)), + load: value => isNullable(value) ? value : +Buffer.from(value), + initial: 0, + }, + } - const baseObject = { - type: 'object', - inner: { nested: { type: 'object', inner: baseFields } }, - initial: { nested: { id: 1 } } - } + const baseObject = { + type: 'object', + inner: { nested: { type: 'object', inner: baseFields } }, + initial: { nested: { id: 1 } } + } - database.extend('dtypes', { - ...baseFields - }, { autoInc: true }) - - database.extend('dobjects', { - id: 'unsigned', - foo: baseObject, - ...flatten(baseObject, 'bar'), - baz: { - type: 'array', - inner: baseObject, - initial: [] - }, - }, { autoInc: true }) + database.extend('dtypes', { + ...baseFields + }, { autoInc: true }) + + database.extend('dobjects', { + id: 'unsigned', + foo: baseObject, + ...flatten(baseObject, 'bar'), + baz: { + type: 'array', + inner: baseObject, + initial: [] + }, + }, { autoInc: true }) - database.extend('recurxs', { - id: 'unsigned', - y: 'recury', - }, { autoInc: true }) + database.extend('recurxs', { + id: 'unsigned', + y: 'recury', + }, { autoInc: true }) + }) } namespace ModelOperations { diff --git a/packages/tests/src/object.ts b/packages/tests/src/object.ts index f4f7c1de..e4f231fc 100644 --- a/packages/tests/src/object.ts +++ b/packages/tests/src/object.ts @@ -21,10 +21,12 @@ interface Tables { } function ObjectOperations(database: Database) { - database.extend('object', { - 'id': 'string', - 'meta.a': { type: 'string', initial: '666' }, - 'meta.embed': { type: 'json', initial: { c: 'world' } }, + before(async () => { + database.extend('object', { + 'id': 'string', + 'meta.a': { type: 'string', initial: '666' }, + 'meta.embed': { type: 'json', initial: { c: 'world' } }, + }) }) } diff --git a/packages/tests/src/query.ts b/packages/tests/src/query.ts index 62383e27..782fba05 100644 --- a/packages/tests/src/query.ts +++ b/packages/tests/src/query.ts @@ -18,18 +18,20 @@ interface Tables { } function QueryOperators(database: Database) { - database.extend('temp1', { - id: 'unsigned', - text: 'string', - value: 'integer', - bool: 'boolean', - list: 'list', - timestamp: 'timestamp', - date: 'date', - time: 'time', - regex: 'string', - }, { - autoInc: true, + before(() => { + database.extend('temp1', { + id: 'unsigned', + text: 'string', + value: 'integer', + bool: 'boolean', + list: 'list', + timestamp: 'timestamp', + date: 'date', + time: 'time', + regex: 'string', + }, { + autoInc: true, + }) }) } diff --git a/packages/tests/src/relation.ts b/packages/tests/src/relation.ts index 0dae57dc..44454ba9 100644 --- a/packages/tests/src/relation.ts +++ b/packages/tests/src/relation.ts @@ -82,135 +82,135 @@ interface Tables { } function RelationTests(database: Database) { - database.extend('user', { - id: 'unsigned', - value: 'integer', - successor: { - type: 'oneToOne', - table: 'user', - target: 'predecessor', - }, - }, { - autoInc: true, - }) + before(async () => { + database.extend('user', { + id: 'unsigned', + value: 'integer', + successor: { + type: 'oneToOne', + table: 'user', + target: 'predecessor', + }, + }, { + autoInc: true, + }) - database.extend('profile', { - id: 'unsigned', - name: 'string', - user: { - type: 'oneToOne', - table: 'user', - target: 'profile', - }, - }, { - unique: [['user', 'name']] - }) + database.extend('profile', { + id: 'unsigned', + name: 'string', + user: { + type: 'oneToOne', + table: 'user', + target: 'profile', + }, + }, { + unique: [['user', 'name']] + }) - database.extend('post', { - id2: 'unsigned', - score: 'unsigned', - content: 'string', - author: { - type: 'manyToOne', - table: 'user', - target: 'posts', - }, - }, { - autoInc: true, - primary: 'id2', - }) + database.extend('post', { + id2: 'unsigned', + score: 'unsigned', + content: 'string', + author: { + type: 'manyToOne', + table: 'user', + target: 'posts', + }, + }, { + autoInc: true, + primary: 'id2', + }) - database.extend('tag', { - id: 'unsigned', - name: 'string', - posts: { - type: 'manyToMany', - table: 'post', - target: 'tags', - }, - }, { - autoInc: true, - }) + database.extend('tag', { + id: 'unsigned', + name: 'string', + posts: { + type: 'manyToMany', + table: 'post', + target: 'tags', + }, + }, { + autoInc: true, + }) - database.extend('post2tag', { - 'post.id': 'unsigned', - 'tag.id': 'unsigned', - post: { - type: 'manyToOne', - table: 'post', - target: '_tags', - }, - tag: { - type: 'manyToOne', - table: 'tag', - target: '_posts', - }, - }, { - primary: ['post.id', 'tag.id'], - }) + database.extend('post2tag', { + 'post.id': 'unsigned', + 'tag.id': 'unsigned', + post: { + type: 'manyToOne', + table: 'post', + target: '_tags', + }, + tag: { + type: 'manyToOne', + table: 'tag', + target: '_posts', + }, + }, { + primary: ['post.id', 'tag.id'], + }) - database.extend('login', { - id: 'string', - platform: 'string(64)', - name: 'string', - }, { - primary: ['id', 'platform'], - }) + database.extend('login', { + id: 'string', + platform: 'string(64)', + name: 'string', + }, { + primary: ['id', 'platform'], + }) - database.extend('guild', { - id: 'string', - platform2: 'string(64)', - name: 'string', - logins: { - type: 'manyToMany', - table: 'login', - target: 'guilds', - shared: { platform2: 'platform' }, - }, - }, { - primary: ['id', 'platform2'], - }) + database.extend('guild', { + id: 'string', + platform2: 'string(64)', + name: 'string', + logins: { + type: 'manyToMany', + table: 'login', + target: 'guilds', + shared: { platform2: 'platform' }, + }, + }, { + primary: ['id', 'platform2'], + }) - database.extend('guildSync', { - syncAt: 'unsigned', - platform: 'string', - guild: { - type: 'manyToOne', - table: 'guild', - target: 'syncs', - fields: ['guild.id', 'platform'], - }, - login: { - type: 'manyToOne', - table: 'login', - target: 'syncs', - fields: ['login.id', 'platform'], - }, - }, { - primary: ['guild', 'login'], - }) + database.extend('guildSync', { + syncAt: 'unsigned', + platform: 'string', + guild: { + type: 'manyToOne', + table: 'guild', + target: 'syncs', + fields: ['guild.id', 'platform'], + }, + login: { + type: 'manyToOne', + table: 'login', + target: 'syncs', + fields: ['login.id', 'platform'], + }, + }, { + primary: ['guild', 'login'], + }) - database.extend('member', { - guild: { - type: 'manyToOne', - table: 'guild', - target: 'members', - }, - user: { - type: 'manyToOne', - table: 'login', - }, - name: 'string', - }, { - primary: ['user', 'guild'], - }) + database.extend('member', { + guild: { + type: 'manyToOne', + table: 'guild', + target: 'members', + }, + user: { + type: 'manyToOne', + table: 'login', + }, + name: 'string', + }, { + primary: ['user', 'guild'], + }) - async function setupAutoInc(database: Database, name: K, length: number) { - await database.upsert(name, Array(length).fill({})) - await database.remove(name, {}) - } + async function setupAutoInc(database: Database, name: K, length: number) { + await database.upsert(name, Array(length).fill({})) + await database.remove(name, {}) + } - before(async () => { await setupAutoInc(database, 'user', 3) await setupAutoInc(database, 'post', 3) await setupAutoInc(database, 'tag', 3) diff --git a/packages/tests/src/selection.ts b/packages/tests/src/selection.ts index 8f058281..31cc9228 100644 --- a/packages/tests/src/selection.ts +++ b/packages/tests/src/selection.ts @@ -21,23 +21,23 @@ interface Tables { } function SelectionTests(database: Database) { - database.extend('foo', { - id: 'unsigned', - value: 'integer', - }) + before(async () => { + database.extend('foo', { + id: 'unsigned', + value: 'integer', + }) - database.migrate('foo', { deprecated: 'unsigned' }, async () => { }) + database.migrate('foo', { deprecated: 'unsigned' }, async () => { }) - database.extend('bar', { - id: 'unsigned', - uid: 'unsigned', - pid: 'unsigned', - value: 'integer', - }, { - autoInc: true, - }) + database.extend('bar', { + id: 'unsigned', + uid: 'unsigned', + pid: 'unsigned', + value: 'integer', + }, { + autoInc: true, + }) - before(async () => { await setup(database, 'foo', [ { id: 1, value: 0 }, { id: 2, value: 2 }, diff --git a/packages/tests/src/transaction.ts b/packages/tests/src/transaction.ts index 8ece1027..5bd2a405 100644 --- a/packages/tests/src/transaction.ts +++ b/packages/tests/src/transaction.ts @@ -17,17 +17,19 @@ interface Tables { } function TransactionOperations(database: Database) { - database.extend('temptx', { - id: 'unsigned', - text: 'string', - num: 'integer', - bool: 'boolean', - list: 'list', - timestamp: 'timestamp', - date: 'date', - time: 'time', - }, { - autoInc: true, + before(() => { + database.extend('temptx', { + id: 'unsigned', + text: 'string', + num: 'integer', + bool: 'boolean', + list: 'list', + timestamp: 'timestamp', + date: 'date', + time: 'time', + }, { + autoInc: true, + }) }) } diff --git a/packages/tests/src/update.ts b/packages/tests/src/update.ts index bd00182f..ba9ec5dc 100644 --- a/packages/tests/src/update.ts +++ b/packages/tests/src/update.ts @@ -29,35 +29,37 @@ interface Tables { } function OrmOperations(database: Database) { - database.extend('temp2', { - id: 'unsigned', - text: 'string', - num: 'integer', - double: 'double', - bool: 'boolean', - list: 'list', - timestamp: 'timestamp', - date: 'date', - time: 'time', - bigtext: 'text', - binary: 'binary', - bigint: { - type: 'string', - dump: value => value ? value.toString() : value, - load: value => value ? BigInt(value) : value, - }, - }, { - autoInc: true, - indexes: ['text'], - }) - - database.extend('temp3', { - ida: 'unsigned', - idb: 'string', - value: 'string', - }, { - primary: ['ida', 'idb'], - unique: ['value'], + before(() => { + database.extend('temp2', { + id: 'unsigned', + text: 'string', + num: 'integer', + double: 'double', + bool: 'boolean', + list: 'list', + timestamp: 'timestamp', + date: 'date', + time: 'time', + bigtext: 'text', + binary: 'binary', + bigint: { + type: 'string', + dump: value => value ? value.toString() : value, + load: value => value ? BigInt(value) : value, + }, + }, { + autoInc: true, + indexes: ['text'], + }) + + database.extend('temp3', { + ida: 'unsigned', + idb: 'string', + value: 'string', + }, { + primary: ['ida', 'idb'], + unique: ['value'], + }) }) } From 7ec6acea05c95623d45fdf2ee7e10c5a50445ea8 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Mon, 14 Oct 2024 01:10:57 +0800 Subject: [PATCH 2/3] feat: ignore indexes and relation field --- packages/core/src/database.ts | 27 ++++++------------- packages/core/src/model.ts | 25 +++++++----------- packages/tests/src/index.ts | 2 +- packages/tests/src/json.ts | 1 - packages/tests/src/migration.ts | 47 ++++++++++++++++++++++++++++----- 5 files changed, 59 insertions(+), 43 deletions(-) diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts index 3145e5af..ff237d2f 100644 --- a/packages/core/src/database.ts +++ b/packages/core/src/database.ts @@ -156,12 +156,11 @@ export class Database extends Servi ;(model.fields[key] = Field.parse('expr')).relation = relation if (def.target) { (relmodel.fields[def.target] ??= Field.parse('expr')).relation = inverse - disposables.unshift(() => delete relmodel.fields[def.target!]) } if (relation.type === 'oneToOne' || relation.type === 'manyToOne') { relation.fields.forEach((x, i) => { - if (!model.fields[x]) disposables.unshift(() => delete model.fields[x]) + if (!(x in model.fields)) disposables.unshift(() => delete model.fields[x]) model.fields[x] ??= { ...relmodel.fields[relation.references[i]] } as any if (!relation.required) { model.fields[x]!.nullable = true @@ -197,25 +196,15 @@ export class Database extends Servi if (Array.isArray(model.primary) || model.fields[model.primary]!.relation) { model.primary = deduplicate(makeArray(model.primary).map(key => model.fields[key]!.relation?.fields || key).flat()) } - model.unique = model.unique.map(keys => { - if (typeof keys === 'string') { - return (model.fields[keys]!.relation?.fields && disposables.unshift(() => model.unique.splice(model.unique.indexOf(keys), 1)), - model.fields[keys]!.relation?.fields || keys) - } else { - const pred = keys.some(key => model.fields[key]!.relation?.fields) - if (pred) { - const newKeys = keys.map(key => model.fields[key]!.relation?.fields || key).flat() - disposables.unshift(() => model.unique.splice(model.unique.indexOf(newKeys), 1)) - return newKeys - } else { - return keys - } - } - }) + model.unique = model.unique.map(keys => typeof keys === 'string' ? model.fields[keys]!.relation?.fields || keys + : keys.map(key => model.fields[key]!.relation?.fields || key).flat()) this.prepareTasks[name] = this.prepare(name) ;(this.ctx as Context).emit('model', name) - return this.ctx.effect(() => () => disposables.splice(0).forEach(dispose => dispose())) + return this.ctx.effect(() => () => { + disposables.splice(0).forEach(dispose => dispose()) + ;(this.ctx as Context).emit('model', name) + }) } private _parseField(field: any, transformers: Driver.Transformer[] = [], setInitial?: (value) => void, setField?: (value) => void): Type { @@ -321,7 +310,7 @@ export class Database extends Servi fields: Field.Extension, callback: Model.Migration, ) { - this.extend(name, fields, { callback }) + return this.extend(name, fields, { callback }) } select(table: Selection, query?: Query): Selection diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index d9af09c3..359ca45e 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -274,23 +274,18 @@ export class Model { extend(fields: Field.Extension, config?: Partial): () => void extend(fields = {}, config: Partial = {}) { - const { primary, autoInc, unique = [], indexes = [], foreign = {}, callback } = config - const disposables: (() => void)[] = [] + const { primary, autoInc, unique = [], indexes = [], foreign, callback } = config this.primary = primary || this.primary this.autoInc = autoInc || this.autoInc - unique.forEach(key => this.unique.includes(key) || (this.unique.push(key), disposables.push(() => this.unique.splice(this.unique.indexOf(key), 1)))) - indexes.map(x => this.parseIndex(x)).forEach(index => (this.indexes.some(ind => deepEqual(ind, index))) - || (this.indexes.push(index), disposables.push(() => this.indexes.splice(this.indexes.indexOf(index), 1)))) - Object.keys(foreign).forEach(key => Object.hasOwn(this.foreign, key) - || (this.foreign[key] = foreign[key], disposables.push(() => delete this.foreign[key]))) + unique.forEach(key => this.unique.includes(key) || this.unique.push(key)) + indexes.map(x => this.parseIndex(x)).forEach(index => (this.indexes.some(ind => deepEqual(ind, index))) || this.indexes.push(index)) + Object.assign(this.foreign, foreign) if (callback) this.migrations.set(callback, Object.keys(fields)) for (const key in fields) { - if (makeArray(this.primary).includes(key)) { - disposables.push(() => delete this.ctx?.get('model')?.tables[this.name]) - } + if (Object.keys(this.fields).includes(key)) throw new TypeError(`field "${key}" already exists in table "${this.name}"`) this.fields[key] = Field.parse(fields[key]) this.fields[key].deprecated = !!callback } @@ -304,12 +299,10 @@ export class Model { this.unique.forEach(index => this.checkIndex(index)) this.indexes.forEach(index => this.checkIndex(index)) - return () => { - for (const key in fields) { - delete this.fields[key] - } - if (callback) this.migrations.delete(callback) - disposables.splice(0).forEach(dispose => dispose()) + if (Object.keys(fields).some(key => makeArray(this.primary).includes(key))) { + return () => delete this.ctx?.get('model')?.tables[this.name] + } else { + return () => Object.keys(fields).forEach(key => delete this.fields[key]) } } diff --git a/packages/tests/src/index.ts b/packages/tests/src/index.ts index 6b183ec7..e745cf20 100644 --- a/packages/tests/src/index.ts +++ b/packages/tests/src/index.ts @@ -20,7 +20,7 @@ type UnitOptions = (T extends (database: Database, options?: infer R) => any [K in keyof T as Exclude]?: false | UnitOptions } -type Unit = ((database: Database | ((arg: T) => Database), options: UnitOptions, fork?: boolean) => void) & { +type Unit = ((database: Database, options?: UnitOptions) => void) & { [K in keyof T as Exclude]: Unit } diff --git a/packages/tests/src/json.ts b/packages/tests/src/json.ts index a22cfa80..dbfedc6d 100644 --- a/packages/tests/src/json.ts +++ b/packages/tests/src/json.ts @@ -38,7 +38,6 @@ interface Tables { function JsonTests(database: Database) { before(async () => { - console.log(Object.keys(database.tables)) database.extend('foo', { id: 'unsigned', value: 'integer', diff --git a/packages/tests/src/migration.ts b/packages/tests/src/migration.ts index 80bfd670..0109bdbd 100644 --- a/packages/tests/src/migration.ts +++ b/packages/tests/src/migration.ts @@ -45,8 +45,6 @@ function MigrationTests(database: Database) { ]) database.extend('qux', { - id: 'unsigned', - text: 'string(64)', number: 'unsigned', }) @@ -73,6 +71,46 @@ function MigrationTests(database: Database) { ]) }) + it('alter disposable field', async () => { + Reflect.deleteProperty(database.tables, 'qux') + + database.extend('qux', { + id: 'unsigned', + text: 'string(64)', + }) + + await database.upsert('qux', [ + { id: 1, text: 'foo' }, + { id: 2, text: 'bar' }, + ]) + + await expect(database.get('qux', {})).to.eventually.deep.equal([ + { id: 1, text: 'foo' }, + { id: 2, text: 'bar' }, + ]) + + const dispose = database.extend('qux', { + number: 'unsigned', + }) + + await database.upsert('qux', [ + { id: 1, text: 'foo', number: 100 }, + { id: 2, text: 'bar', number: 200 }, + ]) + + await expect(database.get('qux', {})).to.eventually.deep.equal([ + { id: 1, text: 'foo', number: 100 }, + { id: 2, text: 'bar', number: 200 }, + ]) + + dispose() + + await expect(database.get('qux', {})).to.eventually.deep.equal([ + { id: 1, text: 'foo' }, + { id: 2, text: 'bar' }, + ]) + }) + it('should migrate field', async () => { Reflect.deleteProperty(database.tables, 'qux') @@ -174,10 +212,7 @@ function MigrationTests(database: Database) { { id: 2, number: 2 }, ]) - database.extend('qux', { - id: 'unsigned', - number: 'unsigned', - }, { + database.extend('qux', {}, { indexes: ['number'], }) From 780f4e1bc758b2b2ce536161ca44237e601f1762 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Thu, 24 Oct 2024 17:34:18 +0800 Subject: [PATCH 3/3] feat: clean migrateTasks and prepareTasks --- packages/core/src/model.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index 359ca45e..51af6a35 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -300,7 +300,12 @@ export class Model { this.indexes.forEach(index => this.checkIndex(index)) if (Object.keys(fields).some(key => makeArray(this.primary).includes(key))) { - return () => delete this.ctx?.get('model')?.tables[this.name] + return () => { + const database = this.ctx?.get('model') + delete database?.tables[this.name] + delete database?.migrateTasks[this.name] + delete database?.['prepareTasks'][this.name] + } } else { return () => Object.keys(fields).forEach(key => delete this.fields[key]) }