mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 21:49:25 +08:00
fix: update association with a non-primary key table (#5495)
* fix: update association with non primaryKey table * fix: test * fix: test * fix: get primary key attribute with multi filter target keys * fix: update has one associations * fix: test * fix: test * chore: test * chore: test * chore: test * chore: test * chore: middleware * chore: error condition * chore: test * fix: test
This commit is contained in:
parent
166681dfad
commit
a7f964988b
20
.github/workflows/nocobase-test-backend.yml
vendored
20
.github/workflows/nocobase-test-backend.yml
vendored
@ -39,8 +39,8 @@ jobs:
|
|||||||
sqlite-test:
|
sqlite-test:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node_version: ['20']
|
node_version: [ '20' ]
|
||||||
underscored: [true, false]
|
underscored: [ true, false ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: node:${{ matrix.node_version }}
|
container: node:${{ matrix.node_version }}
|
||||||
services:
|
services:
|
||||||
@ -70,10 +70,10 @@ jobs:
|
|||||||
postgres-test:
|
postgres-test:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node_version: ['20']
|
node_version: [ '20' ]
|
||||||
underscored: [true, false]
|
underscored: [ true, false ]
|
||||||
schema: [public, nocobase]
|
schema: [ public, nocobase ]
|
||||||
collection_schema: [public, user_schema]
|
collection_schema: [ public, user_schema ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: node:${{ matrix.node_version }}
|
container: node:${{ matrix.node_version }}
|
||||||
services:
|
services:
|
||||||
@ -129,8 +129,8 @@ jobs:
|
|||||||
mysql-test:
|
mysql-test:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node_version: ['20']
|
node_version: [ '20' ]
|
||||||
underscored: [true, false]
|
underscored: [ true, false ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: node:${{ matrix.node_version }}
|
container: node:${{ matrix.node_version }}
|
||||||
services:
|
services:
|
||||||
@ -175,8 +175,8 @@ jobs:
|
|||||||
mariadb-test:
|
mariadb-test:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node_version: ['20']
|
node_version: [ '20' ]
|
||||||
underscored: [true, false]
|
underscored: [ true, false ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: node:${{ matrix.node_version }}
|
container: node:${{ matrix.node_version }}
|
||||||
services:
|
services:
|
||||||
|
@ -346,6 +346,7 @@ describe('collection sync', () => {
|
|||||||
|
|
||||||
const model = collection.model;
|
const model = collection.model;
|
||||||
await collection.sync();
|
await collection.sync();
|
||||||
|
|
||||||
if (db.options.underscored) {
|
if (db.options.underscored) {
|
||||||
const tableFields = await (<any>model).queryInterface.describeTable(`${db.getTablePrefix()}posts_tags`);
|
const tableFields = await (<any>model).queryInterface.describeTable(`${db.getTablePrefix()}posts_tags`);
|
||||||
expect(tableFields['post_id']).toBeDefined();
|
expect(tableFields['post_id']).toBeDefined();
|
||||||
|
@ -75,18 +75,23 @@ describe('targetKey', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
await db.sync();
|
await db.sync();
|
||||||
|
|
||||||
const r1 = db.getRepository('a1');
|
const r1 = db.getRepository('a1');
|
||||||
const r2 = db.getRepository('b1');
|
const r2 = db.getRepository('b1');
|
||||||
|
|
||||||
const b1 = await r2.create({
|
const b1 = await r2.create({
|
||||||
values: {},
|
values: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
await r1.create({
|
await r1.create({
|
||||||
values: {
|
values: {
|
||||||
name: 'a1',
|
name: 'a1',
|
||||||
b1: [b1.toJSON()],
|
b1: [b1.toJSON()],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const b1r = await b1.reload();
|
const b1r = await b1.reload();
|
||||||
|
|
||||||
expect(b1r.a1Id).toBe(b1.id);
|
expect(b1r.a1Id).toBe(b1.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -21,6 +21,72 @@ describe('update associations', () => {
|
|||||||
await db.close();
|
await db.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should update associations with target key', async () => {
|
||||||
|
const T1 = db.collection({
|
||||||
|
name: 'test1',
|
||||||
|
autoGenId: false,
|
||||||
|
timestamps: false,
|
||||||
|
filterTargetKey: 'id_',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'id_',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 't2',
|
||||||
|
foreignKey: 'nvarchar2',
|
||||||
|
targetKey: 'varchar_',
|
||||||
|
sourceKey: 'id_',
|
||||||
|
target: 'test2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const T2 = db.collection({
|
||||||
|
name: 'test2',
|
||||||
|
autoGenId: false,
|
||||||
|
timestamps: false,
|
||||||
|
filterTargetKey: 'varchar_',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'varchar_',
|
||||||
|
type: 'string',
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nvarchar2',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const t2 = await T2.repository.create({
|
||||||
|
values: {
|
||||||
|
varchar_: '1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await T1.repository.create({
|
||||||
|
values: {
|
||||||
|
id_: 1,
|
||||||
|
t2: [
|
||||||
|
{
|
||||||
|
varchar_: '1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const t1 = await T1.repository.findOne({
|
||||||
|
appends: ['t2'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(t1['t2'][0]['varchar_']).toBe('1');
|
||||||
|
});
|
||||||
|
|
||||||
it('hasOne', async () => {
|
it('hasOne', async () => {
|
||||||
db.collection({
|
db.collection({
|
||||||
name: 'a',
|
name: 'a',
|
||||||
|
@ -126,6 +126,7 @@ export interface CollectionOptions extends Omit<ModelOptions, 'name' | 'hooks'>
|
|||||||
*/
|
*/
|
||||||
origin?: string;
|
origin?: string;
|
||||||
asStrategyResource?: boolean;
|
asStrategyResource?: boolean;
|
||||||
|
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,6 +156,7 @@ export class Collection<
|
|||||||
this.modelInit();
|
this.modelInit();
|
||||||
|
|
||||||
this.db.modelCollection.set(this.model, this);
|
this.db.modelCollection.set(this.model, this);
|
||||||
|
this.db.modelNameCollectionMap.set(this.model.name, this);
|
||||||
|
|
||||||
// set tableName to collection map
|
// set tableName to collection map
|
||||||
// the form of key is `${schema}.${tableName}` if schema exists
|
// the form of key is `${schema}.${tableName}` if schema exists
|
||||||
@ -259,8 +261,58 @@ export class Collection<
|
|||||||
M = model;
|
M = model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const collection = this;
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.model = class extends M {};
|
this.model = class extends M {};
|
||||||
|
|
||||||
|
Object.defineProperty(this.model, 'primaryKeyAttribute', {
|
||||||
|
get: function () {
|
||||||
|
const singleFilterTargetKey: string = (() => {
|
||||||
|
if (!collection.options.filterTargetKey) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(collection.options.filterTargetKey) && collection.options.filterTargetKey.length === 1) {
|
||||||
|
return collection.options.filterTargetKey[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return collection.options.filterTargetKey as string;
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (!this._primaryKeyAttribute && singleFilterTargetKey && collection.getField(singleFilterTargetKey)) {
|
||||||
|
return singleFilterTargetKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._primaryKeyAttribute;
|
||||||
|
}.bind(this.model),
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
this._primaryKeyAttribute = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(this.model, 'primaryKeyAttributes', {
|
||||||
|
get: function () {
|
||||||
|
if (Array.isArray(this._primaryKeyAttributes) && this._primaryKeyAttributes.length) {
|
||||||
|
return this._primaryKeyAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collection.options.filterTargetKey) {
|
||||||
|
const fields = lodash.castArray(collection.options.filterTargetKey);
|
||||||
|
if (fields.every((field) => collection.getField(field))) {
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._primaryKeyAttributes;
|
||||||
|
}.bind(this.model),
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
this._primaryKeyAttributes = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
this.model.init(null, this.sequelizeModelOptions());
|
this.model.init(null, this.sequelizeModelOptions());
|
||||||
|
|
||||||
this.model.options.modelName = this.options.name;
|
this.model.options.modelName = this.options.name;
|
||||||
@ -856,12 +908,15 @@ export class Collection<
|
|||||||
|
|
||||||
protected sequelizeModelOptions() {
|
protected sequelizeModelOptions() {
|
||||||
const { name } = this.options;
|
const { name } = this.options;
|
||||||
return {
|
|
||||||
|
const attr = {
|
||||||
..._.omit(this.options, ['name', 'fields', 'model', 'targetKey']),
|
..._.omit(this.options, ['name', 'fields', 'model', 'targetKey']),
|
||||||
modelName: name,
|
modelName: name,
|
||||||
sequelize: this.context.database.sequelize,
|
sequelize: this.context.database.sequelize,
|
||||||
tableName: this.tableName(),
|
tableName: this.tableName(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return attr;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected bindFieldEventListener() {
|
protected bindFieldEventListener() {
|
||||||
|
@ -144,6 +144,7 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
collections = new Map<string, Collection>();
|
collections = new Map<string, Collection>();
|
||||||
pendingFields = new Map<string, RelationField[]>();
|
pendingFields = new Map<string, RelationField[]>();
|
||||||
modelCollection = new Map<ModelStatic<any>, Collection>();
|
modelCollection = new Map<ModelStatic<any>, Collection>();
|
||||||
|
modelNameCollectionMap = new Map<string, Collection>();
|
||||||
tableNameCollectionMap = new Map<string, Collection>();
|
tableNameCollectionMap = new Map<string, Collection>();
|
||||||
context: any = {};
|
context: any = {};
|
||||||
queryInterface: QueryInterface;
|
queryInterface: QueryInterface;
|
||||||
@ -566,6 +567,10 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
return field;
|
return field;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCollectionByModelName(name: string) {
|
||||||
|
return this.modelNameCollectionMap.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get exists collection by its name
|
* get exists collection by its name
|
||||||
* @param name
|
* @param name
|
||||||
|
@ -18,8 +18,7 @@ interface ReferentialIntegrityCheckOptions extends Transactionable {
|
|||||||
export async function referentialIntegrityCheck(options: ReferentialIntegrityCheckOptions) {
|
export async function referentialIntegrityCheck(options: ReferentialIntegrityCheckOptions) {
|
||||||
const { referencedInstance, db, transaction } = options;
|
const { referencedInstance, db, transaction } = options;
|
||||||
|
|
||||||
// @ts-ignore
|
const collection = db.getCollectionByModelName(referencedInstance.constructor.name);
|
||||||
const collection = db.modelCollection.get(referencedInstance.constructor);
|
|
||||||
|
|
||||||
const collectionName = collection.name;
|
const collectionName = collection.name;
|
||||||
const references = db.referenceMap.getReferences(collectionName);
|
const references = db.referenceMap.getReferences(collectionName);
|
||||||
|
@ -18,10 +18,10 @@ import {
|
|||||||
ModelStatic,
|
ModelStatic,
|
||||||
Transactionable,
|
Transactionable,
|
||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
import Database from './database';
|
|
||||||
import { Model } from './model';
|
import { Model } from './model';
|
||||||
import { UpdateGuard } from './update-guard';
|
import { UpdateGuard } from './update-guard';
|
||||||
import { TargetKey } from './repository';
|
import { TargetKey } from './repository';
|
||||||
|
import Database from './database';
|
||||||
|
|
||||||
function isUndefinedOrNull(value: any) {
|
function isUndefinedOrNull(value: any) {
|
||||||
return typeof value === 'undefined' || value === null;
|
return typeof value === 'undefined' || value === null;
|
||||||
@ -449,7 +449,8 @@ export async function updateMultipleAssociation(
|
|||||||
} else if (item.sequelize) {
|
} else if (item.sequelize) {
|
||||||
setItems.push(item);
|
setItems.push(item);
|
||||||
} else if (typeof item === 'object') {
|
} else if (typeof item === 'object') {
|
||||||
const targetKey = (association as any).targetKey || 'id';
|
// @ts-ignore
|
||||||
|
const targetKey = (association as any).targetKey || association.options.targetKey || 'id';
|
||||||
|
|
||||||
if (item[targetKey]) {
|
if (item[targetKey]) {
|
||||||
const attributes = {
|
const attributes = {
|
||||||
@ -468,16 +469,19 @@ export async function updateMultipleAssociation(
|
|||||||
await model[setAccessor](setItems, { transaction, context, individualHooks: true });
|
await model[setAccessor](setItems, { transaction, context, individualHooks: true });
|
||||||
|
|
||||||
const newItems = [];
|
const newItems = [];
|
||||||
|
|
||||||
const pk = association.target.primaryKeyAttribute;
|
const pk = association.target.primaryKeyAttribute;
|
||||||
const tmpKey = association['options']?.['targetKey'];
|
|
||||||
let targetKey = pk;
|
let targetKey = pk;
|
||||||
const db = model.constructor['database'] as Database;
|
const db = model.constructor['database'] as Database;
|
||||||
|
|
||||||
|
const tmpKey = association['options']?.['targetKey'];
|
||||||
if (tmpKey !== pk) {
|
if (tmpKey !== pk) {
|
||||||
const targetKeyFieldOptions = db.getFieldByPath(`${association.target.name}.${tmpKey}`)?.options;
|
const targetKeyFieldOptions = db.getFieldByPath(`${association.target.name}.${tmpKey}`)?.options;
|
||||||
if (targetKeyFieldOptions?.unique) {
|
if (targetKeyFieldOptions?.unique) {
|
||||||
targetKey = tmpKey;
|
targetKey = tmpKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const item of objectItems) {
|
for (const item of objectItems) {
|
||||||
const through = (<any>association).through ? (<any>association).through.model.name : null;
|
const through = (<any>association).through ? (<any>association).through.model.name : null;
|
||||||
|
|
||||||
@ -550,7 +554,10 @@ export async function updateMultipleAssociation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const newItem of newItems) {
|
for (const newItem of newItems) {
|
||||||
const existIndexInSetItems = setItems.findIndex((setItem) => setItem[targetKey] === newItem[targetKey]);
|
// @ts-ignore
|
||||||
|
const findTargetKey = (association as any).targetKey || association.options.targetKey || targetKey;
|
||||||
|
|
||||||
|
const existIndexInSetItems = setItems.findIndex((setItem) => setItem[findTargetKey] === newItem[findTargetKey]);
|
||||||
|
|
||||||
if (existIndexInSetItems !== -1) {
|
if (existIndexInSetItems !== -1) {
|
||||||
setItems[existIndexInSetItems] = newItem;
|
setItems[existIndexInSetItems] = newItem;
|
||||||
|
@ -40,10 +40,13 @@ export function createResourcer(options: ApplicationOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function registerMiddlewares(app: Application, options: ApplicationOptions) {
|
export function registerMiddlewares(app: Application, options: ApplicationOptions) {
|
||||||
app.use(async (ctx, next) => {
|
app.use(
|
||||||
|
async function generateReqId(ctx, next) {
|
||||||
app.context.reqId = randomUUID();
|
app.context.reqId = randomUUID();
|
||||||
await next();
|
await next();
|
||||||
});
|
},
|
||||||
|
{ tag: 'generateReqId' },
|
||||||
|
);
|
||||||
|
|
||||||
app.use(requestLogger(app.name, app.requestLogger, options.logger?.request), { tag: 'logger' });
|
app.use(requestLogger(app.name, app.requestLogger, options.logger?.request), { tag: 'logger' });
|
||||||
|
|
||||||
@ -82,10 +85,10 @@ export function registerMiddlewares(app: Application, options: ApplicationOption
|
|||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(i18n, { tag: 'i18n', after: 'cors' });
|
app.use(i18n, { tag: 'i18n', before: 'cors' });
|
||||||
|
|
||||||
if (options.dataWrapping !== false) {
|
if (options.dataWrapping !== false) {
|
||||||
app.use(dataWrapping(), { tag: 'dataWrapping', after: 'i18n' });
|
app.use(dataWrapping(), { tag: 'dataWrapping', after: 'cors' });
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(app.dataSourceManager.middleware(), { tag: 'dataSource', after: 'dataWrapping' });
|
app.use(app.dataSourceManager.middleware(), { tag: 'dataSource', after: 'dataWrapping' });
|
||||||
|
@ -19,14 +19,17 @@ export async function i18n(ctx, next) {
|
|||||||
'en-US';
|
'en-US';
|
||||||
return lng;
|
return lng;
|
||||||
};
|
};
|
||||||
|
|
||||||
const lng = ctx.getCurrentLocale();
|
const lng = ctx.getCurrentLocale();
|
||||||
const localeManager = ctx.app.localeManager as Locale;
|
const localeManager = ctx.app.localeManager as Locale;
|
||||||
const i18n = await localeManager.getI18nInstance(lng);
|
const i18n = await localeManager.getI18nInstance(lng);
|
||||||
ctx.i18n = i18n;
|
ctx.i18n = i18n;
|
||||||
ctx.t = i18n.t.bind(i18n);
|
ctx.t = i18n.t.bind(i18n);
|
||||||
|
|
||||||
if (lng !== '*' && lng) {
|
if (lng !== '*' && lng) {
|
||||||
i18n.changeLanguage(lng);
|
await i18n.changeLanguage(lng);
|
||||||
await localeManager.loadResourcesByLang(lng);
|
await localeManager.loadResourcesByLang(lng);
|
||||||
}
|
}
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
}
|
}
|
||||||
|
@ -112,13 +112,7 @@ describe('create with exception', () => {
|
|||||||
|
|
||||||
expect(response.statusCode).toEqual(400);
|
expect(response.statusCode).toEqual(400);
|
||||||
|
|
||||||
expect(response.body).toEqual({
|
expect(response.body['errors'][0]['message']).toBe('name must be unique');
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
message: 'name must be unique',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render error with field title', async () => {
|
it('should render error with field title', async () => {
|
||||||
|
@ -65,6 +65,6 @@ export class PluginErrorHandlerServer extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
this.app.use(this.errorHandler.middleware(), { before: 'cors', tag: 'errorHandler' });
|
this.app.use(this.errorHandler.middleware(), { after: 'i18n', tag: 'errorHandler', before: 'cors' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user