From d7d2eb634ee5f69f68ccc48ade3137c6a44dda00 Mon Sep 17 00:00:00 2001 From: ChengLei Shao Date: Mon, 20 Nov 2023 08:54:40 +0800 Subject: [PATCH] feat: mariadb support (#3052) * feat: mariadb support * feat: mariadb test * chore: mariadb test * chore: test * fix: sort field test * fix: sort field test * fix: test * fix: test * fix(bi): chart query support mariadb * chore: test timeout * chore: test --------- Co-authored-by: xilesun <2013xile@gmail.com> --- .github/workflows/nocobase-test-backend.yml | 50 ++ .../core/create-nocobase-app/src/generator.js | 11 + .../database/src/__tests__/bigint.test.ts | 12 +- .../src/__tests__/collection.sortable.test.ts | 1 + .../database/src/__tests__/collection.test.ts | 2 +- .../__tests__/field-options/hidden.test.ts | 1 + .../__tests__/fields/context-field.test.ts | 1 + .../__tests__/fields/has-many-field.test.ts | 2 +- .../src/__tests__/fields/sort-field.test.ts | 1 + .../src/__tests__/fields/uuid-field.test.ts | 1 + .../database/src/__tests__/filter.test.ts | 6 +- .../__tests__/operator/array-operator.test.ts | 2 + .../src/__tests__/operator/eq.test.ts | 1 + .../src/__tests__/operator/notIn.test.ts | 1 + .../src/__tests__/option-parser.test.ts | 2 +- .../database/src/__tests__/repository.test.ts | 9 +- .../src/__tests__/repository/count.test.ts | 2 + .../src/__tests__/repository/find.test.ts | 1 + .../__tests__/repository/update-many.test.ts | 2 + .../core/database/src/__tests__/sort.test.ts | 1 + .../sql-collection/infer-fields.test.ts | 6 +- .../src/__tests__/update-associations.test.ts | 1 + .../__tests__/value-parsers/to-many.test.ts | 1 + packages/core/database/src/collection.ts | 4 - packages/core/database/src/database.ts | 43 +- packages/core/database/src/fields/field.ts | 2 +- .../core/database/src/fields/sort-field.ts | 89 +- packages/core/database/src/helpers.ts | 56 +- packages/core/database/src/model.ts | 2 +- packages/core/database/src/operators/utils.ts | 2 +- packages/core/database/src/options-parser.ts | 2 +- .../query-interface-builder.ts | 1 + .../core/database/src/view/field-type-map.ts | 2 +- packages/core/server/src/application.ts | 33 +- packages/core/test/package.json | 1 + .../core/test/src/scripts/test-db-creator.ts | 28 +- .../http-api/view-collection.test.ts | 30 +- .../20220704225714-drop-foreign-keys.ts | 6 +- .../20221121111110-update-fk-type.ts | 4 +- .../20221121111113-update-id-to-bigint.ts | 4 +- .../repositories/collection-repository.ts | 1 + .../src/server/actions/formatter.ts | 3 +- .../plugin-duplicator/src/server/utils.ts | 2 +- .../server/__tests__/multiple-apps.test.ts | 2 +- .../src/server/server.ts | 7 + .../src/server/repository.ts | 774 +++++++++--------- yarn.lock | 39 +- 47 files changed, 740 insertions(+), 514 deletions(-) diff --git a/.github/workflows/nocobase-test-backend.yml b/.github/workflows/nocobase-test-backend.yml index 6fc0627c6..44a80d52f 100644 --- a/.github/workflows/nocobase-test-backend.yml +++ b/.github/workflows/nocobase-test-backend.yml @@ -173,3 +173,53 @@ jobs: DB_TEST_DISTRIBUTOR_PORT: 23450 DB_TEST_PREFIX: test_ timeout-minutes: 40 + mariadb-test: + strategy: + matrix: + node_version: ['18'] + underscored: [true, false] + runs-on: ubuntu-latest + container: node:${{ matrix.node_version }} + services: + mariadb: + image: mariadb:10.9 + env: + MARIADB_ROOT_PASSWORD: password + MARIADB_DATABASE: nocobase + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node_version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node_version }} + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v3 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Install project dependencies + run: yarn --prefer-offline + - name: Test with MariaDB + run: | + ./node_modules/.bin/tsx packages/core/test/src/scripts/test-db-creator.ts & + sleep 1 + node --max_old_space_size=4096 ./node_modules/.bin/jest --maxWorkers=100% --workerIdleMemoryLimit=3000MB + env: + LOGGER_LEVEL: error + DB_DIALECT: mariadb + DB_HOST: mariadb + DB_PORT: 3306 + DB_USER: root + DB_PASSWORD: password + DB_DATABASE: nocobase + DB_UNDERSCORED: ${{ matrix.underscored }} + DB_TEST_DISTRIBUTOR_PORT: 23450 + DB_TEST_PREFIX: test_ + timeout-minutes: 40 diff --git a/packages/core/create-nocobase-app/src/generator.js b/packages/core/create-nocobase-app/src/generator.js index e76b75837..4670666de 100644 --- a/packages/core/create-nocobase-app/src/generator.js +++ b/packages/core/create-nocobase-app/src/generator.js @@ -70,6 +70,7 @@ class AppGenerator extends Generator { if (allDbDialect) { dependencies.push(`"mysql2": "^2.3.3"`); + dependencies.push(`"mariadb": "^2.5.6"`); dependencies.push(`"pg": "^8.7.3"`); dependencies.push(`"pg-hstore": "^2.3.4"`); dependencies.push(`"sqlite3": "^5.0.8"`); @@ -92,6 +93,16 @@ class AppGenerator extends Generator { envs.push(`DB_USER=${env.DB_USER || ''}`); envs.push(`DB_PASSWORD=${env.DB_PASSWORD || ''}`); break; + case 'mariadb': + if (!allDbDialect) { + dependencies.push(`"mariadb": "^2.5.6"`); + } + envs.push(`DB_HOST=${env.DB_HOST || 'localhost'}`); + envs.push(`DB_PORT=${env.DB_PORT || 3306}`); + envs.push(`DB_DATABASE=${env.DB_DATABASE || ''}`); + envs.push(`DB_USER=${env.DB_USER || ''}`); + envs.push(`DB_PASSWORD=${env.DB_PASSWORD || ''}`); + break; case 'postgres': if (!allDbDialect) { dependencies.push(`"pg": "^8.7.3"`); diff --git a/packages/core/database/src/__tests__/bigint.test.ts b/packages/core/database/src/__tests__/bigint.test.ts index 130283e47..06b59b5a1 100644 --- a/packages/core/database/src/__tests__/bigint.test.ts +++ b/packages/core/database/src/__tests__/bigint.test.ts @@ -27,7 +27,11 @@ excludeSqlite()('collection', () => { await db.sync(); const tableInfo = await db.sequelize.getQueryInterface().describeTable(collection.model.tableName); - expect(tableInfo['id'].type).toBe('BIGINT'); + if (db.inDialect('mariadb')) { + expect(tableInfo['id'].type).toBe('BIGINT(20)'); + } else { + expect(tableInfo['id'].type).toBe('BIGINT'); + } const profile = db.collection({ name: 'profiles', @@ -43,6 +47,10 @@ excludeSqlite()('collection', () => { const profileTableInfo = await db.sequelize.getQueryInterface().describeTable(profile.model.tableName); - expect(profileTableInfo[profile.model.rawAttributes['userId'].field].type).toBe('BIGINT'); + if (db.inDialect('mariadb')) { + expect(profileTableInfo[profile.model.rawAttributes['userId'].field].type).toBe('BIGINT(20)'); + } else { + expect(profileTableInfo[profile.model.rawAttributes['userId'].field].type).toBe('BIGINT'); + } }); }); diff --git a/packages/core/database/src/__tests__/collection.sortable.test.ts b/packages/core/database/src/__tests__/collection.sortable.test.ts index 3c94296ee..4ee1ad52b 100644 --- a/packages/core/database/src/__tests__/collection.sortable.test.ts +++ b/packages/core/database/src/__tests__/collection.sortable.test.ts @@ -6,6 +6,7 @@ describe('collection sortable options', () => { beforeEach(async () => { db = mockDatabase(); + await db.clean({ drop: true }); }); afterEach(async () => { diff --git a/packages/core/database/src/__tests__/collection.test.ts b/packages/core/database/src/__tests__/collection.test.ts index 4cedfd156..7951a25d2 100644 --- a/packages/core/database/src/__tests__/collection.test.ts +++ b/packages/core/database/src/__tests__/collection.test.ts @@ -37,7 +37,7 @@ describe('collection', () => { }); it('should not throw error when create empty collection in sqlite and mysql', async () => { - if (!db.inDialect('sqlite', 'mysql')) { + if (!db.inDialect('sqlite', 'mysql', 'mariadb')) { return; } diff --git a/packages/core/database/src/__tests__/field-options/hidden.test.ts b/packages/core/database/src/__tests__/field-options/hidden.test.ts index c310499e9..33d5eb563 100644 --- a/packages/core/database/src/__tests__/field-options/hidden.test.ts +++ b/packages/core/database/src/__tests__/field-options/hidden.test.ts @@ -7,6 +7,7 @@ describe('hidden field options', () => { beforeEach(async () => { db = mockDatabase(); + await db.clean({ drop: true }); }); afterEach(async () => { diff --git a/packages/core/database/src/__tests__/fields/context-field.test.ts b/packages/core/database/src/__tests__/fields/context-field.test.ts index d1cfc094e..c29d1b0ec 100644 --- a/packages/core/database/src/__tests__/fields/context-field.test.ts +++ b/packages/core/database/src/__tests__/fields/context-field.test.ts @@ -7,6 +7,7 @@ describe('context field', () => { beforeEach(async () => { db = mockDatabase(); + await db.clean({ drop: true }); }); afterEach(async () => { diff --git a/packages/core/database/src/__tests__/fields/has-many-field.test.ts b/packages/core/database/src/__tests__/fields/has-many-field.test.ts index 7bd4ff518..129b85754 100644 --- a/packages/core/database/src/__tests__/fields/has-many-field.test.ts +++ b/packages/core/database/src/__tests__/fields/has-many-field.test.ts @@ -1,6 +1,5 @@ import { Database } from '../../database'; import { mockDatabase } from '../'; -import { makeWatchHost } from 'ts-loader/dist/servicesHost'; import { IdentifierError } from '../../errors/identifier-error'; describe('has many field', () => { @@ -8,6 +7,7 @@ describe('has many field', () => { beforeEach(async () => { db = mockDatabase(); + await db.clean({ drop: true }); }); afterEach(async () => { diff --git a/packages/core/database/src/__tests__/fields/sort-field.test.ts b/packages/core/database/src/__tests__/fields/sort-field.test.ts index ecbd1d049..e53edf6a5 100644 --- a/packages/core/database/src/__tests__/fields/sort-field.test.ts +++ b/packages/core/database/src/__tests__/fields/sort-field.test.ts @@ -8,6 +8,7 @@ describe('string field', () => { beforeEach(async () => { db = mockDatabase(); await db.clean({ drop: true }); + db.registerFieldTypes({ sort: SortField, }); diff --git a/packages/core/database/src/__tests__/fields/uuid-field.test.ts b/packages/core/database/src/__tests__/fields/uuid-field.test.ts index ccd2a576f..bd6351c10 100644 --- a/packages/core/database/src/__tests__/fields/uuid-field.test.ts +++ b/packages/core/database/src/__tests__/fields/uuid-field.test.ts @@ -6,6 +6,7 @@ describe('string field', () => { beforeEach(async () => { db = mockDatabase(); + await db.clean({ drop: true }); }); afterEach(async () => { diff --git a/packages/core/database/src/__tests__/filter.test.ts b/packages/core/database/src/__tests__/filter.test.ts index eab102638..b75fabd78 100644 --- a/packages/core/database/src/__tests__/filter.test.ts +++ b/packages/core/database/src/__tests__/filter.test.ts @@ -142,10 +142,14 @@ describe('filter', () => { }, }); + const userCreatedAt = user.get('createdAt'); + const year = userCreatedAt.getFullYear(); + const month = userCreatedAt.getMonth() + 1; // 月份从0开始,因此加1 + const date = userCreatedAt.getDate(); const count = await PostCollection.repository.count({ filter: { 'user.createdAt': { - $dateOn: user.get('createdAt'), + $dateOn: `${year}-${month}-${date}`, }, }, }); diff --git a/packages/core/database/src/__tests__/operator/array-operator.test.ts b/packages/core/database/src/__tests__/operator/array-operator.test.ts index 96126fd8a..34f7a11ed 100644 --- a/packages/core/database/src/__tests__/operator/array-operator.test.ts +++ b/packages/core/database/src/__tests__/operator/array-operator.test.ts @@ -14,6 +14,8 @@ describe('array field operator', function () { beforeEach(async () => { db = mockDatabase({}); + await db.clean({ drop: true }); + Test = db.collection({ name: 'test', fields: [ diff --git a/packages/core/database/src/__tests__/operator/eq.test.ts b/packages/core/database/src/__tests__/operator/eq.test.ts index a2587ea84..006be28fb 100644 --- a/packages/core/database/src/__tests__/operator/eq.test.ts +++ b/packages/core/database/src/__tests__/operator/eq.test.ts @@ -7,6 +7,7 @@ describe('eq operator', () => { beforeEach(async () => { db = mockDatabase({}); + await db.clean({ drop: true }); Test = db.collection({ name: 'tests', diff --git a/packages/core/database/src/__tests__/operator/notIn.test.ts b/packages/core/database/src/__tests__/operator/notIn.test.ts index 03baab007..04bdc6af4 100644 --- a/packages/core/database/src/__tests__/operator/notIn.test.ts +++ b/packages/core/database/src/__tests__/operator/notIn.test.ts @@ -6,6 +6,7 @@ describe('ne operator', () => { let Test; beforeEach(async () => { db = mockDatabase({}); + await db.clean({ drop: true }); Test = db.collection({ name: 'tests', diff --git a/packages/core/database/src/__tests__/option-parser.test.ts b/packages/core/database/src/__tests__/option-parser.test.ts index 7de220934..c00fa25e9 100644 --- a/packages/core/database/src/__tests__/option-parser.test.ts +++ b/packages/core/database/src/__tests__/option-parser.test.ts @@ -84,7 +84,7 @@ describe('option parser', () => { }); test('with sort option', () => { - if (db.inDialect('mysql')) { + if (db.inDialect('mysql', 'mariadb')) { expect(1).toBe(1); return; } diff --git a/packages/core/database/src/__tests__/repository.test.ts b/packages/core/database/src/__tests__/repository.test.ts index 6b7745f36..df513a520 100644 --- a/packages/core/database/src/__tests__/repository.test.ts +++ b/packages/core/database/src/__tests__/repository.test.ts @@ -81,6 +81,7 @@ describe('repository.find', () => { beforeEach(async () => { db = mockDatabase(); + await db.clean({ drop: true }); User = db.collection({ name: 'users', fields: [ @@ -386,6 +387,8 @@ describe('repository.update', () => { beforeEach(async () => { db = mockDatabase(); + await db.clean({ drop: true }); + User = db.collection({ name: 'users', fields: [ @@ -608,7 +611,7 @@ describe('repository.update', () => { filter: { user: { id: { - $eq: u1.id + $eq: u1.id, }, }, }, @@ -623,9 +626,9 @@ describe('repository.update', () => { const updated = await Post.repository.find({ filter: { id: [p1.id, p2.id], - } + }, }); - expect(updated.map(item => item.name)).toEqual(['p1_1', 'p1_1']); + expect(updated.map((item) => item.name)).toEqual(['p1_1', 'p1_1']); }); }); diff --git a/packages/core/database/src/__tests__/repository/count.test.ts b/packages/core/database/src/__tests__/repository/count.test.ts index 87716daeb..a9c0f9617 100644 --- a/packages/core/database/src/__tests__/repository/count.test.ts +++ b/packages/core/database/src/__tests__/repository/count.test.ts @@ -14,6 +14,8 @@ describe('count', () => { beforeEach(async () => { db = mockDatabase(); + await db.clean({ drop: true }); + User = db.collection({ name: 'users', fields: [ diff --git a/packages/core/database/src/__tests__/repository/find.test.ts b/packages/core/database/src/__tests__/repository/find.test.ts index 9f9bc80f5..ee5e5299d 100644 --- a/packages/core/database/src/__tests__/repository/find.test.ts +++ b/packages/core/database/src/__tests__/repository/find.test.ts @@ -336,6 +336,7 @@ describe('repository find', () => { beforeEach(async () => { db = mockDatabase(); + await db.clean({ drop: true }); User = db.collection<{ id: number; name: string }, { name: string }>({ name: 'users', fields: [ diff --git a/packages/core/database/src/__tests__/repository/update-many.test.ts b/packages/core/database/src/__tests__/repository/update-many.test.ts index ffa94ddbd..aefa89f12 100644 --- a/packages/core/database/src/__tests__/repository/update-many.test.ts +++ b/packages/core/database/src/__tests__/repository/update-many.test.ts @@ -8,6 +8,8 @@ describe('update many', () => { beforeEach(async () => { db = mockDatabase(); + await db.clean({ drop: true }); + db.collection({ name: 't1', fields: [{ type: 'string', name: 'title' }], diff --git a/packages/core/database/src/__tests__/sort.test.ts b/packages/core/database/src/__tests__/sort.test.ts index 069bfc9db..fb62c7fdb 100644 --- a/packages/core/database/src/__tests__/sort.test.ts +++ b/packages/core/database/src/__tests__/sort.test.ts @@ -6,6 +6,7 @@ describe('sort', function () { beforeEach(async () => { db = mockDatabase(); + await db.clean({ drop: true }); }); afterEach(async () => { diff --git a/packages/core/database/src/__tests__/sql-collection/infer-fields.test.ts b/packages/core/database/src/__tests__/sql-collection/infer-fields.test.ts index 6221c76fe..02711cb16 100644 --- a/packages/core/database/src/__tests__/sql-collection/infer-fields.test.ts +++ b/packages/core/database/src/__tests__/sql-collection/infer-fields.test.ts @@ -11,7 +11,7 @@ describe('infer fields', () => { db.collection({ name: 'users', - schema: 'public', + schema: db.inDialect('postgres') ? 'public' : undefined, fields: [ { name: 'id', type: 'bigInt', interface: 'id' }, { name: 'nickname', type: 'string', interface: 'input' }, @@ -19,7 +19,7 @@ describe('infer fields', () => { }); db.collection({ name: 'roles', - schema: 'public', + schema: db.inDialect('postgres') ? 'public' : undefined, fields: [ { name: 'id', type: 'bigInt', interface: 'id' }, { name: 'title', type: 'string', interface: 'input' }, @@ -28,7 +28,7 @@ describe('infer fields', () => { }); db.collection({ name: 'roles_users', - schema: 'public', + schema: db.inDialect('postgres') ? 'public' : undefined, fields: [ { name: 'id', type: 'bigInt', interface: 'id' }, { name: 'userId', type: 'bigInt', interface: 'id' }, diff --git a/packages/core/database/src/__tests__/update-associations.test.ts b/packages/core/database/src/__tests__/update-associations.test.ts index a212d5d47..85351a510 100644 --- a/packages/core/database/src/__tests__/update-associations.test.ts +++ b/packages/core/database/src/__tests__/update-associations.test.ts @@ -359,6 +359,7 @@ describe('update associations', () => { beforeEach(async () => { db = mockDatabase(); + await db.clean({ drop: true }); User = db.collection({ name: 'users', fields: [ diff --git a/packages/core/database/src/__tests__/value-parsers/to-many.test.ts b/packages/core/database/src/__tests__/value-parsers/to-many.test.ts index 3de642236..b90bb26bc 100644 --- a/packages/core/database/src/__tests__/value-parsers/to-many.test.ts +++ b/packages/core/database/src/__tests__/value-parsers/to-many.test.ts @@ -93,6 +93,7 @@ describe.only('china region', () => { beforeEach(async () => { db = mockDatabase(); + await db.clean({ drop: true }); db.collection({ name: 'users', fields: [ diff --git a/packages/core/database/src/collection.ts b/packages/core/database/src/collection.ts index be736eefc..3a4ad53c8 100644 --- a/packages/core/database/src/collection.ts +++ b/packages/core/database/src/collection.ts @@ -42,10 +42,6 @@ function EnsureAtomicity(target: any, propertyKey: string, descriptor: PropertyD const afterRawAttributes = Object.keys(model.rawAttributes); const createdRawAttributes = lodash.difference(afterRawAttributes, beforeRawAttributes); - console.log({ - beforeRawAttributes, - afterRawAttributes, - }); for (const key of createdRawAttributes) { delete this.model.rawAttributes[key]; } diff --git a/packages/core/database/src/database.ts b/packages/core/database/src/database.ts index 9809254f9..27e9d61d0 100644 --- a/packages/core/database/src/database.ts +++ b/packages/core/database/src/database.ts @@ -73,6 +73,8 @@ import { patchSequelizeQueryInterface, snakeCase } from './utils'; import { BaseValueParser, registerFieldValueParsers } from './value-parsers'; import { ViewCollection } from './view-collection'; import { CollectionFactory } from './collection-factory'; +import chalk from 'chalk'; +import { checkDatabaseVersion } from './helpers'; export type MergeOptions = merge.Options; @@ -124,9 +126,13 @@ export const DialectVersionAccessors = { mysql: { sql: 'select version() as version', get: (v: string) => { - if (v.toLowerCase().includes('mariadb')) { - return ''; - } + const m = /([\d+.]+)/.exec(v); + return m[0]; + }, + }, + mariadb: { + sql: 'select version() as version', + get: (v: string) => { const m = /([\d+.]+)/.exec(v); return m[0]; }, @@ -155,7 +161,13 @@ class DatabaseVersion { return false; } const [result] = (await this.db.sequelize.query(accessors[dialect].sql)) as any; - return semver.satisfies(accessors[dialect].get(result?.[0]?.version), versions[dialect]); + const versionResult = accessors[dialect].get(result?.[0]?.version); + + if (lodash.isPlainObject(versionResult) && versionResult.dialect) { + return semver.satisfies(versionResult.version, versions[versionResult.dialect]); + } + + return semver.satisfies(versionResult, versions[dialect]); } } return false; @@ -354,6 +366,7 @@ export class Database extends EventEmitter implements AsyncEmitter { await connection.query('SET search_path TO public;'); }); } + return options; } @@ -466,6 +479,10 @@ export class Database extends EventEmitter implements AsyncEmitter { return dialect.includes(this.sequelize.getDialect()); } + isMySQLCompatibleDialect() { + return this.inDialect('mysql', 'mariadb'); + } + /** * Add collection to database * @param options @@ -670,7 +687,7 @@ export class Database extends EventEmitter implements AsyncEmitter { } async sync(options?: SyncOptions) { - const isMySQL = this.sequelize.getDialect() === 'mysql'; + const isMySQL = this.isMySQLCompatibleDialect(); if (isMySQL) { await this.sequelize.query('SET FOREIGN_KEY_CHECKS = 0', null); } @@ -759,7 +776,23 @@ export class Database extends EventEmitter implements AsyncEmitter { } } + async checkVersion() { + return await checkDatabaseVersion(this); + } + async prepare() { + if (this.isMySQLCompatibleDialect()) { + const result = await this.sequelize.query(`SHOW VARIABLES LIKE 'lower_case_table_names'`, { plain: true }); + + if (result?.Value === '1' && !this.options.underscored) { + console.log( + `Your database lower_case_table_names=1, please add ${chalk.yellow('DB_UNDERSCORED=true')} to the .env file`, + ); + + process.exit(); + } + } + if (this.inDialect('postgres') && this.options.schema && this.options.schema != 'public') { await this.sequelize.query(`CREATE SCHEMA IF NOT EXISTS "${this.options.schema}"`, null); } diff --git a/packages/core/database/src/fields/field.ts b/packages/core/database/src/fields/field.ts index 891afb920..5e4910f46 100644 --- a/packages/core/database/src/fields/field.ts +++ b/packages/core/database/src/fields/field.ts @@ -189,7 +189,7 @@ export abstract class Field { sql = `SELECT * from pragma_table_info('${this.collection.model.tableName}') WHERE name = '${this.columnName()}'`; - } else if (this.database.inDialect('mysql')) { + } else if (this.database.inDialect('mysql', 'mariadb')) { sql = ` select column_name from INFORMATION_SCHEMA.COLUMNS diff --git a/packages/core/database/src/fields/sort-field.ts b/packages/core/database/src/fields/sort-field.ts index 973a1dfa8..41cea73fc 100644 --- a/packages/core/database/src/fields/sort-field.ts +++ b/packages/core/database/src/fields/sort-field.ts @@ -93,47 +93,60 @@ export class SortField extends Field { const quotedOrderField = queryInterface.quoteIdentifier(orderField); - const sortColumnName = this.collection.model.rawAttributes[this.name].field; + const sortColumnName = queryInterface.quoteIdentifier(this.collection.model.rawAttributes[this.name].field); - const sql = ` - WITH ordered_table AS ( - SELECT *, ROW_NUMBER() OVER (${ - scopeKey ? `PARTITION BY ${queryInterface.quoteIdentifier(scopeKey)}` : '' - } ORDER BY ${quotedOrderField}) AS new_sequence_number - FROM ${this.collection.quotedTableName()} - ${(() => { - if (scopeKey && scopeValue) { - const hasNull = scopeValue.includes(null); + let sql: string; - return `WHERE ${queryInterface.quoteIdentifier(scopeKey)} IN (${scopeValue - .filter((v) => v !== null) - .map((v) => `'${v}'`) - .join(',')}) ${hasNull ? `OR ${queryInterface.quoteIdentifier(scopeKey)} IS NULL` : ''} `; - } - - return ''; - })()} - - ) - ${ - this.collection.db.inDialect('mysql') - ? ` - UPDATE ${this.collection.quotedTableName()}, ordered_table - SET ${this.collection.quotedTableName()}.${sortColumnName} = ordered_table.new_sequence_number - WHERE ${this.collection.quotedTableName()}.${quotedOrderField} = ordered_table.${quotedOrderField} - ` - : ` - UPDATE ${this.collection.quotedTableName()} - SET ${queryInterface.quoteIdentifier(sortColumnName)} = ordered_table.new_sequence_number - FROM ordered_table - WHERE ${this.collection.quotedTableName()}.${quotedOrderField} = ${queryInterface.quoteIdentifier( - 'ordered_table', - )}.${quotedOrderField}; - ` - } - - `; + const whereClause = + scopeKey && scopeValue + ? ` + WHERE ${queryInterface.quoteIdentifier(scopeKey)} IN (${scopeValue + .filter((v) => v !== null) + .map((v) => `'${v}'`) + .join(', ')})${scopeValue.includes(null) ? ` OR ${queryInterface.quoteIdentifier(scopeKey)} IS NULL` : ''}` + : ''; + if (this.collection.db.inDialect('postgres')) { + sql = ` + UPDATE ${this.collection.quotedTableName()} + SET ${sortColumnName} = ordered_table.new_sequence_number + FROM ( + SELECT *, ROW_NUMBER() OVER (${ + scopeKey ? `PARTITION BY ${queryInterface.quoteIdentifier(scopeKey)}` : '' + } ORDER BY ${quotedOrderField}) AS new_sequence_number + FROM ${this.collection.quotedTableName()} + ${whereClause} + ) AS ordered_table + WHERE ${this.collection.quotedTableName()}.${quotedOrderField} = ordered_table.${quotedOrderField}; + `; + } else if (this.collection.db.inDialect('sqlite')) { + sql = ` + UPDATE ${this.collection.quotedTableName()} + SET ${sortColumnName} = ( + SELECT new_sequence_number + FROM ( + SELECT *, ROW_NUMBER() OVER (${ + scopeKey ? `PARTITION BY ${queryInterface.quoteIdentifier(scopeKey)}` : '' + } ORDER BY ${quotedOrderField}) AS new_sequence_number + FROM ${this.collection.quotedTableName()} + ${whereClause} + ) AS ordered_table + WHERE ${this.collection.quotedTableName()}.${quotedOrderField} = ordered_table.${quotedOrderField} + ); + `; + } else if (this.collection.db.inDialect('mysql') || this.collection.db.inDialect('mariadb')) { + sql = ` + UPDATE ${this.collection.quotedTableName()} + JOIN ( + SELECT *, ROW_NUMBER() OVER (${ + scopeKey ? `PARTITION BY ${queryInterface.quoteIdentifier(scopeKey)}` : '' + } ORDER BY ${quotedOrderField}) AS new_sequence_number + FROM ${this.collection.quotedTableName()} + ${whereClause} + ) AS ordered_table ON ${this.collection.quotedTableName()}.${quotedOrderField} = ordered_table.${quotedOrderField} + SET ${this.collection.quotedTableName()}.${sortColumnName} = ordered_table.new_sequence_number; + `; + } await this.collection.db.sequelize.query(sql, { transaction, }); diff --git a/packages/core/database/src/helpers.ts b/packages/core/database/src/helpers.ts index 3307a74f9..184bf8138 100644 --- a/packages/core/database/src/helpers.ts +++ b/packages/core/database/src/helpers.ts @@ -1,5 +1,6 @@ -import { IDatabaseOptions } from './database'; +import { Database, IDatabaseOptions } from './database'; import fs from 'fs'; +import semver from 'semver'; function getEnvValue(key, defaultValue?) { return process.env[key] || defaultValue; @@ -90,3 +91,56 @@ function customLogger(queryString, queryObject) { console.log(queryObject.bind); } } + +const dialectVersionAccessors = { + sqlite: { + sql: 'select sqlite_version() as version', + get: (v: string) => v, + version: '3.x', + }, + mysql: { + sql: 'select version() as version', + get: (v: string) => { + const m = /([\d+.]+)/.exec(v); + return m[0]; + }, + version: '>=8.0.17', + }, + mariadb: { + sql: 'select version() as version', + get: (v: string) => { + const m = /([\d+.]+)/.exec(v); + return m[0]; + }, + version: '>=10.9', + }, + postgres: { + sql: 'select version() as version', + get: (v: string) => { + const m = /([\d+.]+)/.exec(v); + return semver.minVersion(m[0]).version; + }, + version: '>=10', + }, +}; + +export async function checkDatabaseVersion(db: Database) { + const dialect = db.sequelize.getDialect(); + const accessor = dialectVersionAccessors[dialect]; + if (!accessor) { + throw new Error(`unsupported dialect ${dialect}`); + } + + const result = await db.sequelize.query(accessor.sql, { + type: 'SELECT', + }); + + // @ts-ignore + const version = accessor.get(result?.[0]?.version); + const versionResult = semver.satisfies(version, accessor.version); + if (!versionResult) { + throw new Error(`to use ${dialect}, please ensure the version is ${accessor.version}`); + } + + return true; +} diff --git a/packages/core/database/src/model.ts b/packages/core/database/src/model.ts index ef1f4eee3..7114a7d63 100644 --- a/packages/core/database/src/model.ts +++ b/packages/core/database/src/model.ts @@ -62,7 +62,7 @@ export class Model { }; const isMySQL = (ctx) => { - return getDialect(ctx) === 'mysql'; + return getDialect(ctx) === 'mysql' || getDialect(ctx) === 'mariadb'; }; export { getDialect, isPg, isMySQL }; diff --git a/packages/core/database/src/options-parser.ts b/packages/core/database/src/options-parser.ts index 38e3c2729..12a66418d 100644 --- a/packages/core/database/src/options-parser.ts +++ b/packages/core/database/src/options-parser.ts @@ -125,7 +125,7 @@ export class OptionsParser { } sortField.push(direction); - if (this.database.inDialect('mysql')) { + if (this.database.isMySQLCompatibleDialect()) { orderParams.push([Sequelize.fn('ISNULL', Sequelize.col(`${this.model.name}.${sortField[0]}`))]); } orderParams.push(sortField); diff --git a/packages/core/database/src/query-interface/query-interface-builder.ts b/packages/core/database/src/query-interface/query-interface-builder.ts index 7944099ff..7ffacef48 100644 --- a/packages/core/database/src/query-interface/query-interface-builder.ts +++ b/packages/core/database/src/query-interface/query-interface-builder.ts @@ -6,6 +6,7 @@ import SqliteQueryInterface from './sqlite-query-interface'; export default function buildQueryInterface(db: Database) { const map = { mysql: MysqlQueryInterface, + mariadb: MysqlQueryInterface, postgres: PostgresQueryInterface, sqlite: SqliteQueryInterface, }; diff --git a/packages/core/database/src/view/field-type-map.ts b/packages/core/database/src/view/field-type-map.ts index 7bf3d270f..526b290c5 100644 --- a/packages/core/database/src/view/field-type-map.ts +++ b/packages/core/database/src/view/field-type-map.ts @@ -55,4 +55,4 @@ const sqlite = { json: ['json', 'array'], }; -export default { postgres, mysql, sqlite }; +export default { postgres, mysql, sqlite, mariadb: mysql }; diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index bf89c01ee..e28edc9c0 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -366,7 +366,7 @@ export class Application exten } this._authenticated = true; await this.db.auth(); - await this.dbVersionCheck({ exit: true }); + await this.db.checkVersion(); await this.db.prepare(); } @@ -559,37 +559,6 @@ export class Application exten this.logger.debug('finish destroy app'); } - async dbVersionCheck(options?: { exit?: boolean }) { - const r = await this.db.version.satisfies({ - mysql: '>=8.0.17', - sqlite: '3.x', - postgres: '>=10', - }); - - if (!r) { - console.log(chalk.red('The database only supports MySQL 8.0.17 and above, SQLite 3.x and PostgreSQL 10+')); - if (options?.exit) { - process.exit(); - } - return false; - } - - if (this.db.inDialect('mysql')) { - const result = await this.db.sequelize.query(`SHOW VARIABLES LIKE 'lower_case_table_names'`, { plain: true }); - if (result?.Value === '1' && !this.db.options.underscored) { - console.log( - `Your database lower_case_table_names=1, please add ${chalk.yellow('DB_UNDERSCORED=true')} to the .env file`, - ); - if (options?.exit) { - process.exit(); - } - return false; - } - } - - return true; - } - async isInstalled() { return ( (await this.db.collectionExistsInDb('applicationVersion')) || (await this.db.collectionExistsInDb('collections')) diff --git a/packages/core/test/package.json b/packages/core/test/package.json index 7f0d2b91d..ba31950b0 100644 --- a/packages/core/test/package.json +++ b/packages/core/test/package.json @@ -12,6 +12,7 @@ "pg": "^8.7.3", "pg-hstore": "^2.3.4", "sqlite3": "^5.0.8", + "mariadb": "^2.5.6", "supertest": "^6.1.6", "ws": "^8.13.0" }, diff --git a/packages/core/test/src/scripts/test-db-creator.ts b/packages/core/test/src/scripts/test-db-creator.ts index bd5c985f8..8b9955bee 100644 --- a/packages/core/test/src/scripts/test-db-creator.ts +++ b/packages/core/test/src/scripts/test-db-creator.ts @@ -4,12 +4,13 @@ import pg from 'pg'; import dotenv from 'dotenv'; import path from 'path'; import mysql from 'mysql2/promise'; +import mariadb from 'mariadb'; dotenv.config({ path: path.resolve(process.cwd(), '.env.test') }); abstract class BaseClient { - private createdDBs: Set = new Set(); protected _client: Client | null = null; + private createdDBs: Set = new Set(); abstract _createDB(name: string): Promise; abstract _createConnection(): Promise; @@ -89,6 +90,28 @@ class MySQLClient extends BaseClient { } } +class MariaDBClient extends BaseClient { + async _removeDB(name: string): Promise { + await this._client.query(`DROP DATABASE IF EXISTS ${name}`); + } + + async _createDB(name: string): Promise { + await this._client.query(`CREATE DATABASE IF NOT EXISTS ${name}`); + } + + async _createConnection(): Promise { + const connection = await mariadb.createConnection({ + host: process.env['DB_HOST'], + port: Number(process.env['DB_PORT']), + user: process.env['DB_USER'], + password: process.env['DB_PASSWORD'], + database: process.env['DB_DATABASE'], + }); + + return connection; + } +} + const client = { postgres: () => { return new PostgresClient(); @@ -96,6 +119,9 @@ const client = { mysql: () => { return new MySQLClient(); }, + mariadb: () => { + return new MariaDBClient(); + }, }; const dialect = process.env['DB_DIALECT']; diff --git a/packages/plugins/@nocobase/plugin-collection-manager/src/server/__tests__/http-api/view-collection.test.ts b/packages/plugins/@nocobase/plugin-collection-manager/src/server/__tests__/http-api/view-collection.test.ts index 8c9bb7d0e..d66f22cae 100644 --- a/packages/plugins/@nocobase/plugin-collection-manager/src/server/__tests__/http-api/view-collection.test.ts +++ b/packages/plugins/@nocobase/plugin-collection-manager/src/server/__tests__/http-api/view-collection.test.ts @@ -1,4 +1,4 @@ -import { Database, Repository, Field, DataTypes } from '@nocobase/database'; +import { Database, DataTypes, Field, Repository } from '@nocobase/database'; import { MockServer } from '@nocobase/test'; import { uid } from '@nocobase/utils'; import { createApp } from '../index'; @@ -56,10 +56,6 @@ SELECT * FROM numbers; it('should support preview field with getter', async () => { class TestField extends Field { - get dataType() { - return DataTypes.STRING; - } - constructor(options: any, context: any) { const { name } = options; super( @@ -72,6 +68,10 @@ SELECT * FROM numbers; context, ); } + + get dataType() { + return DataTypes.STRING; + } } db.registerFieldTypes({ @@ -165,24 +165,18 @@ SELECT * FROM numbers; expect(response.status).toBe(200); const data = response.body.data; - if (app.db.options.dialect === 'mysql') { + if (app.db.inDialect('mysql')) { expect(data.fields.n.type).toBe('bigInt'); - } else if (app.db.options.dialect == 'postgres') { + } else if (app.db.inDialect('postgres', 'mariadb')) { expect(data.fields.n.type).toBe('integer'); } - - console.log( - JSON.stringify( - { - nField: data.fields.n, - }, - null, - 2, - ), - ); }); it('should return possible types for json fields', async () => { + if (app.db.inDialect('mariadb')) { + // can not get json type from mariadb + return; + } const jsonViewName = 'json_view'; const dropSql = `DROP VIEW IF EXISTS ${jsonViewName}`; await app.db.sequelize.query(dropSql); @@ -203,9 +197,11 @@ SELECT * FROM numbers; expect(response.status).toBe(200); const data = response.body.data; + if (!app.db.inDialect('sqlite')) { expect(data.fields.json_field.type).toBe('json'); } + expect(data.fields.json_field.possibleTypes).toBeTruthy(); }); diff --git a/packages/plugins/@nocobase/plugin-collection-manager/src/server/migrations/20220704225714-drop-foreign-keys.ts b/packages/plugins/@nocobase/plugin-collection-manager/src/server/migrations/20220704225714-drop-foreign-keys.ts index aa1e92a85..d4a63cedf 100644 --- a/packages/plugins/@nocobase/plugin-collection-manager/src/server/migrations/20220704225714-drop-foreign-keys.ts +++ b/packages/plugins/@nocobase/plugin-collection-manager/src/server/migrations/20220704225714-drop-foreign-keys.ts @@ -8,11 +8,11 @@ export default class DropForeignKeysMigration extends Migration { } const transaction = await this.db.sequelize.transaction(); try { - if (this.db.inDialect('mysql')) { + if (this.db.isMySQLCompatibleDialect()) { const [results]: any = await this.db.sequelize.query( ` - SELECT CONCAT('ALTER TABLE ',TABLE_SCHEMA,'.',TABLE_NAME,' DROP FOREIGN KEY ',CONSTRAINT_NAME,' ;') as q - FROM information_schema.TABLE_CONSTRAINTS c + SELECT CONCAT('ALTER TABLE ',TABLE_SCHEMA,'.',TABLE_NAME,' DROP FOREIGN KEY ',CONSTRAINT_NAME,' ;') as q + FROM information_schema.TABLE_CONSTRAINTS c WHERE c.TABLE_SCHEMA='${this.db.options.database}' AND c.CONSTRAINT_TYPE='FOREIGN KEY'; `, { transaction }, diff --git a/packages/plugins/@nocobase/plugin-collection-manager/src/server/migrations/20221121111110-update-fk-type.ts b/packages/plugins/@nocobase/plugin-collection-manager/src/server/migrations/20221121111110-update-fk-type.ts index 97810a7cb..3d8ee64d1 100644 --- a/packages/plugins/@nocobase/plugin-collection-manager/src/server/migrations/20221121111110-update-fk-type.ts +++ b/packages/plugins/@nocobase/plugin-collection-manager/src/server/migrations/20221121111110-update-fk-type.ts @@ -19,7 +19,7 @@ export default class UpdateIdToBigIntMigrator extends Migration { }, }); - if (!db.inDialect('mysql', 'postgres')) { + if (!db.inDialect('mysql', 'mariadb', 'postgres')) { return; } @@ -36,7 +36,7 @@ export default class UpdateIdToBigIntMigrator extends Migration { if (model.rawAttributes[fieldName].type instanceof DataTypes.INTEGER) { if (db.inDialect('postgres')) { sql = `ALTER TABLE "${tableName}" ALTER COLUMN "${fieldName}" SET DATA TYPE BIGINT;`; - } else if (db.inDialect('mysql')) { + } else if (db.inDialect('mysql', 'mariadb')) { const dataTypeOrOptions = model.rawAttributes[fieldName]; const attributeName = fieldName; diff --git a/packages/plugins/@nocobase/plugin-collection-manager/src/server/migrations/20221121111113-update-id-to-bigint.ts b/packages/plugins/@nocobase/plugin-collection-manager/src/server/migrations/20221121111113-update-id-to-bigint.ts index c14be4c63..eebd2258a 100644 --- a/packages/plugins/@nocobase/plugin-collection-manager/src/server/migrations/20221121111113-update-id-to-bigint.ts +++ b/packages/plugins/@nocobase/plugin-collection-manager/src/server/migrations/20221121111113-update-id-to-bigint.ts @@ -23,7 +23,7 @@ export default class UpdateIdToBigIntMigrator extends Migration { }, }); - if (!db.inDialect('mysql', 'postgres')) { + if (!db.inDialect('mysql', 'mariadb', 'postgres')) { return; } @@ -59,7 +59,7 @@ export default class UpdateIdToBigIntMigrator extends Migration { if (model.rawAttributes[fieldName].type instanceof DataTypes.INTEGER) { if (db.inDialect('postgres')) { sql = `ALTER TABLE ${quoteTableName} ALTER COLUMN "${columnName}" SET DATA TYPE BIGINT;`; - } else if (db.inDialect('mysql')) { + } else if (db.inDialect('mysql', 'mariadb')) { const dataTypeOrOptions = model.rawAttributes[fieldName]; const attributeName = fieldName; diff --git a/packages/plugins/@nocobase/plugin-collection-manager/src/server/repositories/collection-repository.ts b/packages/plugins/@nocobase/plugin-collection-manager/src/server/repositories/collection-repository.ts index 10a33a5f7..fb17f96b0 100644 --- a/packages/plugins/@nocobase/plugin-collection-manager/src/server/repositories/collection-repository.ts +++ b/packages/plugins/@nocobase/plugin-collection-manager/src/server/repositories/collection-repository.ts @@ -90,6 +90,7 @@ export class CollectionRepository extends Repository { if (lodash.isArray(skipField) && skipField.length) { lazyCollectionFields[instanceName] = skipField; } + this.database.logger.debug(`load ${instanceName} collection`); this.app.setMaintainingMessage(`load ${instanceName} collection`); diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/server/actions/formatter.ts b/packages/plugins/@nocobase/plugin-data-visualization/src/server/actions/formatter.ts index 850872368..2fe4b6598 100644 --- a/packages/plugins/@nocobase/plugin-data-visualization/src/server/actions/formatter.ts +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/server/actions/formatter.ts @@ -10,6 +10,7 @@ export const dateFormatFn = (sequelize: any, dialect: string, field: string, for .replace(/ss/g, '%S'); return sequelize.fn('strftime', format, sequelize.col(field)); case 'mysql': + case 'mariadb': format = format .replace(/YYYY/g, '%Y') .replace(/MM/g, '%m') @@ -22,7 +23,7 @@ export const dateFormatFn = (sequelize: any, dialect: string, field: string, for format = format.replace(/hh/g, 'HH24').replace(/mm/g, 'MI').replace(/ss/g, 'SS'); return sequelize.fn('to_char', sequelize.col(field), format); default: - return field; + return sequelize.col(field); } }; diff --git a/packages/plugins/@nocobase/plugin-duplicator/src/server/utils.ts b/packages/plugins/@nocobase/plugin-duplicator/src/server/utils.ts index 657c155d5..4897b33ef 100644 --- a/packages/plugins/@nocobase/plugin-duplicator/src/server/utils.ts +++ b/packages/plugins/@nocobase/plugin-duplicator/src/server/utils.ts @@ -6,7 +6,7 @@ import readline from 'readline'; export const DUMPED_EXTENSION = 'nbdump'; export function sqlAdapter(database: Database, sql: string) { - if (database.sequelize.getDialect() === 'mysql') { + if (database.isMySQLCompatibleDialect()) { return lodash.replace(sql, /"/g, '`'); } diff --git a/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/__tests__/multiple-apps.test.ts b/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/__tests__/multiple-apps.test.ts index 934fff93d..0ef36863f 100644 --- a/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/__tests__/multiple-apps.test.ts +++ b/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/__tests__/multiple-apps.test.ts @@ -306,7 +306,7 @@ describe('multiple apps', () => { }); await app.start(); - await sleep(5000); + await sleep(10000); expect(AppSupervisor.getInstance().hasApp(subAppName)).toBeTruthy(); const appStatus = AppSupervisor.getInstance().getAppStatus(subAppName); expect(appStatus).toEqual('running'); diff --git a/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/server.ts b/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/server.ts index 178b51a9f..fc939ce3f 100644 --- a/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/server.ts @@ -65,6 +65,13 @@ const defaultDbCreator = async (app: Application) => { await connection.close(); } + if (dialect === 'mariadb') { + const mariadb = require('mariadb'); + const connection = await mariadb.createConnection({ host, port, user: username, password }); + await connection.query(`CREATE DATABASE IF NOT EXISTS \`${database}\`;`); + await connection.end(); + } + if (dialect === 'postgres') { const { Client } = require('pg'); diff --git a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/repository.ts b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/repository.ts index a81a9207d..c42f561b6 100644 --- a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/repository.ts +++ b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/repository.ts @@ -72,41 +72,6 @@ function transaction(transactionAbleArgPosition?: number) { export class UiSchemaRepository extends Repository { cache: Cache; - // if you need to handle cache in repo method, so you must set cache first - setCache(cache: Cache) { - this.cache = cache; - } - - /** - * clear cache with xUid which in uiSchemaTreePath's Path - * @param {string} xUid - * @param {Transaction} transaction - * @returns {Promise} - */ - async clearXUidPathCache(xUid: string, transaction: Transaction) { - if (!this.cache || !xUid) { - return; - } - // find all xUid node's parent nodes - const uiSchemaNodes = await this.database.getRepository('uiSchemaTreePath').find({ - filter: { - descendant: xUid, - }, - transaction: transaction, - }); - for (const uiSchemaNode of uiSchemaNodes) { - await this.cache.del(`p_${uiSchemaNode['ancestor']}`); - await this.cache.del(`s_${uiSchemaNode['ancestor']}`); - } - } - - tableNameAdapter(tableName) { - if (this.database.sequelize.getDialect() === 'postgres') { - return `"${this.database.options.schema || 'public'}"."${tableName}"`; - } - return tableName; - } - get uiSchemasTableName() { return this.tableNameAdapter(this.model.tableName); } @@ -116,14 +81,6 @@ export class UiSchemaRepository extends Repository { return this.tableNameAdapter(model.tableName); } - sqlAdapter(sql: string) { - if (this.database.sequelize.getDialect() === 'mysql') { - return lodash.replace(sql, /"/g, '`'); - } - - return sql; - } - static schemaToSingleNodes(schema: any, carry: SchemaNode[] = [], childOptions: ChildOptions = null): SchemaNode[] { const node = lodash.cloneDeep( lodash.isString(schema) @@ -180,6 +137,49 @@ export class UiSchemaRepository extends Repository { return carry; } + // if you need to handle cache in repo method, so you must set cache first + setCache(cache: Cache) { + this.cache = cache; + } + + /** + * clear cache with xUid which in uiSchemaTreePath's Path + * @param {string} xUid + * @param {Transaction} transaction + * @returns {Promise} + */ + async clearXUidPathCache(xUid: string, transaction: Transaction) { + if (!this.cache || !xUid) { + return; + } + // find all xUid node's parent nodes + const uiSchemaNodes = await this.database.getRepository('uiSchemaTreePath').find({ + filter: { + descendant: xUid, + }, + transaction: transaction, + }); + for (const uiSchemaNode of uiSchemaNodes) { + await this.cache.del(`p_${uiSchemaNode['ancestor']}`); + await this.cache.del(`s_${uiSchemaNode['ancestor']}`); + } + } + + tableNameAdapter(tableName) { + if (this.database.sequelize.getDialect() === 'postgres') { + return `"${this.database.options.schema || 'public'}"."${tableName}"`; + } + return tableName; + } + + sqlAdapter(sql: string) { + if (this.database.isMySQLCompatibleDialect()) { + return lodash.replace(sql, /"/g, '`'); + } + + return sql; + } + async getProperties(uid: string, options: GetPropertiesOptions = {}) { if (options?.readFromCache && this.cache) { return this.cache.wrap(`p_${uid}`, () => { @@ -189,66 +189,6 @@ export class UiSchemaRepository extends Repository { return this.doGetProperties(uid, options); } - private async doGetProperties(uid: string, options: GetPropertiesOptions = {}) { - const { transaction } = options; - - const db = this.database; - - const rawSql = ` - SELECT "SchemaTable"."x-uid" as "x-uid", "SchemaTable"."name" as "name", "SchemaTable"."schema" as "schema", - TreePath.depth as depth, - NodeInfo.type as type, NodeInfo.async as async, ParentPath.ancestor as parent, ParentPath.sort as sort - FROM ${this.uiSchemaTreePathTableName} as TreePath - LEFT JOIN ${this.uiSchemasTableName} as "SchemaTable" ON "SchemaTable"."x-uid" = TreePath.descendant - LEFT JOIN ${this.uiSchemaTreePathTableName} as NodeInfo ON NodeInfo.descendant = "SchemaTable"."x-uid" and NodeInfo.descendant = NodeInfo.ancestor and NodeInfo.depth = 0 - LEFT JOIN ${this.uiSchemaTreePathTableName} as ParentPath ON (ParentPath.descendant = "SchemaTable"."x-uid" AND ParentPath.depth = 1) - WHERE TreePath.ancestor = :ancestor AND (NodeInfo.async = false or TreePath.depth = 1)`; - - const nodes = await db.sequelize.query(this.sqlAdapter(rawSql), { - replacements: { - ancestor: uid, - }, - transaction, - }); - - if (nodes[0].length == 0) { - return {}; - } - - const schema = this.nodesToSchema(nodes[0], uid); - return lodash.pick(schema, ['type', 'properties']); - } - - private async doGetJsonSchema(uid: string, options?: GetJsonSchemaOptions) { - const db = this.database; - - const treeTable = this.uiSchemaTreePathTableName; - - const rawSql = ` - SELECT "SchemaTable"."x-uid" as "x-uid", "SchemaTable"."name" as name, "SchemaTable"."schema" as "schema" , - TreePath.depth as depth, - NodeInfo.type as type, NodeInfo.async as async, ParentPath.ancestor as parent, ParentPath.sort as sort - FROM ${treeTable} as TreePath - LEFT JOIN ${this.uiSchemasTableName} as "SchemaTable" ON "SchemaTable"."x-uid" = TreePath.descendant - LEFT JOIN ${treeTable} as NodeInfo ON NodeInfo.descendant = "SchemaTable"."x-uid" and NodeInfo.descendant = NodeInfo.ancestor and NodeInfo.depth = 0 - LEFT JOIN ${treeTable} as ParentPath ON (ParentPath.descendant = "SchemaTable"."x-uid" AND ParentPath.depth = 1) - WHERE TreePath.ancestor = :ancestor ${options?.includeAsyncNode ? '' : 'AND (NodeInfo.async != true )'} - `; - - const nodes = await db.sequelize.query(this.sqlAdapter(rawSql), { - replacements: { - ancestor: uid, - }, - transaction: options?.transaction, - }); - - if (nodes[0].length == 0) { - return {}; - } - - return this.nodesToSchema(nodes[0], uid); - } - async getJsonSchema(uid: string, options?: GetJsonSchemaOptions): Promise { if (options?.readFromCache && this.cache) { return this.cache.wrap(`s_${uid}`, () => { @@ -258,10 +198,6 @@ export class UiSchemaRepository extends Repository { return this.doGetJsonSchema(uid, options); } - private ignoreSchemaProperties(schemaProperties) { - return lodash.omit(schemaProperties, nodeKeys); - } - nodesToSchema(nodes, rootUid) { const nodeAttributeSanitize = (node) => { const schema = { @@ -374,104 +310,6 @@ export class UiSchemaRepository extends Repository { } } - protected async updateNode(uid: string, schema: any, transaction?: Transaction) { - const nodeModel = await this.findOne({ - filter: { - 'x-uid': uid, - }, - }); - - await nodeModel.update( - { - schema: { - ...(nodeModel.get('schema') as any), - ...lodash.omit(schema, ['x-async', 'name', 'x-uid', 'properties']), - }, - }, - { - hooks: false, - transaction, - }, - ); - - if (schema['x-server-hooks']) { - await this.database.emitAsync(`${this.collection.name}.afterSave`, nodeModel, { transaction }); - } - } - - protected async childrenCount(uid, transaction) { - const db = this.database; - - const countResult = await db.sequelize.query( - `SELECT COUNT(*) as count FROM ${this.uiSchemaTreePathTableName} where ancestor = :ancestor and depth = 1`, - { - replacements: { - ancestor: uid, - }, - type: 'SELECT', - transaction, - }, - ); - - return parseInt(countResult[0]['count']); - } - - protected async isLeafNode(uid, transaction) { - const childrenCount = await this.childrenCount(uid, transaction); - return childrenCount === 0; - } - - protected async findParentUid(uid, transaction?) { - const parent = await this.database.getRepository('uiSchemaTreePath').findOne({ - filter: { - descendant: uid, - depth: 1, - }, - transaction, - }); - - return parent ? (parent.get('ancestor') as string) : null; - } - - protected async findNodeSchemaWithParent(uid, transaction) { - const schema = await this.database.getRepository('uiSchemas').findOne({ - filter: { - 'x-uid': uid, - }, - transaction, - }); - - return { - parentUid: await this.findParentUid(uid, transaction), - schema, - }; - } - - protected async isSingleChild(uid, transaction) { - const db = this.database; - - const parent = await this.findParentUid(uid, transaction); - - if (!parent) { - return null; - } - - const parentChildrenCount = await this.childrenCount(parent, transaction); - - if (parentChildrenCount == 1) { - const schema = await db.getRepository('uiSchemas').findOne({ - filter: { - 'x-uid': parent, - }, - transaction, - }); - - return schema; - } - - return null; - } - async removeEmptyParents(options: Transactionable & { uid: string; breakRemoveOn?: BreakRemoveOnType }) { const { transaction, uid, breakRemoveOn } = options; @@ -490,22 +328,6 @@ export class UiSchemaRepository extends Repository { await removeParent(uid); } - private breakOnMatched(schemaInstance, breakRemoveOn: BreakRemoveOnType): boolean { - if (!breakRemoveOn) { - return false; - } - - for (const key of Object.keys(breakRemoveOn)) { - const instanceValue = schemaInstance.get(key); - const breakRemoveOnValue = breakRemoveOn[key]; - if (instanceValue !== breakRemoveOnValue) { - return false; - } - } - - return true; - } - async recursivelyRemoveIfNoChildren(options: Transactionable & { uid: string; breakRemoveOn?: BreakRemoveOnType }) { const { uid, transaction, breakRemoveOn } = options; @@ -572,92 +394,6 @@ export class UiSchemaRepository extends Repository { ); } - @transaction() - protected async insertBeside( - targetUid: string, - schema: any, - side: 'before' | 'after', - options?: InsertAdjacentOptions, - ) { - const { transaction } = options; - const targetParent = await this.findParentUid(targetUid, transaction); - - const db = this.database; - - const treeTable = this.uiSchemaTreePathTableName; - - const typeQuery = await db.sequelize.query(`SELECT type from ${treeTable} WHERE ancestor = :uid AND depth = 0;`, { - type: 'SELECT', - replacements: { - uid: targetUid, - }, - transaction, - }); - - const nodes = UiSchemaRepository.schemaToSingleNodes(schema); - - const rootNode = nodes[0]; - - rootNode.childOptions = { - parentUid: targetParent, - type: typeQuery[0]['type'], - position: { - type: side, - target: targetUid, - }, - }; - - const insertedNodes = await this.insertNodes(nodes, options); - return await this.getJsonSchema(insertedNodes[0].get('x-uid'), { - transaction, - }); - } - - @transaction() - protected async insertInner( - targetUid: string, - schema: any, - position: 'first' | 'last', - options?: InsertAdjacentOptions, - ) { - const { transaction } = options; - - const nodes = UiSchemaRepository.schemaToSingleNodes(schema); - const rootNode = nodes[0]; - - rootNode.childOptions = { - parentUid: targetUid, - type: lodash.get(schema, 'x-node-type', 'properties'), - position, - }; - - const insertedNodes = await this.insertNodes(nodes, options); - - return await this.getJsonSchema(insertedNodes[0].get('x-uid'), { - transaction, - }); - } - - private async schemaExists(schema: any, options?: Transactionable): Promise { - if (lodash.isObject(schema) && !schema['x-uid']) { - return false; - } - - const { transaction } = options; - const result = await this.database.sequelize.query( - this.sqlAdapter(`select "x-uid" from ${this.uiSchemasTableName} where "x-uid" = :uid`), - { - type: 'SELECT', - replacements: { - uid: lodash.isString(schema) ? schema : schema['x-uid'], - }, - transaction, - }, - ); - - return result.length > 0; - } - @transaction() async insertAdjacent( position: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd', @@ -706,51 +442,6 @@ export class UiSchemaRepository extends Repository { return result; } - @transaction() - protected async insertAfterBegin(targetUid: string, schema: any, options?: InsertAdjacentOptions) { - return await this.insertInner(targetUid, schema, 'first', options); - } - - @transaction() - protected async insertBeforeEnd(targetUid: string, schema: any, options?: InsertAdjacentOptions) { - return await this.insertInner(targetUid, schema, 'last', options); - } - - @transaction() - protected async insertBeforeBegin(targetUid: string, schema: any, options?: InsertAdjacentOptions) { - return await this.insertBeside(targetUid, schema, 'before', options); - } - - @transaction() - protected async insertAfterEnd(targetUid: string, schema: any, options?: InsertAdjacentOptions) { - return await this.insertBeside(targetUid, schema, 'after', options); - } - - @transaction() - protected async insertNodes(nodes: SchemaNode[], options?: Transactionable) { - const { transaction } = options; - - const insertedNodes = []; - - for (const node of nodes) { - insertedNodes.push( - await this.insertSingleNode(node, { - ...options, - transaction, - }), - ); - } - - return insertedNodes; - } - - private regenerateUid(s: any) { - s['x-uid'] = uid(); - Object.keys(s.properties || {}).forEach((key) => { - this.regenerateUid(s.properties[key]); - }); - } - @transaction() async duplicate(uid: string, options?: Transactionable) { const s = await this.getJsonSchema(uid, { ...options, includeAsyncNode: true }); @@ -842,39 +533,6 @@ export class UiSchemaRepository extends Repository { return result; } - private async insertSchemaRecord(name, uid, schema, transaction) { - const serverHooks = schema['x-server-hooks'] || []; - - const node = await this.create({ - values: { - name, - ['x-uid']: uid, - schema, - serverHooks, - }, - transaction, - context: { - disableInsertHook: true, - }, - }); - - return node; - } - - private prepareSingleNodeForInsert(schema: SchemaNode) { - const uid = schema['x-uid']; - const name = schema['name']; - const async = lodash.get(schema, 'x-async', false); - const childOptions = schema['childOptions']; - - delete schema['x-uid']; - delete schema['x-async']; - delete schema['name']; - delete schema['childOptions']; - - return { uid, name, async, childOptions }; - } - async insertSingleNode(schema: SchemaNode, options: Transactionable & removeParentOptions) { const { transaction } = options; @@ -1001,7 +659,7 @@ export class UiSchemaRepository extends Repository { AND TreeTable.depth = 1 AND TreeTable.ancestor = :ancestor and NodeInfo.type = :type`; // Compatible with mysql - if (this.database.sequelize.getDialect() === 'mysql') { + if (this.database.isMySQLCompatibleDialect()) { updateSql = `UPDATE ${treeTable} as TreeTable JOIN ${treeTable} as NodeInfo ON (NodeInfo.descendant = TreeTable.descendant and NodeInfo.depth = 0) SET TreeTable.sort = TreeTable.sort + 1 @@ -1074,7 +732,7 @@ export class UiSchemaRepository extends Repository { and TreeTable.sort >= :sort and NodeInfo.type = :type`; - if (this.database.sequelize.getDialect() === 'mysql') { + if (this.database.isMySQLCompatibleDialect()) { updateSql = `UPDATE ${treeTable} as TreeTable JOIN ${treeTable} as NodeInfo ON (NodeInfo.descendant = TreeTable.descendant and NodeInfo.depth = 0) SET TreeTable.sort = TreeTable.sort + 1 @@ -1136,6 +794,348 @@ WHERE TreeTable.depth = 1 AND TreeTable.ancestor = :ancestor and TreeTable.sort await this.clearXUidPathCache(uid, transaction); return savedNode; } + + protected async updateNode(uid: string, schema: any, transaction?: Transaction) { + const nodeModel = await this.findOne({ + filter: { + 'x-uid': uid, + }, + }); + + await nodeModel.update( + { + schema: { + ...(nodeModel.get('schema') as any), + ...lodash.omit(schema, ['x-async', 'name', 'x-uid', 'properties']), + }, + }, + { + hooks: false, + transaction, + }, + ); + + if (schema['x-server-hooks']) { + await this.database.emitAsync(`${this.collection.name}.afterSave`, nodeModel, { transaction }); + } + } + + protected async childrenCount(uid, transaction) { + const db = this.database; + + const countResult = await db.sequelize.query( + `SELECT COUNT(*) as count FROM ${this.uiSchemaTreePathTableName} where ancestor = :ancestor and depth = 1`, + { + replacements: { + ancestor: uid, + }, + type: 'SELECT', + transaction, + }, + ); + + return parseInt(countResult[0]['count']); + } + + protected async isLeafNode(uid, transaction) { + const childrenCount = await this.childrenCount(uid, transaction); + return childrenCount === 0; + } + + protected async findParentUid(uid, transaction?) { + const parent = await this.database.getRepository('uiSchemaTreePath').findOne({ + filter: { + descendant: uid, + depth: 1, + }, + transaction, + }); + + return parent ? (parent.get('ancestor') as string) : null; + } + + protected async findNodeSchemaWithParent(uid, transaction) { + const schema = await this.database.getRepository('uiSchemas').findOne({ + filter: { + 'x-uid': uid, + }, + transaction, + }); + + return { + parentUid: await this.findParentUid(uid, transaction), + schema, + }; + } + + protected async isSingleChild(uid, transaction) { + const db = this.database; + + const parent = await this.findParentUid(uid, transaction); + + if (!parent) { + return null; + } + + const parentChildrenCount = await this.childrenCount(parent, transaction); + + if (parentChildrenCount == 1) { + const schema = await db.getRepository('uiSchemas').findOne({ + filter: { + 'x-uid': parent, + }, + transaction, + }); + + return schema; + } + + return null; + } + + @transaction() + protected async insertBeside( + targetUid: string, + schema: any, + side: 'before' | 'after', + options?: InsertAdjacentOptions, + ) { + const { transaction } = options; + const targetParent = await this.findParentUid(targetUid, transaction); + + const db = this.database; + + const treeTable = this.uiSchemaTreePathTableName; + + const typeQuery = await db.sequelize.query(`SELECT type from ${treeTable} WHERE ancestor = :uid AND depth = 0;`, { + type: 'SELECT', + replacements: { + uid: targetUid, + }, + transaction, + }); + + const nodes = UiSchemaRepository.schemaToSingleNodes(schema); + + const rootNode = nodes[0]; + + rootNode.childOptions = { + parentUid: targetParent, + type: typeQuery[0]['type'], + position: { + type: side, + target: targetUid, + }, + }; + + const insertedNodes = await this.insertNodes(nodes, options); + return await this.getJsonSchema(insertedNodes[0].get('x-uid'), { + transaction, + }); + } + + @transaction() + protected async insertInner( + targetUid: string, + schema: any, + position: 'first' | 'last', + options?: InsertAdjacentOptions, + ) { + const { transaction } = options; + + const nodes = UiSchemaRepository.schemaToSingleNodes(schema); + const rootNode = nodes[0]; + + rootNode.childOptions = { + parentUid: targetUid, + type: lodash.get(schema, 'x-node-type', 'properties'), + position, + }; + + const insertedNodes = await this.insertNodes(nodes, options); + + return await this.getJsonSchema(insertedNodes[0].get('x-uid'), { + transaction, + }); + } + + @transaction() + protected async insertAfterBegin(targetUid: string, schema: any, options?: InsertAdjacentOptions) { + return await this.insertInner(targetUid, schema, 'first', options); + } + + @transaction() + protected async insertBeforeEnd(targetUid: string, schema: any, options?: InsertAdjacentOptions) { + return await this.insertInner(targetUid, schema, 'last', options); + } + + @transaction() + protected async insertBeforeBegin(targetUid: string, schema: any, options?: InsertAdjacentOptions) { + return await this.insertBeside(targetUid, schema, 'before', options); + } + + @transaction() + protected async insertAfterEnd(targetUid: string, schema: any, options?: InsertAdjacentOptions) { + return await this.insertBeside(targetUid, schema, 'after', options); + } + + @transaction() + protected async insertNodes(nodes: SchemaNode[], options?: Transactionable) { + const { transaction } = options; + + const insertedNodes = []; + + for (const node of nodes) { + insertedNodes.push( + await this.insertSingleNode(node, { + ...options, + transaction, + }), + ); + } + + return insertedNodes; + } + + private async doGetProperties(uid: string, options: GetPropertiesOptions = {}) { + const { transaction } = options; + + const db = this.database; + + const rawSql = ` + SELECT "SchemaTable"."x-uid" as "x-uid", "SchemaTable"."name" as "name", "SchemaTable"."schema" as "schema", + TreePath.depth as depth, + NodeInfo.type as type, NodeInfo.async as async, ParentPath.ancestor as parent, ParentPath.sort as sort + FROM ${this.uiSchemaTreePathTableName} as TreePath + LEFT JOIN ${this.uiSchemasTableName} as "SchemaTable" ON "SchemaTable"."x-uid" = TreePath.descendant + LEFT JOIN ${this.uiSchemaTreePathTableName} as NodeInfo ON NodeInfo.descendant = "SchemaTable"."x-uid" and NodeInfo.descendant = NodeInfo.ancestor and NodeInfo.depth = 0 + LEFT JOIN ${this.uiSchemaTreePathTableName} as ParentPath ON (ParentPath.descendant = "SchemaTable"."x-uid" AND ParentPath.depth = 1) + WHERE TreePath.ancestor = :ancestor AND (NodeInfo.async = false or TreePath.depth = 1)`; + + const nodes = await db.sequelize.query(this.sqlAdapter(rawSql), { + replacements: { + ancestor: uid, + }, + transaction, + }); + + if (nodes[0].length == 0) { + return {}; + } + + const schema = this.nodesToSchema(nodes[0], uid); + return lodash.pick(schema, ['type', 'properties']); + } + + private async doGetJsonSchema(uid: string, options?: GetJsonSchemaOptions) { + const db = this.database; + + const treeTable = this.uiSchemaTreePathTableName; + + const rawSql = ` + SELECT "SchemaTable"."x-uid" as "x-uid", "SchemaTable"."name" as name, "SchemaTable"."schema" as "schema" , + TreePath.depth as depth, + NodeInfo.type as type, NodeInfo.async as async, ParentPath.ancestor as parent, ParentPath.sort as sort + FROM ${treeTable} as TreePath + LEFT JOIN ${this.uiSchemasTableName} as "SchemaTable" ON "SchemaTable"."x-uid" = TreePath.descendant + LEFT JOIN ${treeTable} as NodeInfo ON NodeInfo.descendant = "SchemaTable"."x-uid" and NodeInfo.descendant = NodeInfo.ancestor and NodeInfo.depth = 0 + LEFT JOIN ${treeTable} as ParentPath ON (ParentPath.descendant = "SchemaTable"."x-uid" AND ParentPath.depth = 1) + WHERE TreePath.ancestor = :ancestor ${options?.includeAsyncNode ? '' : 'AND (NodeInfo.async != true )'} + `; + + const nodes = await db.sequelize.query(this.sqlAdapter(rawSql), { + replacements: { + ancestor: uid, + }, + transaction: options?.transaction, + }); + + if (nodes[0].length == 0) { + return {}; + } + + return this.nodesToSchema(nodes[0], uid); + } + + private ignoreSchemaProperties(schemaProperties) { + return lodash.omit(schemaProperties, nodeKeys); + } + + private breakOnMatched(schemaInstance, breakRemoveOn: BreakRemoveOnType): boolean { + if (!breakRemoveOn) { + return false; + } + + for (const key of Object.keys(breakRemoveOn)) { + const instanceValue = schemaInstance.get(key); + const breakRemoveOnValue = breakRemoveOn[key]; + if (instanceValue !== breakRemoveOnValue) { + return false; + } + } + + return true; + } + + private async schemaExists(schema: any, options?: Transactionable): Promise { + if (lodash.isObject(schema) && !schema['x-uid']) { + return false; + } + + const { transaction } = options; + const result = await this.database.sequelize.query( + this.sqlAdapter(`select "x-uid" from ${this.uiSchemasTableName} where "x-uid" = :uid`), + { + type: 'SELECT', + replacements: { + uid: lodash.isString(schema) ? schema : schema['x-uid'], + }, + transaction, + }, + ); + + return result.length > 0; + } + + private regenerateUid(s: any) { + s['x-uid'] = uid(); + Object.keys(s.properties || {}).forEach((key) => { + this.regenerateUid(s.properties[key]); + }); + } + + private async insertSchemaRecord(name, uid, schema, transaction) { + const serverHooks = schema['x-server-hooks'] || []; + + const node = await this.create({ + values: { + name, + ['x-uid']: uid, + schema, + serverHooks, + }, + transaction, + context: { + disableInsertHook: true, + }, + }); + + return node; + } + + private prepareSingleNodeForInsert(schema: SchemaNode) { + const uid = schema['x-uid']; + const name = schema['name']; + const async = lodash.get(schema, 'x-async', false); + const childOptions = schema['childOptions']; + + delete schema['x-uid']; + delete schema['x-async']; + delete schema['name']; + delete schema['childOptions']; + + return { uid, name, async, childOptions }; + } } export default UiSchemaRepository; diff --git a/yarn.lock b/yarn.lock index 657fc6f26..76740cdab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5805,6 +5805,11 @@ version "7946.0.10" resolved "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249" +"@types/geojson@^7946.0.8": + version "7946.0.13" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.13.tgz#e6e77ea9ecf36564980a861e24e62a095988775e" + integrity sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ== + "@types/glob-stream@*": version "8.0.0" resolved "https://registry.npmmirror.com/@types/glob-stream/-/glob-stream-8.0.0.tgz#ffa679e43d896de883ffac408a32a78ca123db33" @@ -6073,7 +6078,7 @@ version "14.18.53" resolved "https://registry.npmmirror.com/@types/node/-/node-14.18.53.tgz#42855629b8773535ab868238718745bf56c56219" -"@types/node@^17.0.5": +"@types/node@^17.0.10", "@types/node@^17.0.5": version "17.0.45" resolved "https://registry.npmmirror.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" @@ -15945,6 +15950,11 @@ long@^4.0.0: version "4.0.0" resolved "https://registry.npmmirror.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" +long@^5.2.0: + version "5.2.3" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== + longest-streak@^3.0.0: version "3.1.0" resolved "https://registry.npmmirror.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" @@ -16144,6 +16154,19 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +mariadb@^2.5.6: + version "2.5.6" + resolved "https://registry.yarnpkg.com/mariadb/-/mariadb-2.5.6.tgz#7314e9287cdba212831ebf16ef3b34dc6a1f0f06" + integrity sha512-zBx7loYY5GzLl8Y6AKxGXfY9DUYIIdGrmEORPOK9FEu0pg5ZLBKCGJuucHwKADxTBxKY7eM4rxndqxRcnMZKIw== + dependencies: + "@types/geojson" "^7946.0.8" + "@types/node" "^17.0.10" + denque "^2.0.1" + iconv-lite "^0.6.3" + long "^5.2.0" + moment-timezone "^0.5.34" + please-upgrade-node "^3.2.0" + markdown-it-highlightjs@3.3.1: version "3.3.1" resolved "https://registry.npmmirror.com/markdown-it-highlightjs/-/markdown-it-highlightjs-3.3.1.tgz#38403610487292b8a1ae2d1acc7bb66e4ede6be8" @@ -17028,7 +17051,7 @@ module-details-from-path@^1.0.3: version "1.0.3" resolved "https://registry.npmmirror.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" -moment-timezone@^0.5.40, moment-timezone@^0.5.43: +moment-timezone@^0.5.34, moment-timezone@^0.5.40, moment-timezone@^0.5.43: version "0.5.43" resolved "https://registry.npmmirror.com/moment-timezone/-/moment-timezone-0.5.43.tgz#3dd7f3d0c67f78c23cd1906b9b2137a09b3c4790" dependencies: @@ -18653,6 +18676,13 @@ playwright-core@1.37.1: resolved "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.37.1.tgz#cb517d52e2e8cb4fa71957639f1cd105d1683126" integrity sha512-17EuQxlSIYCmEMwzMqusJ2ztDgJePjrbttaefgdsiqeLWidjYz9BxXaTaZWxH1J95SHGk6tjE+dwgWILJoUZfA== +please-upgrade-node@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" + integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== + dependencies: + semver-compare "^1.0.0" + plugin-error@^1.0.1: version "1.0.1" resolved "https://registry.npmmirror.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c" @@ -20993,6 +21023,11 @@ select-hose@^2.0.0: version "2.0.0" resolved "https://registry.npmmirror.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== + semver-diff@^2.0.0: version "2.1.0" resolved "https://registry.npmmirror.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"