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:
parent
fbe3d2d9a5
commit
d7d2eb634e
50
.github/workflows/nocobase-test-backend.yml
vendored
50
.github/workflows/nocobase-test-backend.yml
vendored
@ -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
|
||||
|
@ -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"`);
|
||||
|
@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -6,6 +6,7 @@ describe('collection sortable options', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
await db.clean({ drop: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ describe('hidden field options', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
await db.clean({ drop: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
@ -7,6 +7,7 @@ describe('context field', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
await db.clean({ drop: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
@ -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 () => {
|
||||
|
@ -8,6 +8,7 @@ describe('string field', () => {
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
await db.clean({ drop: true });
|
||||
|
||||
db.registerFieldTypes({
|
||||
sort: SortField,
|
||||
});
|
||||
|
@ -6,6 +6,7 @@ describe('string field', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
await db.clean({ drop: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
@ -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}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -14,6 +14,8 @@ describe('array field operator', function () {
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase({});
|
||||
await db.clean({ drop: true });
|
||||
|
||||
Test = db.collection({
|
||||
name: 'test',
|
||||
fields: [
|
||||
|
@ -7,6 +7,7 @@ describe('eq operator', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase({});
|
||||
await db.clean({ drop: true });
|
||||
|
||||
Test = db.collection({
|
||||
name: 'tests',
|
||||
|
@ -6,6 +6,7 @@ describe('ne operator', () => {
|
||||
let Test;
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase({});
|
||||
await db.clean({ drop: true });
|
||||
|
||||
Test = db.collection({
|
||||
name: 'tests',
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -14,6 +14,8 @@ describe('count', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
await db.clean({ drop: true });
|
||||
|
||||
User = db.collection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
|
@ -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: [
|
||||
|
@ -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' }],
|
||||
|
@ -6,6 +6,7 @@ describe('sort', function () {
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
await db.clean({ drop: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
@ -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' },
|
||||
|
@ -359,6 +359,7 @@ describe('update associations', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
await db.clean({ drop: true });
|
||||
User = db.collection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
|
@ -93,6 +93,7 @@ describe.only('china region', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
await db.clean({ drop: true });
|
||||
db.collection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ export class Model<TModelAttributes extends {} = any, TCreationAttributes extend
|
||||
|
||||
// fix sequelize sync with model that not have any column
|
||||
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()}`);
|
||||
return;
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ const isPg = (ctx) => {
|
||||
};
|
||||
|
||||
const isMySQL = (ctx) => {
|
||||
return getDialect(ctx) === 'mysql';
|
||||
return getDialect(ctx) === 'mysql' || getDialect(ctx) === 'mariadb';
|
||||
};
|
||||
|
||||
export { getDialect, isPg, isMySQL };
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -55,4 +55,4 @@ const sqlite = {
|
||||
json: ['json', 'array'],
|
||||
};
|
||||
|
||||
export default { postgres, mysql, sqlite };
|
||||
export default { postgres, mysql, sqlite, mariadb: mysql };
|
||||
|
@ -366,7 +366,7 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> 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<StateT = DefaultState, ContextT = DefaultContext> 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'))
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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<Client> {
|
||||
private createdDBs: Set<string> = new Set();
|
||||
protected _client: Client | null = null;
|
||||
private createdDBs: Set<string> = new Set();
|
||||
|
||||
abstract _createDB(name: string): Promise<void>;
|
||||
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 = {
|
||||
postgres: () => {
|
||||
return new PostgresClient();
|
||||
@ -96,6 +119,9 @@ const client = {
|
||||
mysql: () => {
|
||||
return new MySQLClient();
|
||||
},
|
||||
mariadb: () => {
|
||||
return new MariaDBClient();
|
||||
},
|
||||
};
|
||||
|
||||
const dialect = process.env['DB_DIALECT'];
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
@ -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 },
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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`);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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, '`');
|
||||
}
|
||||
|
||||
|
@ -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');
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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<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() {
|
||||
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<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 = {}) {
|
||||
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<any> {
|
||||
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<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()
|
||||
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<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;
|
||||
|
39
yarn.lock
39
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"
|
||||
|
Loading…
Reference in New Issue
Block a user