From a550fae42260866fba32437c0c59fea53ea4f729 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Thu, 24 Oct 2024 16:45:47 +0800 Subject: [PATCH 1/3] feat(minato): impl model.immutable and driver readOnly --- packages/core/src/database.ts | 14 ++++---- packages/core/src/driver.ts | 19 +++++++--- packages/core/src/model.ts | 5 ++- packages/core/src/selection.ts | 3 ++ packages/mongo/src/index.ts | 48 +++++++++++++++---------- packages/mongo/src/locales/en-US.yml | 1 + packages/mongo/src/locales/zh-CN.yml | 1 + packages/mongo/tests/index.spec.ts | 3 ++ packages/mysql/src/index.ts | 11 +++++- packages/mysql/src/locales/en-US.yml | 1 + packages/mysql/src/locales/zh-CN.yml | 1 + packages/postgres/src/index.ts | 27 +++++++++----- packages/postgres/src/locales/en-US.yml | 1 + packages/postgres/src/locales/zh-CN.yml | 1 + packages/sqlite/src/index.ts | 19 +++++++--- packages/sqlite/src/locales/en-US.yml | 1 + packages/sqlite/src/locales/zh-CN.yml | 1 + packages/tests/src/migration.ts | 46 +++++++++++++++++++++++- 18 files changed, 160 insertions(+), 43 deletions(-) diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts index 4607195a..2af1e8fc 100644 --- a/packages/core/src/database.ts +++ b/packages/core/src/database.ts @@ -85,15 +85,15 @@ export class Database extends Servi static readonly migrate = Symbol('minato.migrate') public tables: Dict = Object.create(null) - public drivers: Driver[] = [] + public drivers: Driver[] = [] public types: Dict = Object.create(null) - private _driver: Driver | undefined + private _driver: Driver | undefined private stashed = new Set() private prepareTasks: Dict> = Object.create(null) public migrateTasks: Dict> = Object.create(null) - async connect(driver: Driver.Constructor, ...args: Spread) { + async connect(driver: Driver.Constructor, ...args: Spread) { this.ctx.plugin(driver, args[0] as any) await this.ctx.start() } @@ -109,7 +109,7 @@ export class Database extends Servi await Promise.all(Object.values(this.prepareTasks)) } - private getDriver(table: string | Selection): Driver { + private getDriver(table: string | Selection): Driver { if (Selection.is(table)) return table.driver as any const model: Model = this.tables[table] if (!model) throw new Error(`cannot resolve table "${table}"`) @@ -602,12 +602,14 @@ export class Database extends Servi async drop>(table: K) { if (this[Database.transact]) throw new Error('cannot drop table in transaction') - await this.getDriver(table).drop(table) + const driver = this.getDriver(table) + if (driver.config.readOnly) throw new Error('cannot drop table in read-only mode') + await driver.drop(table) } async dropAll() { if (this[Database.transact]) throw new Error('cannot drop table in transaction') - await Promise.all(Object.values(this.drivers).map(driver => driver.dropAll())) + await Promise.all(Object.values(this.drivers).filter(driver => !driver.config.readOnly).map(driver => driver.dropAll())) } async stats() { diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index 3e420d52..c8559ee1 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -1,5 +1,5 @@ import { Awaitable, deepEqual, defineProperty, Dict, mapValues, remove } from 'cosmokit' -import { Context, Logger, Service } from 'cordis' +import { Context, Logger, Service, z } from 'cordis' import { Eval, Update } from './eval.ts' import { Direction, Modifier, Selection } from './selection.ts' import { Field, Model, Relation } from './model.ts' @@ -52,10 +52,10 @@ export namespace Driver { } export namespace Driver { - export type Constructor = new (ctx: Context, config: T) => Driver + export type Constructor = new (ctx: Context, config: T) => Driver } -export abstract class Driver { +export abstract class Driver { static inject = ['model'] abstract start(): Promise @@ -165,8 +165,9 @@ export abstract class Driver { async _ensureSession() {} async prepareIndexes(table: string) { + const { immutable, indexes } = this.model(table) + if (immutable || this.config.readOnly) return const oldIndexes = await this.getIndexes(table) - const { indexes } = this.model(table) for (const index of indexes) { const oldIndex = oldIndexes.find(info => info.name === index.name) if (!oldIndex) { @@ -179,6 +180,16 @@ export abstract class Driver { } } +export namespace Driver { + export interface Config { + readOnly?: boolean + } + + export const Config: z = z.object({ + readOnly: z.boolean().default(false), + }) +} + export interface MigrationHooks { before: (keys: string[]) => boolean after: (keys: string[]) => void diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index ebc2bf2e..c64cfd04 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -245,6 +245,7 @@ export namespace Model { export interface Config { callback?: Migration autoInc: boolean + immutable: boolean primary: MaybeArray unique: MaybeArray[] indexes: (MaybeArray | Driver.IndexDef)[] @@ -266,6 +267,7 @@ export class Model { constructor(public name: string) { this.autoInc = false + this.immutable = false this.primary = 'id' as never this.unique = [] this.indexes = [] @@ -274,10 +276,11 @@ export class Model { extend(fields: Field.Extension, config?: Partial): void extend(fields = {}, config: Partial = {}) { - const { primary, autoInc, unique = [], indexes = [], foreign, callback } = config + const { primary, autoInc, immutable, unique = [], indexes = [], foreign, callback } = config this.primary = primary || this.primary this.autoInc = autoInc || this.autoInc + this.immutable = immutable || this.immutable 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) diff --git a/packages/core/src/selection.ts b/packages/core/src/selection.ts index 9366833d..c8d41500 100644 --- a/packages/core/src/selection.ts +++ b/packages/core/src/selection.ts @@ -146,6 +146,9 @@ class Executable { } async execute(): Promise { + if (this.driver.config.readOnly && !['get', 'eval'].includes(this.type)) { + throw new Error(`database is in read-only mode`) + } await this.driver.database.prepared() await this.driver._ensureSession() return this.driver[this.type as any](this, ...this.args) diff --git a/packages/mongo/src/index.ts b/packages/mongo/src/index.ts index b0a50406..cd69533d 100644 --- a/packages/mongo/src/index.ts +++ b/packages/mongo/src/index.ts @@ -229,6 +229,15 @@ export class MongoDriver extends Driver { /** synchronize table schema */ async prepare(table: string) { + const { immutable } = this.model(table) + + if (immutable || this.config.readOnly) { + if (immutable && this.shouldEnsurePrimary(table)) { + throw new Error(`immutable table ${table} cannot be autoInc`) + } + return + } + await Promise.all([ this._createInternalTable(), this.db.createCollection(table).catch(noop), @@ -536,7 +545,7 @@ export class MongoDriver extends Driver { } export namespace MongoDriver { - export interface Config extends MongoClientOptions { + export interface Config extends Driver.Config, MongoClientOptions { username?: string password?: string protocol?: string @@ -556,24 +565,27 @@ export namespace MongoDriver { optimizeIndex?: boolean } - export const Config: z = z.object({ - protocol: z.string().default('mongodb'), - host: z.string().default('localhost'), - port: z.natural().max(65535), - username: z.string(), - password: z.string().role('secret'), - database: z.string().required(), - authDatabase: z.string(), - writeConcern: z.object({ - w: z.union([ - z.const(undefined), - z.number().required(), - z.const('majority').required(), - ]), - wtimeoutMS: z.number(), - journal: z.boolean(), + export const Config: z = z.intersect([ + Driver.Config, + z.object({ + protocol: z.string().default('mongodb'), + host: z.string().default('localhost'), + port: z.natural().max(65535), + username: z.string(), + password: z.string().role('secret'), + database: z.string().required(), + authDatabase: z.string(), + writeConcern: z.object({ + w: z.union([ + z.const(undefined), + z.number().required(), + z.const('majority').required(), + ]), + wtimeoutMS: z.number(), + journal: z.boolean(), + }) as any, }), - }).i18n({ + ]).i18n({ 'en-US': enUS, 'zh-CN': zhCN, }) diff --git a/packages/mongo/src/locales/en-US.yml b/packages/mongo/src/locales/en-US.yml index 8471c859..68f118b7 100644 --- a/packages/mongo/src/locales/en-US.yml +++ b/packages/mongo/src/locales/en-US.yml @@ -15,3 +15,4 @@ writeConcern: - Majority wtimeoutMS: The write concern timeout. journal: The journal write concern. +readOnly: Connect in read-only mode. diff --git a/packages/mongo/src/locales/zh-CN.yml b/packages/mongo/src/locales/zh-CN.yml index 7b66cd9d..30963c28 100644 --- a/packages/mongo/src/locales/zh-CN.yml +++ b/packages/mongo/src/locales/zh-CN.yml @@ -15,3 +15,4 @@ writeConcern: - Majority wtimeoutMS: The write concern timeout. journal: The journal write concern. +readOnly: 以只读模式连接。 diff --git a/packages/mongo/tests/index.spec.ts b/packages/mongo/tests/index.spec.ts index 41dacd6b..2908d910 100644 --- a/packages/mongo/tests/index.spec.ts +++ b/packages/mongo/tests/index.spec.ts @@ -30,6 +30,9 @@ describe('@minatojs/driver-mongo', () => { aggregateNull: false, } }, + migration: { + definition: false, + }, transaction: { abort: false } diff --git a/packages/mysql/src/index.ts b/packages/mysql/src/index.ts index 75ddc333..d8fba3fd 100644 --- a/packages/mysql/src/index.ts +++ b/packages/mysql/src/index.ts @@ -157,6 +157,7 @@ export class MySQLDriver extends Driver { const table = this.model(name) const { primary, foreign, autoInc } = table + const immutable = table.immutable || this.config.readOnly const fields = table.avaiableFields() const unique = [...table.unique] const create: string[] = [] @@ -232,6 +233,13 @@ export class MySQLDriver extends Driver { } } + if (immutable) { + if (create.length || update.length) { + throw new Error(`immutable table ${name} cannot be migrated`) + } + return + } + if (!columns.length) { this.logger.info('auto creating table %c', name) return this.query(`CREATE TABLE ${escapeId(name)} (${create.join(', ')}) COLLATE = ${this.sql.escape(this.config.charset ?? 'utf8mb4_general_ci')}`) @@ -593,9 +601,10 @@ INSERT INTO mtt VALUES(json_extract(j, concat('$[', i, ']'))); SET i=i+1; END WH } export namespace MySQLDriver { - export interface Config extends PoolConfig {} + export interface Config extends Driver.Config, PoolConfig {} export const Config: z = z.intersect([ + Driver.Config, z.object({ host: z.string().default('localhost'), port: z.natural().max(65535).default(3306), diff --git a/packages/mysql/src/locales/en-US.yml b/packages/mysql/src/locales/en-US.yml index 6b951df3..965bf36f 100644 --- a/packages/mysql/src/locales/en-US.yml +++ b/packages/mysql/src/locales/en-US.yml @@ -10,3 +10,4 @@ ssl: - Default - $description: Custom rejectUnauthorized: Reject clients with invalid certificates. +readOnly: Connect in read-only mode. diff --git a/packages/mysql/src/locales/zh-CN.yml b/packages/mysql/src/locales/zh-CN.yml index 8a79dc6f..c3fbddf3 100644 --- a/packages/mysql/src/locales/zh-CN.yml +++ b/packages/mysql/src/locales/zh-CN.yml @@ -10,3 +10,4 @@ ssl: - 默认值 - $description: 自定义 rejectUnauthorized: 拒绝使用无效证书的客户端。 +readOnly: 以只读模式连接。 diff --git a/packages/postgres/src/index.ts b/packages/postgres/src/index.ts index 47aabda2..a8c95b52 100644 --- a/packages/postgres/src/index.ts +++ b/packages/postgres/src/index.ts @@ -176,6 +176,7 @@ export class PostgresDriver extends Driver { const table = this.model(name) const { primary, foreign } = table + const immutable = table.immutable || this.config.readOnly const fields = { ...table.avaiableFields() } const unique = [...table.unique] const create: string[] = [] @@ -236,6 +237,13 @@ export class PostgresDriver extends Driver { } } + if (immutable) { + if (create.length || update.length) { + throw new Error(`immutable table ${name} cannot be migrated`) + } + return + } + if (!columns.length) { this.logger.info('auto creating table %c', name) return this.query(`CREATE TABLE ${escapeId(name)} (${create.join(', ')}, _pg_mtime BIGINT)`) @@ -522,7 +530,7 @@ export class PostgresDriver extends Driver { } export namespace PostgresDriver { - export interface Config = {}> extends postgres.Options { + export interface Config = {}> extends Driver.Config, postgres.Options { host: string port: number user: string @@ -530,13 +538,16 @@ export namespace PostgresDriver { database: string } - export const Config: z = z.object({ - host: z.string().default('localhost'), - port: z.natural().max(65535).default(5432), - user: z.string().default('root'), - password: z.string().role('secret'), - database: z.string().required(), - }).i18n({ + export const Config: z = z.intersect([ + Driver.Config, + z.object({ + host: z.string().default('localhost'), + port: z.natural().max(65535).default(5432), + user: z.string().default('root'), + password: z.string().role('secret'), + database: z.string().required(), + }), + ]).i18n({ 'en-US': enUS, 'zh-CN': zhCN, }) diff --git a/packages/postgres/src/locales/en-US.yml b/packages/postgres/src/locales/en-US.yml index 6b373cbe..1523c27d 100644 --- a/packages/postgres/src/locales/en-US.yml +++ b/packages/postgres/src/locales/en-US.yml @@ -3,3 +3,4 @@ port: The port number to connect to. user: The MySQL user to authenticate as. password: The password of that MySQL user. database: Name of the database to use for this connection. +readOnly: Connect in read-only mode. diff --git a/packages/postgres/src/locales/zh-CN.yml b/packages/postgres/src/locales/zh-CN.yml index 353924bd..dfe3f74a 100644 --- a/packages/postgres/src/locales/zh-CN.yml +++ b/packages/postgres/src/locales/zh-CN.yml @@ -3,3 +3,4 @@ port: 要连接到的端口号。 username: 要使用的用户名。 password: 要使用的密码。 database: 要访问的数据库名。 +readOnly: 以只读模式连接。 diff --git a/packages/sqlite/src/index.ts b/packages/sqlite/src/index.ts index da8d82c7..845b26b8 100644 --- a/packages/sqlite/src/index.ts +++ b/packages/sqlite/src/index.ts @@ -63,6 +63,7 @@ export class SQLiteDriver extends Driver { async prepare(table: string, dropKeys?: string[]) { const columns = this._all(`PRAGMA table_info(${escapeId(table)})`) as SQLiteFieldInfo[] const model = this.model(table) + const immutable = model.immutable || this.config.readOnly const columnDefs: string[] = [] const indexDefs: string[] = [] const alter: string[] = [] @@ -112,6 +113,13 @@ export class SQLiteDriver extends Driver { })) } + if (immutable) { + if (!columns.length || shouldMigrate || alter.length) { + throw new Error(`immutable table ${table} cannot be migrated`) + } + return + } + if (!columns.length) { this.logger.info('auto creating table %c', table) this._run(`CREATE TABLE ${escapeId(table)} (${[...columnDefs, ...indexDefs].join(', ')})`) @@ -482,13 +490,16 @@ export class SQLiteDriver extends Driver { } export namespace SQLiteDriver { - export interface Config { + export interface Config extends Driver.Config { path: string } - export const Config: z = z.object({ - path: z.string().role('path').required(), - }).i18n({ + export const Config: z = z.intersect([ + Driver.Config, + z.object({ + path: z.string().role('path').required(), + }), + ]).i18n({ 'en-US': enUS, 'zh-CN': zhCN, }) diff --git a/packages/sqlite/src/locales/en-US.yml b/packages/sqlite/src/locales/en-US.yml index 4728d564..b56a1f79 100644 --- a/packages/sqlite/src/locales/en-US.yml +++ b/packages/sqlite/src/locales/en-US.yml @@ -1 +1,2 @@ path: Database path. +readOnly: Connect in read-only mode. diff --git a/packages/sqlite/src/locales/zh-CN.yml b/packages/sqlite/src/locales/zh-CN.yml index b09c9d6b..0edeab89 100644 --- a/packages/sqlite/src/locales/zh-CN.yml +++ b/packages/sqlite/src/locales/zh-CN.yml @@ -1 +1,2 @@ path: 数据库路径。 +readOnly: 以只读模式连接。 diff --git a/packages/tests/src/migration.ts b/packages/tests/src/migration.ts index 80bfd670..d97221fd 100644 --- a/packages/tests/src/migration.ts +++ b/packages/tests/src/migration.ts @@ -21,7 +21,13 @@ interface Tables { qux2: Qux2 } -function MigrationTests(database: Database) { +interface MigrationOptions { + definition?: boolean +} + +function MigrationTests(database: Database, options: MigrationOptions = {}) { + const { definition = true } = options + beforeEach(async () => { await database.drop('qux').catch(noop) }) @@ -105,6 +111,8 @@ function MigrationTests(database: Database) { flag: 'boolean', }) + await database.prepared() + database.migrate('qux', { flag: 'boolean', }, async (database) => { @@ -271,6 +279,42 @@ function MigrationTests(database: Database) { }, }))).to.not.be.undefined }) + + definition && it('model.immutable', 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.have.deep.members([ + { id: 1, text: 'foo' }, + { id: 2, text: 'bar' }, + ]) + + Reflect.deleteProperty(database.tables, 'qux') + + database.extend('qux', { + id: 'unsigned', + text: 'integer' as any, + }, { + immutable: true, + }) + + await expect(database.upsert('qux', [ + { id: 1, text: 'foo' }, + { id: 2, text: 'bar' }, + ])).to.eventually.be.rejectedWith('immutable') + + Reflect.deleteProperty(database.tables, 'qux') + Reflect.deleteProperty(database['prepareTasks'], 'qux') + }) } export default MigrationTests From bf2647685e31fdf24458d0b2ab8cb7c0a9337fa7 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Thu, 31 Oct 2024 15:35:57 +0800 Subject: [PATCH 2/3] stage --- packages/core/src/database.ts | 4 ++-- packages/core/src/driver.ts | 17 ++++++++++----- packages/core/src/locales/en-US.yml | 3 +++ packages/core/src/locales/zh-CN.yml | 3 +++ packages/core/src/model.ts | 5 +---- packages/core/src/selection.ts | 2 +- packages/mongo/src/index.ts | 16 +++++++------- packages/mongo/src/locales/en-US.yml | 1 - packages/mongo/src/locales/zh-CN.yml | 1 - packages/mysql/src/index.ts | 28 ++++++++++++------------- packages/mysql/src/locales/en-US.yml | 1 - packages/mysql/src/locales/zh-CN.yml | 1 - packages/postgres/src/index.ts | 26 ++++++++++++----------- packages/postgres/src/locales/en-US.yml | 1 - packages/postgres/src/locales/zh-CN.yml | 1 - packages/sqlite/src/index.ts | 15 ++++++------- packages/sqlite/src/locales/en-US.yml | 1 - packages/sqlite/src/locales/zh-CN.yml | 1 - packages/tests/src/migration.ts | 7 +++---- 19 files changed, 68 insertions(+), 66 deletions(-) create mode 100644 packages/core/src/locales/en-US.yml create mode 100644 packages/core/src/locales/zh-CN.yml diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts index 2af1e8fc..b9f8b162 100644 --- a/packages/core/src/database.ts +++ b/packages/core/src/database.ts @@ -603,13 +603,13 @@ export class Database extends Servi async drop>(table: K) { if (this[Database.transact]) throw new Error('cannot drop table in transaction') const driver = this.getDriver(table) - if (driver.config.readOnly) throw new Error('cannot drop table in read-only mode') + if (driver.config.readonly) throw new Error('cannot drop table in read-only mode') await driver.drop(table) } async dropAll() { if (this[Database.transact]) throw new Error('cannot drop table in transaction') - await Promise.all(Object.values(this.drivers).filter(driver => !driver.config.readOnly).map(driver => driver.dropAll())) + await Promise.all(Object.values(this.drivers).filter(driver => !driver.config.readonly).map(driver => driver.dropAll())) } async stats() { diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index c8559ee1..70928b7d 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -6,6 +6,8 @@ import { Field, Model, Relation } from './model.ts' import { Database } from './database.ts' import { Type } from './type.ts' import { FlatKeys, Keys, Values } from './utils.ts' +import enUS from './locales/en-US.yml' +import zhCN from './locales/zh-CN.yml' export namespace Driver { export interface Stats { @@ -165,14 +167,14 @@ export abstract class Driver info.name === index.name) if (!oldIndex) { await this.createIndex(table, index) - } else if (!deepEqual(oldIndex, index)) { + } else if (this.config.migrateStrategy === 'auto' && !deepEqual(oldIndex, index)) { await this.dropIndex(table, index.name!) await this.createIndex(table, index) } @@ -182,11 +184,16 @@ export abstract class Driver = z.object({ - readOnly: z.boolean().default(false), + readonly: z.boolean().default(false), + migrateStrategy: z.union([z.const('auto'), z.const('create'), z.const('never')]).default('auto'), + }).i18n({ + 'en-US': enUS, + 'zh-CN': zhCN, }) } diff --git a/packages/core/src/locales/en-US.yml b/packages/core/src/locales/en-US.yml new file mode 100644 index 00000000..a9bc22ba --- /dev/null +++ b/packages/core/src/locales/en-US.yml @@ -0,0 +1,3 @@ +$description: Access settings +readonly: Connect in read-only mode. +migrateStrategy: Table migration strategy. diff --git a/packages/core/src/locales/zh-CN.yml b/packages/core/src/locales/zh-CN.yml new file mode 100644 index 00000000..805ed2b3 --- /dev/null +++ b/packages/core/src/locales/zh-CN.yml @@ -0,0 +1,3 @@ +$description: 访问设置 +readonly: 以只读模式连接。 +migrateStrategy: 表迁移策略。 diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index c64cfd04..ebc2bf2e 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -245,7 +245,6 @@ export namespace Model { export interface Config { callback?: Migration autoInc: boolean - immutable: boolean primary: MaybeArray unique: MaybeArray[] indexes: (MaybeArray | Driver.IndexDef)[] @@ -267,7 +266,6 @@ export class Model { constructor(public name: string) { this.autoInc = false - this.immutable = false this.primary = 'id' as never this.unique = [] this.indexes = [] @@ -276,11 +274,10 @@ export class Model { extend(fields: Field.Extension, config?: Partial): void extend(fields = {}, config: Partial = {}) { - const { primary, autoInc, immutable, unique = [], indexes = [], foreign, callback } = config + const { primary, autoInc, unique = [], indexes = [], foreign, callback } = config this.primary = primary || this.primary this.autoInc = autoInc || this.autoInc - this.immutable = immutable || this.immutable 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) diff --git a/packages/core/src/selection.ts b/packages/core/src/selection.ts index c8d41500..91eae13b 100644 --- a/packages/core/src/selection.ts +++ b/packages/core/src/selection.ts @@ -146,7 +146,7 @@ class Executable { } async execute(): Promise { - if (this.driver.config.readOnly && !['get', 'eval'].includes(this.type)) { + if (this.driver.config.readonly && !['get', 'eval'].includes(this.type)) { throw new Error(`database is in read-only mode`) } await this.driver.database.prepared() diff --git a/packages/mongo/src/index.ts b/packages/mongo/src/index.ts index cd69533d..0c9dc7b7 100644 --- a/packages/mongo/src/index.ts +++ b/packages/mongo/src/index.ts @@ -229,10 +229,8 @@ export class MongoDriver extends Driver { /** synchronize table schema */ async prepare(table: string) { - const { immutable } = this.model(table) - - if (immutable || this.config.readOnly) { - if (immutable && this.shouldEnsurePrimary(table)) { + if (this.config.migrateStrategy === 'never' || this.config.readonly) { + if (this.config.migrateStrategy === 'never' && this.shouldEnsurePrimary(table)) { throw new Error(`immutable table ${table} cannot be autoInc`) } return @@ -566,7 +564,6 @@ export namespace MongoDriver { } export const Config: z = z.intersect([ - Driver.Config, z.object({ protocol: z.string().default('mongodb'), host: z.string().default('localhost'), @@ -584,11 +581,12 @@ export namespace MongoDriver { wtimeoutMS: z.number(), journal: z.boolean(), }) as any, + }).i18n({ + 'en-US': enUS, + 'zh-CN': zhCN, }), - ]).i18n({ - 'en-US': enUS, - 'zh-CN': zhCN, - }) + Driver.Config, + ]) } export default MongoDriver diff --git a/packages/mongo/src/locales/en-US.yml b/packages/mongo/src/locales/en-US.yml index 68f118b7..8471c859 100644 --- a/packages/mongo/src/locales/en-US.yml +++ b/packages/mongo/src/locales/en-US.yml @@ -15,4 +15,3 @@ writeConcern: - Majority wtimeoutMS: The write concern timeout. journal: The journal write concern. -readOnly: Connect in read-only mode. diff --git a/packages/mongo/src/locales/zh-CN.yml b/packages/mongo/src/locales/zh-CN.yml index 30963c28..7b66cd9d 100644 --- a/packages/mongo/src/locales/zh-CN.yml +++ b/packages/mongo/src/locales/zh-CN.yml @@ -15,4 +15,3 @@ writeConcern: - Majority wtimeoutMS: The write concern timeout. journal: The journal write concern. -readOnly: 以只读模式连接。 diff --git a/packages/mysql/src/index.ts b/packages/mysql/src/index.ts index d8fba3fd..d31f5fc6 100644 --- a/packages/mysql/src/index.ts +++ b/packages/mysql/src/index.ts @@ -157,7 +157,6 @@ export class MySQLDriver extends Driver { const table = this.model(name) const { primary, foreign, autoInc } = table - const immutable = table.immutable || this.config.readOnly const fields = table.avaiableFields() const unique = [...table.unique] const create: string[] = [] @@ -233,18 +232,21 @@ export class MySQLDriver extends Driver { } } - if (immutable) { + if (!columns.length) { + if (this.config.readonly || this.config.migrateStrategy === 'never') { + throw new Error(`immutable table ${name} cannot be created`) + } + this.logger.info('auto creating table %c', name) + return this.query(`CREATE TABLE ${escapeId(name)} (${create.join(', ')}) COLLATE = ${this.sql.escape(this.config.charset ?? 'utf8mb4_general_ci')}`) + } + + if (this.config.readonly || this.config.migrateStrategy !== 'auto') { if (create.length || update.length) { throw new Error(`immutable table ${name} cannot be migrated`) } return } - if (!columns.length) { - this.logger.info('auto creating table %c', name) - return this.query(`CREATE TABLE ${escapeId(name)} (${create.join(', ')}) COLLATE = ${this.sql.escape(this.config.charset ?? 'utf8mb4_general_ci')}`) - } - const operations = [ ...create.map(def => 'ADD ' + def), ...update, @@ -604,15 +606,12 @@ export namespace MySQLDriver { export interface Config extends Driver.Config, PoolConfig {} export const Config: z = z.intersect([ - Driver.Config, z.object({ host: z.string().default('localhost'), port: z.natural().max(65535).default(3306), user: z.string().default('root'), password: z.string().role('secret'), database: z.string().required(), - }), - z.object({ ssl: z.union([ z.const(undefined), z.object({ @@ -639,11 +638,12 @@ export namespace MySQLDriver { sessionTimeout: z.number(), }), ]) as any, + }).i18n({ + 'en-US': enUS, + 'zh-CN': zhCN, }), - ]).i18n({ - 'en-US': enUS, - 'zh-CN': zhCN, - }) + Driver.Config, + ]) } export default MySQLDriver diff --git a/packages/mysql/src/locales/en-US.yml b/packages/mysql/src/locales/en-US.yml index 965bf36f..6b951df3 100644 --- a/packages/mysql/src/locales/en-US.yml +++ b/packages/mysql/src/locales/en-US.yml @@ -10,4 +10,3 @@ ssl: - Default - $description: Custom rejectUnauthorized: Reject clients with invalid certificates. -readOnly: Connect in read-only mode. diff --git a/packages/mysql/src/locales/zh-CN.yml b/packages/mysql/src/locales/zh-CN.yml index c3fbddf3..8a79dc6f 100644 --- a/packages/mysql/src/locales/zh-CN.yml +++ b/packages/mysql/src/locales/zh-CN.yml @@ -10,4 +10,3 @@ ssl: - 默认值 - $description: 自定义 rejectUnauthorized: 拒绝使用无效证书的客户端。 -readOnly: 以只读模式连接。 diff --git a/packages/postgres/src/index.ts b/packages/postgres/src/index.ts index a8c95b52..990dfdc3 100644 --- a/packages/postgres/src/index.ts +++ b/packages/postgres/src/index.ts @@ -176,7 +176,6 @@ export class PostgresDriver extends Driver { const table = this.model(name) const { primary, foreign } = table - const immutable = table.immutable || this.config.readOnly const fields = { ...table.avaiableFields() } const unique = [...table.unique] const create: string[] = [] @@ -237,18 +236,21 @@ export class PostgresDriver extends Driver { } } - if (immutable) { + if (!columns.length) { + if (this.config.readonly || this.config.migrateStrategy === 'never') { + throw new Error(`immutable table ${name} cannot be created`) + } + this.logger.info('auto creating table %c', name) + return this.query(`CREATE TABLE ${escapeId(name)} (${create.join(', ')}, _pg_mtime BIGINT)`) + } + + if (this.config.readonly || this.config.migrateStrategy !== 'auto') { if (create.length || update.length) { throw new Error(`immutable table ${name} cannot be migrated`) } return } - if (!columns.length) { - this.logger.info('auto creating table %c', name) - return this.query(`CREATE TABLE ${escapeId(name)} (${create.join(', ')}, _pg_mtime BIGINT)`) - } - const operations = [ ...create.map(def => 'ADD ' + def), ...update, @@ -539,18 +541,18 @@ export namespace PostgresDriver { } export const Config: z = z.intersect([ - Driver.Config, z.object({ host: z.string().default('localhost'), port: z.natural().max(65535).default(5432), user: z.string().default('root'), password: z.string().role('secret'), database: z.string().required(), + }).i18n({ + 'en-US': enUS, + 'zh-CN': zhCN, }), - ]).i18n({ - 'en-US': enUS, - 'zh-CN': zhCN, - }) + Driver.Config, + ]) } export default PostgresDriver diff --git a/packages/postgres/src/locales/en-US.yml b/packages/postgres/src/locales/en-US.yml index 1523c27d..6b373cbe 100644 --- a/packages/postgres/src/locales/en-US.yml +++ b/packages/postgres/src/locales/en-US.yml @@ -3,4 +3,3 @@ port: The port number to connect to. user: The MySQL user to authenticate as. password: The password of that MySQL user. database: Name of the database to use for this connection. -readOnly: Connect in read-only mode. diff --git a/packages/postgres/src/locales/zh-CN.yml b/packages/postgres/src/locales/zh-CN.yml index dfe3f74a..353924bd 100644 --- a/packages/postgres/src/locales/zh-CN.yml +++ b/packages/postgres/src/locales/zh-CN.yml @@ -3,4 +3,3 @@ port: 要连接到的端口号。 username: 要使用的用户名。 password: 要使用的密码。 database: 要访问的数据库名。 -readOnly: 以只读模式连接。 diff --git a/packages/sqlite/src/index.ts b/packages/sqlite/src/index.ts index 845b26b8..ee8cce4f 100644 --- a/packages/sqlite/src/index.ts +++ b/packages/sqlite/src/index.ts @@ -63,7 +63,6 @@ export class SQLiteDriver extends Driver { async prepare(table: string, dropKeys?: string[]) { const columns = this._all(`PRAGMA table_info(${escapeId(table)})`) as SQLiteFieldInfo[] const model = this.model(table) - const immutable = model.immutable || this.config.readOnly const columnDefs: string[] = [] const indexDefs: string[] = [] const alter: string[] = [] @@ -113,7 +112,7 @@ export class SQLiteDriver extends Driver { })) } - if (immutable) { + if (this.config.readonly || this.config.migrateStrategy === 'never') { if (!columns.length || shouldMigrate || alter.length) { throw new Error(`immutable table ${table} cannot be migrated`) } @@ -123,6 +122,8 @@ export class SQLiteDriver extends Driver { if (!columns.length) { this.logger.info('auto creating table %c', table) this._run(`CREATE TABLE ${escapeId(table)} (${[...columnDefs, ...indexDefs].join(', ')})`) + } else if (this.config.migrateStrategy === 'create') { + throw new Error(`immutable table ${table} cannot be migrated`) } else if (shouldMigrate) { // preserve old columns for (const { name, type, notnull, pk, dflt_value: value } of columns) { @@ -495,14 +496,14 @@ export namespace SQLiteDriver { } export const Config: z = z.intersect([ - Driver.Config, z.object({ path: z.string().role('path').required(), + }).i18n({ + 'en-US': enUS, + 'zh-CN': zhCN, }), - ]).i18n({ - 'en-US': enUS, - 'zh-CN': zhCN, - }) + Driver.Config, + ]) } export default SQLiteDriver diff --git a/packages/sqlite/src/locales/en-US.yml b/packages/sqlite/src/locales/en-US.yml index b56a1f79..4728d564 100644 --- a/packages/sqlite/src/locales/en-US.yml +++ b/packages/sqlite/src/locales/en-US.yml @@ -1,2 +1 @@ path: Database path. -readOnly: Connect in read-only mode. diff --git a/packages/sqlite/src/locales/zh-CN.yml b/packages/sqlite/src/locales/zh-CN.yml index 0edeab89..b09c9d6b 100644 --- a/packages/sqlite/src/locales/zh-CN.yml +++ b/packages/sqlite/src/locales/zh-CN.yml @@ -1,2 +1 @@ path: 数据库路径。 -readOnly: 以只读模式连接。 diff --git a/packages/tests/src/migration.ts b/packages/tests/src/migration.ts index d97221fd..7e492825 100644 --- a/packages/tests/src/migration.ts +++ b/packages/tests/src/migration.ts @@ -280,7 +280,7 @@ function MigrationTests(database: Database, options: MigrationOptions = }))).to.not.be.undefined }) - definition && it('model.immutable', async () => { + definition && it('immutable model', async () => { Reflect.deleteProperty(database.tables, 'qux') database.extend('qux', { @@ -299,12 +299,10 @@ function MigrationTests(database: Database, options: MigrationOptions = ]) Reflect.deleteProperty(database.tables, 'qux') - + Object.values(database.drivers)[0].config.migrateStrategy = 'never' database.extend('qux', { id: 'unsigned', text: 'integer' as any, - }, { - immutable: true, }) await expect(database.upsert('qux', [ @@ -314,6 +312,7 @@ function MigrationTests(database: Database, options: MigrationOptions = Reflect.deleteProperty(database.tables, 'qux') Reflect.deleteProperty(database['prepareTasks'], 'qux') + Object.values(database.drivers)[0].config.migrateStrategy = 'auto' }) } From 08e29aaec711cb0be4e761a3edc4a113d6563a28 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Sat, 2 Nov 2024 02:06:06 +0800 Subject: [PATCH 3/3] chore: add tests for coverage --- packages/tests/src/migration.ts | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/tests/src/migration.ts b/packages/tests/src/migration.ts index 7e492825..abeed6d1 100644 --- a/packages/tests/src/migration.ts +++ b/packages/tests/src/migration.ts @@ -281,6 +281,7 @@ function MigrationTests(database: Database, options: MigrationOptions = }) definition && it('immutable model', async () => { + const driver = Object.values(database.drivers)[0] Reflect.deleteProperty(database.tables, 'qux') database.extend('qux', { @@ -299,7 +300,7 @@ function MigrationTests(database: Database, options: MigrationOptions = ]) Reflect.deleteProperty(database.tables, 'qux') - Object.values(database.drivers)[0].config.migrateStrategy = 'never' + driver.config.migrateStrategy = 'never' database.extend('qux', { id: 'unsigned', text: 'integer' as any, @@ -309,10 +310,30 @@ function MigrationTests(database: Database, options: MigrationOptions = { id: 1, text: 'foo' }, { id: 2, text: 'bar' }, ])).to.eventually.be.rejectedWith('immutable') + await expect(database.get('qux', {})).to.eventually.be.rejectedWith('immutable') + + Reflect.deleteProperty(database.tables, 'qux') + Reflect.deleteProperty(database['prepareTasks'], 'qux') + driver.config.migrateStrategy = 'auto' + driver.config.readonly = true + + database.extend('qux', { + id: 'unsigned', + text: 'string(64)', + }) + + await expect(database.get('qux', {})).to.eventually.be.fulfilled + await expect(database.set('qux', 1, { text: 'foo' })).to.eventually.be.rejectedWith('read-only') + await expect(database.upsert('qux', [ + { id: 1, text: 'foo' }, + { id: 2, text: 'bar' }, + ])).to.eventually.be.rejectedWith('read-only') + await expect(database.remove('qux', 1)).to.eventually.be.rejectedWith('read-only') Reflect.deleteProperty(database.tables, 'qux') Reflect.deleteProperty(database['prepareTasks'], 'qux') - Object.values(database.drivers)[0].config.migrateStrategy = 'auto' + driver.config.migrateStrategy = 'auto' + driver.config.readonly = false }) }