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>
This commit is contained in:
ChengLei Shao 2023-11-20 08:54:40 +08:00 committed by GitHub
parent fbe3d2d9a5
commit d7d2eb634e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 740 additions and 514 deletions

View File

@ -173,3 +173,53 @@ jobs:
DB_TEST_DISTRIBUTOR_PORT: 23450 DB_TEST_DISTRIBUTOR_PORT: 23450
DB_TEST_PREFIX: test_ DB_TEST_PREFIX: test_
timeout-minutes: 40 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

View File

@ -70,6 +70,7 @@ class AppGenerator extends Generator {
if (allDbDialect) { if (allDbDialect) {
dependencies.push(`"mysql2": "^2.3.3"`); dependencies.push(`"mysql2": "^2.3.3"`);
dependencies.push(`"mariadb": "^2.5.6"`);
dependencies.push(`"pg": "^8.7.3"`); dependencies.push(`"pg": "^8.7.3"`);
dependencies.push(`"pg-hstore": "^2.3.4"`); dependencies.push(`"pg-hstore": "^2.3.4"`);
dependencies.push(`"sqlite3": "^5.0.8"`); dependencies.push(`"sqlite3": "^5.0.8"`);
@ -92,6 +93,16 @@ class AppGenerator extends Generator {
envs.push(`DB_USER=${env.DB_USER || ''}`); envs.push(`DB_USER=${env.DB_USER || ''}`);
envs.push(`DB_PASSWORD=${env.DB_PASSWORD || ''}`); envs.push(`DB_PASSWORD=${env.DB_PASSWORD || ''}`);
break; 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': case 'postgres':
if (!allDbDialect) { if (!allDbDialect) {
dependencies.push(`"pg": "^8.7.3"`); dependencies.push(`"pg": "^8.7.3"`);

View File

@ -27,7 +27,11 @@ excludeSqlite()('collection', () => {
await db.sync(); await db.sync();
const tableInfo = await db.sequelize.getQueryInterface().describeTable(collection.model.tableName); 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({ const profile = db.collection({
name: 'profiles', name: 'profiles',
@ -43,6 +47,10 @@ excludeSqlite()('collection', () => {
const profileTableInfo = await db.sequelize.getQueryInterface().describeTable(profile.model.tableName); 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');
}
}); });
}); });

View File

@ -6,6 +6,7 @@ describe('collection sortable options', () => {
beforeEach(async () => { beforeEach(async () => {
db = mockDatabase(); db = mockDatabase();
await db.clean({ drop: true });
}); });
afterEach(async () => { afterEach(async () => {

View File

@ -37,7 +37,7 @@ describe('collection', () => {
}); });
it('should not throw error when create empty collection in sqlite and mysql', async () => { 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; return;
} }

View File

@ -7,6 +7,7 @@ describe('hidden field options', () => {
beforeEach(async () => { beforeEach(async () => {
db = mockDatabase(); db = mockDatabase();
await db.clean({ drop: true });
}); });
afterEach(async () => { afterEach(async () => {

View File

@ -7,6 +7,7 @@ describe('context field', () => {
beforeEach(async () => { beforeEach(async () => {
db = mockDatabase(); db = mockDatabase();
await db.clean({ drop: true });
}); });
afterEach(async () => { afterEach(async () => {

View File

@ -1,6 +1,5 @@
import { Database } from '../../database'; import { Database } from '../../database';
import { mockDatabase } from '../'; import { mockDatabase } from '../';
import { makeWatchHost } from 'ts-loader/dist/servicesHost';
import { IdentifierError } from '../../errors/identifier-error'; import { IdentifierError } from '../../errors/identifier-error';
describe('has many field', () => { describe('has many field', () => {
@ -8,6 +7,7 @@ describe('has many field', () => {
beforeEach(async () => { beforeEach(async () => {
db = mockDatabase(); db = mockDatabase();
await db.clean({ drop: true });
}); });
afterEach(async () => { afterEach(async () => {

View File

@ -8,6 +8,7 @@ describe('string field', () => {
beforeEach(async () => { beforeEach(async () => {
db = mockDatabase(); db = mockDatabase();
await db.clean({ drop: true }); await db.clean({ drop: true });
db.registerFieldTypes({ db.registerFieldTypes({
sort: SortField, sort: SortField,
}); });

View File

@ -6,6 +6,7 @@ describe('string field', () => {
beforeEach(async () => { beforeEach(async () => {
db = mockDatabase(); db = mockDatabase();
await db.clean({ drop: true });
}); });
afterEach(async () => { afterEach(async () => {

View File

@ -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({ const count = await PostCollection.repository.count({
filter: { filter: {
'user.createdAt': { 'user.createdAt': {
$dateOn: user.get('createdAt'), $dateOn: `${year}-${month}-${date}`,
}, },
}, },
}); });

View File

@ -14,6 +14,8 @@ describe('array field operator', function () {
beforeEach(async () => { beforeEach(async () => {
db = mockDatabase({}); db = mockDatabase({});
await db.clean({ drop: true });
Test = db.collection({ Test = db.collection({
name: 'test', name: 'test',
fields: [ fields: [

View File

@ -7,6 +7,7 @@ describe('eq operator', () => {
beforeEach(async () => { beforeEach(async () => {
db = mockDatabase({}); db = mockDatabase({});
await db.clean({ drop: true });
Test = db.collection({ Test = db.collection({
name: 'tests', name: 'tests',

View File

@ -6,6 +6,7 @@ describe('ne operator', () => {
let Test; let Test;
beforeEach(async () => { beforeEach(async () => {
db = mockDatabase({}); db = mockDatabase({});
await db.clean({ drop: true });
Test = db.collection({ Test = db.collection({
name: 'tests', name: 'tests',

View File

@ -84,7 +84,7 @@ describe('option parser', () => {
}); });
test('with sort option', () => { test('with sort option', () => {
if (db.inDialect('mysql')) { if (db.inDialect('mysql', 'mariadb')) {
expect(1).toBe(1); expect(1).toBe(1);
return; return;
} }

View File

@ -81,6 +81,7 @@ describe('repository.find', () => {
beforeEach(async () => { beforeEach(async () => {
db = mockDatabase(); db = mockDatabase();
await db.clean({ drop: true });
User = db.collection({ User = db.collection({
name: 'users', name: 'users',
fields: [ fields: [
@ -386,6 +387,8 @@ describe('repository.update', () => {
beforeEach(async () => { beforeEach(async () => {
db = mockDatabase(); db = mockDatabase();
await db.clean({ drop: true });
User = db.collection({ User = db.collection({
name: 'users', name: 'users',
fields: [ fields: [
@ -608,7 +611,7 @@ describe('repository.update', () => {
filter: { filter: {
user: { user: {
id: { id: {
$eq: u1.id $eq: u1.id,
}, },
}, },
}, },
@ -623,9 +626,9 @@ describe('repository.update', () => {
const updated = await Post.repository.find({ const updated = await Post.repository.find({
filter: { filter: {
id: [p1.id, p2.id], 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']);
}); });
}); });

View File

@ -14,6 +14,8 @@ describe('count', () => {
beforeEach(async () => { beforeEach(async () => {
db = mockDatabase(); db = mockDatabase();
await db.clean({ drop: true });
User = db.collection({ User = db.collection({
name: 'users', name: 'users',
fields: [ fields: [

View File

@ -336,6 +336,7 @@ describe('repository find', () => {
beforeEach(async () => { beforeEach(async () => {
db = mockDatabase(); db = mockDatabase();
await db.clean({ drop: true });
User = db.collection<{ id: number; name: string }, { name: string }>({ User = db.collection<{ id: number; name: string }, { name: string }>({
name: 'users', name: 'users',
fields: [ fields: [

View File

@ -8,6 +8,8 @@ describe('update many', () => {
beforeEach(async () => { beforeEach(async () => {
db = mockDatabase(); db = mockDatabase();
await db.clean({ drop: true });
db.collection({ db.collection({
name: 't1', name: 't1',
fields: [{ type: 'string', name: 'title' }], fields: [{ type: 'string', name: 'title' }],

View File

@ -6,6 +6,7 @@ describe('sort', function () {
beforeEach(async () => { beforeEach(async () => {
db = mockDatabase(); db = mockDatabase();
await db.clean({ drop: true });
}); });
afterEach(async () => { afterEach(async () => {

View File

@ -11,7 +11,7 @@ describe('infer fields', () => {
db.collection({ db.collection({
name: 'users', name: 'users',
schema: 'public', schema: db.inDialect('postgres') ? 'public' : undefined,
fields: [ fields: [
{ name: 'id', type: 'bigInt', interface: 'id' }, { name: 'id', type: 'bigInt', interface: 'id' },
{ name: 'nickname', type: 'string', interface: 'input' }, { name: 'nickname', type: 'string', interface: 'input' },
@ -19,7 +19,7 @@ describe('infer fields', () => {
}); });
db.collection({ db.collection({
name: 'roles', name: 'roles',
schema: 'public', schema: db.inDialect('postgres') ? 'public' : undefined,
fields: [ fields: [
{ name: 'id', type: 'bigInt', interface: 'id' }, { name: 'id', type: 'bigInt', interface: 'id' },
{ name: 'title', type: 'string', interface: 'input' }, { name: 'title', type: 'string', interface: 'input' },
@ -28,7 +28,7 @@ describe('infer fields', () => {
}); });
db.collection({ db.collection({
name: 'roles_users', name: 'roles_users',
schema: 'public', schema: db.inDialect('postgres') ? 'public' : undefined,
fields: [ fields: [
{ name: 'id', type: 'bigInt', interface: 'id' }, { name: 'id', type: 'bigInt', interface: 'id' },
{ name: 'userId', type: 'bigInt', interface: 'id' }, { name: 'userId', type: 'bigInt', interface: 'id' },

View File

@ -359,6 +359,7 @@ describe('update associations', () => {
beforeEach(async () => { beforeEach(async () => {
db = mockDatabase(); db = mockDatabase();
await db.clean({ drop: true });
User = db.collection({ User = db.collection({
name: 'users', name: 'users',
fields: [ fields: [

View File

@ -93,6 +93,7 @@ describe.only('china region', () => {
beforeEach(async () => { beforeEach(async () => {
db = mockDatabase(); db = mockDatabase();
await db.clean({ drop: true });
db.collection({ db.collection({
name: 'users', name: 'users',
fields: [ fields: [

View File

@ -42,10 +42,6 @@ function EnsureAtomicity(target: any, propertyKey: string, descriptor: PropertyD
const afterRawAttributes = Object.keys(model.rawAttributes); const afterRawAttributes = Object.keys(model.rawAttributes);
const createdRawAttributes = lodash.difference(afterRawAttributes, beforeRawAttributes); const createdRawAttributes = lodash.difference(afterRawAttributes, beforeRawAttributes);
console.log({
beforeRawAttributes,
afterRawAttributes,
});
for (const key of createdRawAttributes) { for (const key of createdRawAttributes) {
delete this.model.rawAttributes[key]; delete this.model.rawAttributes[key];
} }

View File

@ -73,6 +73,8 @@ import { patchSequelizeQueryInterface, snakeCase } from './utils';
import { BaseValueParser, registerFieldValueParsers } from './value-parsers'; import { BaseValueParser, registerFieldValueParsers } from './value-parsers';
import { ViewCollection } from './view-collection'; import { ViewCollection } from './view-collection';
import { CollectionFactory } from './collection-factory'; import { CollectionFactory } from './collection-factory';
import chalk from 'chalk';
import { checkDatabaseVersion } from './helpers';
export type MergeOptions = merge.Options; export type MergeOptions = merge.Options;
@ -124,9 +126,13 @@ export const DialectVersionAccessors = {
mysql: { mysql: {
sql: 'select version() as version', sql: 'select version() as version',
get: (v: string) => { get: (v: string) => {
if (v.toLowerCase().includes('mariadb')) { const m = /([\d+.]+)/.exec(v);
return ''; return m[0];
} },
},
mariadb: {
sql: 'select version() as version',
get: (v: string) => {
const m = /([\d+.]+)/.exec(v); const m = /([\d+.]+)/.exec(v);
return m[0]; return m[0];
}, },
@ -155,7 +161,13 @@ class DatabaseVersion {
return false; return false;
} }
const [result] = (await this.db.sequelize.query(accessors[dialect].sql)) as any; 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; return false;
@ -354,6 +366,7 @@ export class Database extends EventEmitter implements AsyncEmitter {
await connection.query('SET search_path TO public;'); await connection.query('SET search_path TO public;');
}); });
} }
return options; return options;
} }
@ -466,6 +479,10 @@ export class Database extends EventEmitter implements AsyncEmitter {
return dialect.includes(this.sequelize.getDialect()); return dialect.includes(this.sequelize.getDialect());
} }
isMySQLCompatibleDialect() {
return this.inDialect('mysql', 'mariadb');
}
/** /**
* Add collection to database * Add collection to database
* @param options * @param options
@ -670,7 +687,7 @@ export class Database extends EventEmitter implements AsyncEmitter {
} }
async sync(options?: SyncOptions) { async sync(options?: SyncOptions) {
const isMySQL = this.sequelize.getDialect() === 'mysql'; const isMySQL = this.isMySQLCompatibleDialect();
if (isMySQL) { if (isMySQL) {
await this.sequelize.query('SET FOREIGN_KEY_CHECKS = 0', null); 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() { 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') { if (this.inDialect('postgres') && this.options.schema && this.options.schema != 'public') {
await this.sequelize.query(`CREATE SCHEMA IF NOT EXISTS "${this.options.schema}"`, null); await this.sequelize.query(`CREATE SCHEMA IF NOT EXISTS "${this.options.schema}"`, null);
} }

View File

@ -189,7 +189,7 @@ export abstract class Field {
sql = `SELECT * sql = `SELECT *
from pragma_table_info('${this.collection.model.tableName}') from pragma_table_info('${this.collection.model.tableName}')
WHERE name = '${this.columnName()}'`; WHERE name = '${this.columnName()}'`;
} else if (this.database.inDialect('mysql')) { } else if (this.database.inDialect('mysql', 'mariadb')) {
sql = ` sql = `
select column_name select column_name
from INFORMATION_SCHEMA.COLUMNS from INFORMATION_SCHEMA.COLUMNS

View File

@ -93,47 +93,60 @@ export class SortField extends Field {
const quotedOrderField = queryInterface.quoteIdentifier(orderField); 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 = ` let sql: string;
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);
return `WHERE ${queryInterface.quoteIdentifier(scopeKey)} IN (${scopeValue const whereClause =
.filter((v) => v !== null) scopeKey && scopeValue
.map((v) => `'${v}'`) ? `
.join(',')}) ${hasNull ? `OR ${queryInterface.quoteIdentifier(scopeKey)} IS NULL` : ''} `; WHERE ${queryInterface.quoteIdentifier(scopeKey)} IN (${scopeValue
} .filter((v) => v !== null)
.map((v) => `'${v}'`)
return ''; .join(', ')})${scopeValue.includes(null) ? ` OR ${queryInterface.quoteIdentifier(scopeKey)} IS NULL` : ''}`
})()} : '';
)
${
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};
`
}
`;
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, { await this.collection.db.sequelize.query(sql, {
transaction, transaction,
}); });

View File

@ -1,5 +1,6 @@
import { IDatabaseOptions } from './database'; import { Database, IDatabaseOptions } from './database';
import fs from 'fs'; import fs from 'fs';
import semver from 'semver';
function getEnvValue(key, defaultValue?) { function getEnvValue(key, defaultValue?) {
return process.env[key] || defaultValue; return process.env[key] || defaultValue;
@ -90,3 +91,56 @@ function customLogger(queryString, queryObject) {
console.log(queryObject.bind); 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;
}

View File

@ -62,7 +62,7 @@ export class Model<TModelAttributes extends {} = any, TCreationAttributes extend
// fix sequelize sync with model that not have any column // fix sequelize sync with model that not have any column
if (Object.keys(model.tableAttributes).length === 0) { if (Object.keys(model.tableAttributes).length === 0) {
if (this.database.inDialect('sqlite', 'mysql')) { if (this.database.inDialect('sqlite', 'mysql', 'mariadb')) {
console.error(`Zero-column tables aren't supported in ${this.database.sequelize.getDialect()}`); console.error(`Zero-column tables aren't supported in ${this.database.sequelize.getDialect()}`);
return; return;
} }

View File

@ -7,7 +7,7 @@ const isPg = (ctx) => {
}; };
const isMySQL = (ctx) => { const isMySQL = (ctx) => {
return getDialect(ctx) === 'mysql'; return getDialect(ctx) === 'mysql' || getDialect(ctx) === 'mariadb';
}; };
export { getDialect, isPg, isMySQL }; export { getDialect, isPg, isMySQL };

View File

@ -125,7 +125,7 @@ export class OptionsParser {
} }
sortField.push(direction); 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([Sequelize.fn('ISNULL', Sequelize.col(`${this.model.name}.${sortField[0]}`))]);
} }
orderParams.push(sortField); orderParams.push(sortField);

View File

@ -6,6 +6,7 @@ import SqliteQueryInterface from './sqlite-query-interface';
export default function buildQueryInterface(db: Database) { export default function buildQueryInterface(db: Database) {
const map = { const map = {
mysql: MysqlQueryInterface, mysql: MysqlQueryInterface,
mariadb: MysqlQueryInterface,
postgres: PostgresQueryInterface, postgres: PostgresQueryInterface,
sqlite: SqliteQueryInterface, sqlite: SqliteQueryInterface,
}; };

View File

@ -55,4 +55,4 @@ const sqlite = {
json: ['json', 'array'], json: ['json', 'array'],
}; };
export default { postgres, mysql, sqlite }; export default { postgres, mysql, sqlite, mariadb: mysql };

View File

@ -366,7 +366,7 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
} }
this._authenticated = true; this._authenticated = true;
await this.db.auth(); await this.db.auth();
await this.dbVersionCheck({ exit: true }); await this.db.checkVersion();
await this.db.prepare(); await this.db.prepare();
} }
@ -559,37 +559,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
this.logger.debug('finish destroy app'); 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() { async isInstalled() {
return ( return (
(await this.db.collectionExistsInDb('applicationVersion')) || (await this.db.collectionExistsInDb('collections')) (await this.db.collectionExistsInDb('applicationVersion')) || (await this.db.collectionExistsInDb('collections'))

View File

@ -12,6 +12,7 @@
"pg": "^8.7.3", "pg": "^8.7.3",
"pg-hstore": "^2.3.4", "pg-hstore": "^2.3.4",
"sqlite3": "^5.0.8", "sqlite3": "^5.0.8",
"mariadb": "^2.5.6",
"supertest": "^6.1.6", "supertest": "^6.1.6",
"ws": "^8.13.0" "ws": "^8.13.0"
}, },

View File

@ -4,12 +4,13 @@ import pg from 'pg';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import path from 'path'; import path from 'path';
import mysql from 'mysql2/promise'; import mysql from 'mysql2/promise';
import mariadb from 'mariadb';
dotenv.config({ path: path.resolve(process.cwd(), '.env.test') }); dotenv.config({ path: path.resolve(process.cwd(), '.env.test') });
abstract class BaseClient<Client> { abstract class BaseClient<Client> {
private createdDBs: Set<string> = new Set();
protected _client: Client | null = null; protected _client: Client | null = null;
private createdDBs: Set<string> = new Set();
abstract _createDB(name: string): Promise<void>; abstract _createDB(name: string): Promise<void>;
abstract _createConnection(): Promise<Client>; abstract _createConnection(): Promise<Client>;
@ -89,6 +90,28 @@ class MySQLClient extends BaseClient<any> {
} }
} }
class MariaDBClient extends BaseClient<any> {
async _removeDB(name: string): Promise<void> {
await this._client.query(`DROP DATABASE IF EXISTS ${name}`);
}
async _createDB(name: string): Promise<void> {
await this._client.query(`CREATE DATABASE IF NOT EXISTS ${name}`);
}
async _createConnection(): Promise<mariadb.Connection> {
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 = { const client = {
postgres: () => { postgres: () => {
return new PostgresClient(); return new PostgresClient();
@ -96,6 +119,9 @@ const client = {
mysql: () => { mysql: () => {
return new MySQLClient(); return new MySQLClient();
}, },
mariadb: () => {
return new MariaDBClient();
},
}; };
const dialect = process.env['DB_DIALECT']; const dialect = process.env['DB_DIALECT'];

View File

@ -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 { MockServer } from '@nocobase/test';
import { uid } from '@nocobase/utils'; import { uid } from '@nocobase/utils';
import { createApp } from '../index'; import { createApp } from '../index';
@ -56,10 +56,6 @@ SELECT * FROM numbers;
it('should support preview field with getter', async () => { it('should support preview field with getter', async () => {
class TestField extends Field { class TestField extends Field {
get dataType() {
return DataTypes.STRING;
}
constructor(options: any, context: any) { constructor(options: any, context: any) {
const { name } = options; const { name } = options;
super( super(
@ -72,6 +68,10 @@ SELECT * FROM numbers;
context, context,
); );
} }
get dataType() {
return DataTypes.STRING;
}
} }
db.registerFieldTypes({ db.registerFieldTypes({
@ -165,24 +165,18 @@ SELECT * FROM numbers;
expect(response.status).toBe(200); expect(response.status).toBe(200);
const data = response.body.data; const data = response.body.data;
if (app.db.options.dialect === 'mysql') { if (app.db.inDialect('mysql')) {
expect(data.fields.n.type).toBe('bigInt'); 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'); 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 () => { 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 jsonViewName = 'json_view';
const dropSql = `DROP VIEW IF EXISTS ${jsonViewName}`; const dropSql = `DROP VIEW IF EXISTS ${jsonViewName}`;
await app.db.sequelize.query(dropSql); await app.db.sequelize.query(dropSql);
@ -203,9 +197,11 @@ SELECT * FROM numbers;
expect(response.status).toBe(200); expect(response.status).toBe(200);
const data = response.body.data; const data = response.body.data;
if (!app.db.inDialect('sqlite')) { if (!app.db.inDialect('sqlite')) {
expect(data.fields.json_field.type).toBe('json'); expect(data.fields.json_field.type).toBe('json');
} }
expect(data.fields.json_field.possibleTypes).toBeTruthy(); expect(data.fields.json_field.possibleTypes).toBeTruthy();
}); });

View File

@ -8,11 +8,11 @@ export default class DropForeignKeysMigration extends Migration {
} }
const transaction = await this.db.sequelize.transaction(); const transaction = await this.db.sequelize.transaction();
try { try {
if (this.db.inDialect('mysql')) { if (this.db.isMySQLCompatibleDialect()) {
const [results]: any = await this.db.sequelize.query( const [results]: any = await this.db.sequelize.query(
` `
SELECT CONCAT('ALTER TABLE ',TABLE_SCHEMA,'.',TABLE_NAME,' DROP FOREIGN KEY ',CONSTRAINT_NAME,' ;') as q SELECT CONCAT('ALTER TABLE ',TABLE_SCHEMA,'.',TABLE_NAME,' DROP FOREIGN KEY ',CONSTRAINT_NAME,' ;') as q
FROM information_schema.TABLE_CONSTRAINTS c FROM information_schema.TABLE_CONSTRAINTS c
WHERE c.TABLE_SCHEMA='${this.db.options.database}' AND c.CONSTRAINT_TYPE='FOREIGN KEY'; WHERE c.TABLE_SCHEMA='${this.db.options.database}' AND c.CONSTRAINT_TYPE='FOREIGN KEY';
`, `,
{ transaction }, { transaction },

View File

@ -19,7 +19,7 @@ export default class UpdateIdToBigIntMigrator extends Migration {
}, },
}); });
if (!db.inDialect('mysql', 'postgres')) { if (!db.inDialect('mysql', 'mariadb', 'postgres')) {
return; return;
} }
@ -36,7 +36,7 @@ export default class UpdateIdToBigIntMigrator extends Migration {
if (model.rawAttributes[fieldName].type instanceof DataTypes.INTEGER) { if (model.rawAttributes[fieldName].type instanceof DataTypes.INTEGER) {
if (db.inDialect('postgres')) { if (db.inDialect('postgres')) {
sql = `ALTER TABLE "${tableName}" ALTER COLUMN "${fieldName}" SET DATA TYPE BIGINT;`; 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 dataTypeOrOptions = model.rawAttributes[fieldName];
const attributeName = fieldName; const attributeName = fieldName;

View File

@ -23,7 +23,7 @@ export default class UpdateIdToBigIntMigrator extends Migration {
}, },
}); });
if (!db.inDialect('mysql', 'postgres')) { if (!db.inDialect('mysql', 'mariadb', 'postgres')) {
return; return;
} }
@ -59,7 +59,7 @@ export default class UpdateIdToBigIntMigrator extends Migration {
if (model.rawAttributes[fieldName].type instanceof DataTypes.INTEGER) { if (model.rawAttributes[fieldName].type instanceof DataTypes.INTEGER) {
if (db.inDialect('postgres')) { if (db.inDialect('postgres')) {
sql = `ALTER TABLE ${quoteTableName} ALTER COLUMN "${columnName}" SET DATA TYPE BIGINT;`; 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 dataTypeOrOptions = model.rawAttributes[fieldName];
const attributeName = fieldName; const attributeName = fieldName;

View File

@ -90,6 +90,7 @@ export class CollectionRepository extends Repository {
if (lodash.isArray(skipField) && skipField.length) { if (lodash.isArray(skipField) && skipField.length) {
lazyCollectionFields[instanceName] = skipField; lazyCollectionFields[instanceName] = skipField;
} }
this.database.logger.debug(`load ${instanceName} collection`); this.database.logger.debug(`load ${instanceName} collection`);
this.app.setMaintainingMessage(`load ${instanceName} collection`); this.app.setMaintainingMessage(`load ${instanceName} collection`);

View File

@ -10,6 +10,7 @@ export const dateFormatFn = (sequelize: any, dialect: string, field: string, for
.replace(/ss/g, '%S'); .replace(/ss/g, '%S');
return sequelize.fn('strftime', format, sequelize.col(field)); return sequelize.fn('strftime', format, sequelize.col(field));
case 'mysql': case 'mysql':
case 'mariadb':
format = format format = format
.replace(/YYYY/g, '%Y') .replace(/YYYY/g, '%Y')
.replace(/MM/g, '%m') .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'); format = format.replace(/hh/g, 'HH24').replace(/mm/g, 'MI').replace(/ss/g, 'SS');
return sequelize.fn('to_char', sequelize.col(field), format); return sequelize.fn('to_char', sequelize.col(field), format);
default: default:
return field; return sequelize.col(field);
} }
}; };

View File

@ -6,7 +6,7 @@ import readline from 'readline';
export const DUMPED_EXTENSION = 'nbdump'; export const DUMPED_EXTENSION = 'nbdump';
export function sqlAdapter(database: Database, sql: string) { export function sqlAdapter(database: Database, sql: string) {
if (database.sequelize.getDialect() === 'mysql') { if (database.isMySQLCompatibleDialect()) {
return lodash.replace(sql, /"/g, '`'); return lodash.replace(sql, /"/g, '`');
} }

View File

@ -306,7 +306,7 @@ describe('multiple apps', () => {
}); });
await app.start(); await app.start();
await sleep(5000); await sleep(10000);
expect(AppSupervisor.getInstance().hasApp(subAppName)).toBeTruthy(); expect(AppSupervisor.getInstance().hasApp(subAppName)).toBeTruthy();
const appStatus = AppSupervisor.getInstance().getAppStatus(subAppName); const appStatus = AppSupervisor.getInstance().getAppStatus(subAppName);
expect(appStatus).toEqual('running'); expect(appStatus).toEqual('running');

View File

@ -65,6 +65,13 @@ const defaultDbCreator = async (app: Application) => {
await connection.close(); 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') { if (dialect === 'postgres') {
const { Client } = require('pg'); const { Client } = require('pg');

View File

@ -72,41 +72,6 @@ function transaction(transactionAbleArgPosition?: number) {
export class UiSchemaRepository extends Repository { export class UiSchemaRepository extends Repository {
cache: Cache; 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<void>}
*/
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() { get uiSchemasTableName() {
return this.tableNameAdapter(this.model.tableName); return this.tableNameAdapter(this.model.tableName);
} }
@ -116,14 +81,6 @@ export class UiSchemaRepository extends Repository {
return this.tableNameAdapter(model.tableName); 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[] { static schemaToSingleNodes(schema: any, carry: SchemaNode[] = [], childOptions: ChildOptions = null): SchemaNode[] {
const node = lodash.cloneDeep( const node = lodash.cloneDeep(
lodash.isString(schema) lodash.isString(schema)
@ -180,6 +137,49 @@ export class UiSchemaRepository extends Repository {
return carry; 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<void>}
*/
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 = {}) { async getProperties(uid: string, options: GetPropertiesOptions = {}) {
if (options?.readFromCache && this.cache) { if (options?.readFromCache && this.cache) {
return this.cache.wrap(`p_${uid}`, () => { return this.cache.wrap(`p_${uid}`, () => {
@ -189,66 +189,6 @@ export class UiSchemaRepository extends Repository {
return this.doGetProperties(uid, options); 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<any> { async getJsonSchema(uid: string, options?: GetJsonSchemaOptions): Promise<any> {
if (options?.readFromCache && this.cache) { if (options?.readFromCache && this.cache) {
return this.cache.wrap(`s_${uid}`, () => { return this.cache.wrap(`s_${uid}`, () => {
@ -258,10 +198,6 @@ export class UiSchemaRepository extends Repository {
return this.doGetJsonSchema(uid, options); return this.doGetJsonSchema(uid, options);
} }
private ignoreSchemaProperties(schemaProperties) {
return lodash.omit(schemaProperties, nodeKeys);
}
nodesToSchema(nodes, rootUid) { nodesToSchema(nodes, rootUid) {
const nodeAttributeSanitize = (node) => { const nodeAttributeSanitize = (node) => {
const schema = { 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 }) { async removeEmptyParents(options: Transactionable & { uid: string; breakRemoveOn?: BreakRemoveOnType }) {
const { transaction, uid, breakRemoveOn } = options; const { transaction, uid, breakRemoveOn } = options;
@ -490,22 +328,6 @@ export class UiSchemaRepository extends Repository {
await removeParent(uid); 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 }) { async recursivelyRemoveIfNoChildren(options: Transactionable & { uid: string; breakRemoveOn?: BreakRemoveOnType }) {
const { uid, transaction, breakRemoveOn } = options; 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<boolean> {
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() @transaction()
async insertAdjacent( async insertAdjacent(
position: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd', position: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd',
@ -706,51 +442,6 @@ export class UiSchemaRepository extends Repository {
return result; 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() @transaction()
async duplicate(uid: string, options?: Transactionable) { async duplicate(uid: string, options?: Transactionable) {
const s = await this.getJsonSchema(uid, { ...options, includeAsyncNode: true }); const s = await this.getJsonSchema(uid, { ...options, includeAsyncNode: true });
@ -842,39 +533,6 @@ export class UiSchemaRepository extends Repository {
return result; 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) { async insertSingleNode(schema: SchemaNode, options: Transactionable & removeParentOptions) {
const { transaction } = options; const { transaction } = options;
@ -1001,7 +659,7 @@ export class UiSchemaRepository extends Repository {
AND TreeTable.depth = 1 AND TreeTable.ancestor = :ancestor and NodeInfo.type = :type`; AND TreeTable.depth = 1 AND TreeTable.ancestor = :ancestor and NodeInfo.type = :type`;
// Compatible with mysql // Compatible with mysql
if (this.database.sequelize.getDialect() === 'mysql') { if (this.database.isMySQLCompatibleDialect()) {
updateSql = `UPDATE ${treeTable} as TreeTable updateSql = `UPDATE ${treeTable} as TreeTable
JOIN ${treeTable} as NodeInfo ON (NodeInfo.descendant = TreeTable.descendant and NodeInfo.depth = 0) JOIN ${treeTable} as NodeInfo ON (NodeInfo.descendant = TreeTable.descendant and NodeInfo.depth = 0)
SET TreeTable.sort = TreeTable.sort + 1 SET TreeTable.sort = TreeTable.sort + 1
@ -1074,7 +732,7 @@ export class UiSchemaRepository extends Repository {
and TreeTable.sort >= :sort and TreeTable.sort >= :sort
and NodeInfo.type = :type`; and NodeInfo.type = :type`;
if (this.database.sequelize.getDialect() === 'mysql') { if (this.database.isMySQLCompatibleDialect()) {
updateSql = `UPDATE ${treeTable} as TreeTable updateSql = `UPDATE ${treeTable} as TreeTable
JOIN ${treeTable} as NodeInfo ON (NodeInfo.descendant = TreeTable.descendant and NodeInfo.depth = 0) JOIN ${treeTable} as NodeInfo ON (NodeInfo.descendant = TreeTable.descendant and NodeInfo.depth = 0)
SET TreeTable.sort = TreeTable.sort + 1 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); await this.clearXUidPathCache(uid, transaction);
return savedNode; 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<boolean> {
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; export default UiSchemaRepository;

View File

@ -5805,6 +5805,11 @@
version "7946.0.10" version "7946.0.10"
resolved "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249" 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@*": "@types/glob-stream@*":
version "8.0.0" version "8.0.0"
resolved "https://registry.npmmirror.com/@types/glob-stream/-/glob-stream-8.0.0.tgz#ffa679e43d896de883ffac408a32a78ca123db33" resolved "https://registry.npmmirror.com/@types/glob-stream/-/glob-stream-8.0.0.tgz#ffa679e43d896de883ffac408a32a78ca123db33"
@ -6073,7 +6078,7 @@
version "14.18.53" version "14.18.53"
resolved "https://registry.npmmirror.com/@types/node/-/node-14.18.53.tgz#42855629b8773535ab868238718745bf56c56219" 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" version "17.0.45"
resolved "https://registry.npmmirror.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" 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" version "4.0.0"
resolved "https://registry.npmmirror.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" 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: longest-streak@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.npmmirror.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" resolved "https://registry.npmmirror.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4"
@ -16144,6 +16154,19 @@ map-visit@^1.0.0:
dependencies: dependencies:
object-visit "^1.0.0" 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: markdown-it-highlightjs@3.3.1:
version "3.3.1" version "3.3.1"
resolved "https://registry.npmmirror.com/markdown-it-highlightjs/-/markdown-it-highlightjs-3.3.1.tgz#38403610487292b8a1ae2d1acc7bb66e4ede6be8" 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" version "1.0.3"
resolved "https://registry.npmmirror.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" 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" version "0.5.43"
resolved "https://registry.npmmirror.com/moment-timezone/-/moment-timezone-0.5.43.tgz#3dd7f3d0c67f78c23cd1906b9b2137a09b3c4790" resolved "https://registry.npmmirror.com/moment-timezone/-/moment-timezone-0.5.43.tgz#3dd7f3d0c67f78c23cd1906b9b2137a09b3c4790"
dependencies: dependencies:
@ -18653,6 +18676,13 @@ playwright-core@1.37.1:
resolved "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.37.1.tgz#cb517d52e2e8cb4fa71957639f1cd105d1683126" resolved "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.37.1.tgz#cb517d52e2e8cb4fa71957639f1cd105d1683126"
integrity sha512-17EuQxlSIYCmEMwzMqusJ2ztDgJePjrbttaefgdsiqeLWidjYz9BxXaTaZWxH1J95SHGk6tjE+dwgWILJoUZfA== 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: plugin-error@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmmirror.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c" 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" version "2.0.0"
resolved "https://registry.npmmirror.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" 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: semver-diff@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.npmmirror.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" resolved "https://registry.npmmirror.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"