node_modules ignore

This commit is contained in:
oier committed 2025-05-08 23:43:47 +02:00
1 parent e19d52f172
commit 4574544c9f
65041 files changed
+10593536

No files matched your search

@@ -0,0 +1,4 @@
import type { Database } from '..';
import type { Repository } from './types';
export declare const createRepository: (uid: string, db: Database) => Repository;
//# sourceMappingURL=entity-repository.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"entity-repository.d.ts","sourceRoot":"","sources":["../../src/entity-manager/entity-repository.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AACnC,OAAO,KAAK,EAAE,UAAU,EAAU,MAAM,SAAS,CAAC;AAkClD,eAAO,MAAM,gBAAgB,QAAS,MAAM,MAAM,QAAQ,KAAG,UA8H5D,CAAC"}
@@ -0,0 +1,139 @@
'use strict';
var _ = require('lodash/fp');
const withDefaultPagination = (params)=>{
const { page = 1, pageSize = 10, ...rest } = params;
return {
page: Number(page),
pageSize: Number(pageSize),
...rest
};
};
const withOffsetLimit = (params)=>{
const { page, pageSize, ...rest } = withDefaultPagination(params);
const offset = Math.max(page - 1, 0) * pageSize;
const limit = pageSize;
const query = {
...rest,
limit,
offset
};
return [
query,
{
page,
pageSize
}
];
};
const createRepository = (uid, db)=>{
return {
findOne (params = {}) {
return db.entityManager.findOne(uid, params);
},
findMany (params = {}) {
return db.entityManager.findMany(uid, params);
},
findWithCount (params = {}) {
return Promise.all([
db.entityManager.findMany(uid, params),
db.entityManager.count(uid, params)
]);
},
async findPage (params) {
const [query, { page, pageSize }] = withOffsetLimit(params);
const [results, total] = await Promise.all([
db.entityManager.findMany(uid, query),
db.entityManager.count(uid, query)
]);
return {
results,
pagination: {
page,
pageSize,
pageCount: Math.ceil(total / pageSize),
total
}
};
},
create (params) {
return db.entityManager.create(uid, params);
},
createMany (params) {
return db.entityManager.createMany(uid, params);
},
update (params) {
return db.entityManager.update(uid, params);
},
updateMany (params) {
return db.entityManager.updateMany(uid, params);
},
delete (params) {
return db.entityManager.delete(uid, params);
},
deleteMany (params = {}) {
return db.entityManager.deleteMany(uid, params);
},
count (params) {
return db.entityManager.count(uid, params);
},
attachRelations (id, data) {
return db.entityManager.attachRelations(uid, id, data);
},
async updateRelations (id, data) {
const trx = await db.transaction();
try {
await db.entityManager.updateRelations(uid, id, data, {
transaction: trx.get()
});
return await trx.commit();
} catch (e) {
await trx.rollback();
throw e;
}
},
deleteRelations (id) {
return db.entityManager.deleteRelations(uid, id);
},
populate (entity, populate) {
return db.entityManager.populate(uid, entity, populate);
},
load (entity, fields, params) {
return db.entityManager.load(uid, entity, fields, params);
},
async loadPages (entity, field, params) {
if (!_.isString(field)) {
throw new Error(`Invalid load. Expected ${field} to be a string`);
}
const { attributes } = db.metadata.get(uid);
const attribute = attributes[field];
if (!attribute || attribute.type !== 'relation' || !attribute.relation || ![
'oneToMany',
'manyToMany'
].includes(attribute.relation)) {
throw new Error(`Invalid load. Expected ${field} to be an anyToMany relational attribute`);
}
const [query, { page, pageSize }] = withOffsetLimit(params);
const [results, { count: total }] = await Promise.all([
db.entityManager.load(uid, entity, field, query),
db.entityManager.load(uid, entity, field, {
...query,
count: true
})
]);
return {
results,
pagination: {
page,
pageSize,
pageCount: Math.ceil(total / pageSize),
total
}
};
}
};
};
exports.createRepository = createRepository;
//# sourceMappingURL=entity-repository.js.map
@@ -0,0 +1 @@
{"version":3,"file":"entity-repository.js","sources":["../../src/entity-manager/entity-repository.ts"],"sourcesContent":["import { isString } from 'lodash/fp';\nimport type { Database } from '..';\nimport type { Repository, Params } from './types';\n\nconst withDefaultPagination = (params: Params) => {\n const { page = 1, pageSize = 10, ...rest } = params;\n\n return {\n page: Number(page),\n pageSize: Number(pageSize),\n ...rest,\n };\n};\n\ntype ParamsWithLimits = Omit<Params, 'page' | 'pageSize'> & {\n limit: number;\n offset: number;\n};\n\nconst withOffsetLimit = (\n params: Params\n): [ParamsWithLimits, { page: number; pageSize: number }] => {\n const { page, pageSize, ...rest } = withDefaultPagination(params);\n\n const offset = Math.max(page - 1, 0) * pageSize;\n const limit = pageSize;\n\n const query = {\n ...rest,\n limit,\n offset,\n };\n\n return [query, { page, pageSize }];\n};\n\nexport const createRepository = (uid: string, db: Database): Repository => {\n return {\n findOne(params = {}) {\n return db.entityManager.findOne(uid, params);\n },\n\n findMany(params = {}) {\n return db.entityManager.findMany(uid, params);\n },\n\n findWithCount(params = {}) {\n return Promise.all([\n db.entityManager.findMany(uid, params),\n db.entityManager.count(uid, params),\n ]);\n },\n\n async findPage(params) {\n const [query, { page, pageSize }] = withOffsetLimit(params);\n\n const [results, total] = await Promise.all([\n db.entityManager.findMany(uid, query),\n db.entityManager.count(uid, query),\n ]);\n\n return {\n results,\n pagination: {\n page,\n pageSize,\n pageCount: Math.ceil(total / pageSize),\n total,\n },\n };\n },\n\n create(params) {\n return db.entityManager.create(uid, params);\n },\n\n createMany(params) {\n return db.entityManager.createMany(uid, params);\n },\n\n update(params) {\n return db.entityManager.update(uid, params);\n },\n\n updateMany(params) {\n return db.entityManager.updateMany(uid, params);\n },\n\n delete(params) {\n return db.entityManager.delete(uid, params);\n },\n\n deleteMany(params = {}) {\n return db.entityManager.deleteMany(uid, params);\n },\n\n count(params) {\n return db.entityManager.count(uid, params);\n },\n\n attachRelations(id, data) {\n return db.entityManager.attachRelations(uid, id, data);\n },\n\n async updateRelations(id, data) {\n const trx = await db.transaction();\n try {\n await db.entityManager.updateRelations(uid, id, data, { transaction: trx.get() });\n return await trx.commit();\n } catch (e) {\n await trx.rollback();\n throw e;\n }\n },\n\n deleteRelations(id) {\n return db.entityManager.deleteRelations(uid, id);\n },\n\n populate(entity, populate) {\n return db.entityManager.populate(uid, entity, populate);\n },\n\n load(entity, fields, params) {\n return db.entityManager.load(uid, entity, fields, params);\n },\n\n async loadPages(entity, field, params) {\n if (!isString(field)) {\n throw new Error(`Invalid load. Expected ${field} to be a string`);\n }\n\n const { attributes } = db.metadata.get(uid);\n const attribute = attributes[field];\n\n if (\n !attribute ||\n attribute.type !== 'relation' ||\n !attribute.relation ||\n !['oneToMany', 'manyToMany'].includes(attribute.relation)\n ) {\n throw new Error(`Invalid load. Expected ${field} to be an anyToMany relational attribute`);\n }\n\n const [query, { page, pageSize }] = withOffsetLimit(params);\n\n const [results, { count: total }] = await Promise.all([\n db.entityManager.load(uid, entity, field, query),\n db.entityManager.load(uid, entity, field, { ...query, count: true }),\n ]);\n\n return {\n results,\n pagination: {\n page,\n pageSize,\n pageCount: Math.ceil(total / pageSize),\n total,\n },\n };\n },\n };\n};\n"],"names":["withDefaultPagination","params","page","pageSize","rest","Number","withOffsetLimit","offset","Math","max","limit","query","createRepository","uid","db","findOne","entityManager","findMany","findWithCount","Promise","all","count","findPage","results","total","pagination","pageCount","ceil","create","createMany","update","updateMany","delete","deleteMany","attachRelations","id","data","updateRelations","trx","transaction","get","commit","e","rollback","deleteRelations","populate","entity","load","fields","loadPages","field","isString","Error","attributes","metadata","attribute","type","relation","includes"],"mappings":";;;;AAIA,MAAMA,wBAAwB,CAACC,MAAAA,GAAAA;IAC7B,MAAM,EAAEC,OAAO,CAAC,EAAEC,WAAW,EAAE,EAAE,GAAGC,IAAAA,EAAM,GAAGH,MAAAA;IAE7Line truncated
@@ -0,0 +1,137 @@
import { isString } from 'lodash/fp';
const withDefaultPagination = (params)=>{
const { page = 1, pageSize = 10, ...rest } = params;
return {
page: Number(page),
pageSize: Number(pageSize),
...rest
};
};
const withOffsetLimit = (params)=>{
const { page, pageSize, ...rest } = withDefaultPagination(params);
const offset = Math.max(page - 1, 0) * pageSize;
const limit = pageSize;
const query = {
...rest,
limit,
offset
};
return [
query,
{
page,
pageSize
}
];
};
const createRepository = (uid, db)=>{
return {
findOne (params = {}) {
return db.entityManager.findOne(uid, params);
},
findMany (params = {}) {
return db.entityManager.findMany(uid, params);
},
findWithCount (params = {}) {
return Promise.all([
db.entityManager.findMany(uid, params),
db.entityManager.count(uid, params)
]);
},
async findPage (params) {
const [query, { page, pageSize }] = withOffsetLimit(params);
const [results, total] = await Promise.all([
db.entityManager.findMany(uid, query),
db.entityManager.count(uid, query)
]);
return {
results,
pagination: {
page,
pageSize,
pageCount: Math.ceil(total / pageSize),
total
}
};
},
create (params) {
return db.entityManager.create(uid, params);
},
createMany (params) {
return db.entityManager.createMany(uid, params);
},
update (params) {
return db.entityManager.update(uid, params);
},
updateMany (params) {
return db.entityManager.updateMany(uid, params);
},
delete (params) {
return db.entityManager.delete(uid, params);
},
deleteMany (params = {}) {
return db.entityManager.deleteMany(uid, params);
},
count (params) {
return db.entityManager.count(uid, params);
},
attachRelations (id, data) {
return db.entityManager.attachRelations(uid, id, data);
},
async updateRelations (id, data) {
const trx = await db.transaction();
try {
await db.entityManager.updateRelations(uid, id, data, {
transaction: trx.get()
});
return await trx.commit();
} catch (e) {
await trx.rollback();
throw e;
}
},
deleteRelations (id) {
return db.entityManager.deleteRelations(uid, id);
},
populate (entity, populate) {
return db.entityManager.populate(uid, entity, populate);
},
load (entity, fields, params) {
return db.entityManager.load(uid, entity, fields, params);
},
async loadPages (entity, field, params) {
if (!isString(field)) {
throw new Error(`Invalid load. Expected ${field} to be a string`);
}
const { attributes } = db.metadata.get(uid);
const attribute = attributes[field];
if (!attribute || attribute.type !== 'relation' || !attribute.relation || ![
'oneToMany',
'manyToMany'
].includes(attribute.relation)) {
throw new Error(`Invalid load. Expected ${field} to be an anyToMany relational attribute`);
}
const [query, { page, pageSize }] = withOffsetLimit(params);
const [results, { count: total }] = await Promise.all([
db.entityManager.load(uid, entity, field, query),
db.entityManager.load(uid, entity, field, {
...query,
count: true
})
]);
return {
results,
pagination: {
page,
pageSize,
pageCount: Math.ceil(total / pageSize),
total
}
};
}
};
};
export { createRepository };
//# sourceMappingURL=entity-repository.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"entity-repository.mjs","sources":["../../src/entity-manager/entity-repository.ts"],"sourcesContent":["import { isString } from 'lodash/fp';\nimport type { Database } from '..';\nimport type { Repository, Params } from './types';\n\nconst withDefaultPagination = (params: Params) => {\n const { page = 1, pageSize = 10, ...rest } = params;\n\n return {\n page: Number(page),\n pageSize: Number(pageSize),\n ...rest,\n };\n};\n\ntype ParamsWithLimits = Omit<Params, 'page' | 'pageSize'> & {\n limit: number;\n offset: number;\n};\n\nconst withOffsetLimit = (\n params: Params\n): [ParamsWithLimits, { page: number; pageSize: number }] => {\n const { page, pageSize, ...rest } = withDefaultPagination(params);\n\n const offset = Math.max(page - 1, 0) * pageSize;\n const limit = pageSize;\n\n const query = {\n ...rest,\n limit,\n offset,\n };\n\n return [query, { page, pageSize }];\n};\n\nexport const createRepository = (uid: string, db: Database): Repository => {\n return {\n findOne(params = {}) {\n return db.entityManager.findOne(uid, params);\n },\n\n findMany(params = {}) {\n return db.entityManager.findMany(uid, params);\n },\n\n findWithCount(params = {}) {\n return Promise.all([\n db.entityManager.findMany(uid, params),\n db.entityManager.count(uid, params),\n ]);\n },\n\n async findPage(params) {\n const [query, { page, pageSize }] = withOffsetLimit(params);\n\n const [results, total] = await Promise.all([\n db.entityManager.findMany(uid, query),\n db.entityManager.count(uid, query),\n ]);\n\n return {\n results,\n pagination: {\n page,\n pageSize,\n pageCount: Math.ceil(total / pageSize),\n total,\n },\n };\n },\n\n create(params) {\n return db.entityManager.create(uid, params);\n },\n\n createMany(params) {\n return db.entityManager.createMany(uid, params);\n },\n\n update(params) {\n return db.entityManager.update(uid, params);\n },\n\n updateMany(params) {\n return db.entityManager.updateMany(uid, params);\n },\n\n delete(params) {\n return db.entityManager.delete(uid, params);\n },\n\n deleteMany(params = {}) {\n return db.entityManager.deleteMany(uid, params);\n },\n\n count(params) {\n return db.entityManager.count(uid, params);\n },\n\n attachRelations(id, data) {\n return db.entityManager.attachRelations(uid, id, data);\n },\n\n async updateRelations(id, data) {\n const trx = await db.transaction();\n try {\n await db.entityManager.updateRelations(uid, id, data, { transaction: trx.get() });\n return await trx.commit();\n } catch (e) {\n await trx.rollback();\n throw e;\n }\n },\n\n deleteRelations(id) {\n return db.entityManager.deleteRelations(uid, id);\n },\n\n populate(entity, populate) {\n return db.entityManager.populate(uid, entity, populate);\n },\n\n load(entity, fields, params) {\n return db.entityManager.load(uid, entity, fields, params);\n },\n\n async loadPages(entity, field, params) {\n if (!isString(field)) {\n throw new Error(`Invalid load. Expected ${field} to be a string`);\n }\n\n const { attributes } = db.metadata.get(uid);\n const attribute = attributes[field];\n\n if (\n !attribute ||\n attribute.type !== 'relation' ||\n !attribute.relation ||\n !['oneToMany', 'manyToMany'].includes(attribute.relation)\n ) {\n throw new Error(`Invalid load. Expected ${field} to be an anyToMany relational attribute`);\n }\n\n const [query, { page, pageSize }] = withOffsetLimit(params);\n\n const [results, { count: total }] = await Promise.all([\n db.entityManager.load(uid, entity, field, query),\n db.entityManager.load(uid, entity, field, { ...query, count: true }),\n ]);\n\n return {\n results,\n pagination: {\n page,\n pageSize,\n pageCount: Math.ceil(total / pageSize),\n total,\n },\n };\n },\n };\n};\n"],"names":["withDefaultPagination","params","page","pageSize","rest","Number","withOffsetLimit","offset","Math","max","limit","query","createRepository","uid","db","findOne","entityManager","findMany","findWithCount","Promise","all","count","findPage","results","total","pagination","pageCount","ceil","create","createMany","update","updateMany","delete","deleteMany","attachRelations","id","data","updateRelations","trx","transaction","get","commit","e","rollback","deleteRelations","populate","entity","load","fields","loadPages","field","isString","Error","attributes","metadata","attribute","type","relation","includes"],"mappings":";;AAIA,MAAMA,wBAAwB,CAACC,MAAAA,GAAAA;IAC7B,MAAM,EAAEC,OAAO,CAAC,EAAEC,WAAW,EAAE,EAAE,GAAGC,IAAAA,EAAM,GAAGH,MAAAA;IAE7CLine truncated
+5
View File
@@ -0,0 +1,5 @@
import type { Database } from '..';
import { EntityManager } from './types';
export * from './types';
export declare const createEntityManager: (db: Database) => EntityManager;
//# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/entity-manager/index.ts"],"names":[],"mappings":"AA8CA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AAGnC,OAAO,EAAE,aAAa,EAAsB,MAAM,SAAS,CAAC;AAE5D,cAAc,SAAS,CAAC;AA2LxB,eAAO,MAAM,mBAAmB,OAAQ,QAAQ,KAAG,aAmzClD,CAAC"}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sources":["../../src/entity-manager/index.ts"],"sourcesContent":["import {\n castArray,\n compact,\n difference,\n differenceWith,\n has,\n isArray,\n isEmpty,\n isEqual,\n isInteger,\n isNil,\n isNull,\n isNumber,\n isObject,\n isPlainObject,\n isString,\n isUndefined,\n map,\n pick,\n uniqBy,\n uniqWith,\n} from 'lodash/fp';\n\nimport * as types from '../utils/types';\nimport { createField } from '../fields';\nimport { createQueryBuilder } from '../query';\nimport { createRepository } from './entity-repository';\nimport {\n deleteRelatedMorphOneRelationsAfterMorphToManyUpdate,\n encodePolymorphicRelation,\n encodePolymorphicId,\n} from './morph-relations';\nimport {\n isBidirectional,\n isAnyToOne,\n isOneToAny,\n hasOrderColumn,\n hasInverseOrderColumn,\n} from '../metadata';\nimport {\n deletePreviousOneToAnyRelations,\n deletePreviousAnyToOneRelations,\n deleteRelations,\n cleanOrderColumns,\n} from './regular-relations';\nimport { relationsOrderer } from './relations-orderer';\nimport type { Database } from '..';\nimport type { Meta } from '../metadata';\nimport type { ID } from '../types';\nimport { EntityManager, Repository, Entity } from './types';\n\nexport * from './types';\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n isObject(value) && !isNil(value);\n\nconst toId = (value: unknown | { id: unknown }): ID => {\n if (isRecord(value) && 'id' in value && isValidId(value.id)) {\n return value.id;\n }\n\n if (isValidId(value)) {\n return value;\n }\n\n throw new Error(`Invalid id, expected a string or integer, got ${JSON.stringify(value)}`);\n};\nconst toIds = (value: unknown): ID[] => castArray(value || []).map(toId);\n\nconst isValidId = (value: unknown): value is ID => isString(value) || isInteger(value);\n\nconst isValidObjectId = (value: unknown): value is Entity =>\n isRecord(value) && 'id' in value && isValidId(value.id);\n\nconst toIdArray = (\n data: unknown\n): {\n id: ID;\n __pivot?: { [key: string]: any };\n [key: string]: any;\n}[] => {\n const array = castArray(data)\n .filter((datum) => !isNil(datum))\n .map((datum) => {\n // if it is a string or an integer return an obj with id = to datum\n if (isValidId(datum)) {\n return { id: datum, __pivot: {} };\n }\n\n // if it is an object check it has at least a valid id\n if (!isValidObjectId(datum)) {\n throw new Error(`Invalid id, expected a string or integer, got ${datum}`);\n }\n\n return datum;\n });\n\n return uniqWith(isEqual, array);\n};\n\ntype ScalarAssoc = string | number | null;\ntype Assocs =\n | ScalarAssoc\n | { id: ScalarAssoc | Array<ScalarAssoc> }\n | Array<ScalarAssoc>\n | {\n set?: Array<ScalarAssoc> | null;\n options?: { strict?: boolean };\n connect?: Array<{\n id: ScalarAssoc;\n position?: { start?: boolean; end?: boolean; before?: ID; after?: ID };\n __pivot?: any;\n __type?: any;\n }> | null;\n disconnect?: Array<ScalarAssoc> | null;\n };\n\nconst toAssocs = (data: Assocs) => {\n if (\n isArray(data) ||\n isString(data) ||\n isNumber(data) ||\n isNull(data) ||\n (isRecord(data) && 'id' in data)\n ) {\n return {\n set: isNull(data) ? data : toIdArray(data),\n };\n }\n\n if (data?.set) {\n return {\n set: isNull(data.set) ? data.set : toIdArray(data.set),\n };\n }\n\n return {\n options: {\n strict: data?.options?.strict,\n },\n connect: toIdArray(data?.connect).map((elm) => ({\n id: elm.id,\n position: elm.position ? elm.position : { end: true },\n __pivot: elm.__pivot ?? {},\n __type: elm.__type,\n })),\n disconnect: toIdArray(data?.disconnect),\n };\n};\n\nconst processData = (\n metadata: Meta,\n data: Record<string, unknown> = {},\n { withDefaults = false } = {}\n) => {\n const { attributes } = metadata;\n\n const obj: Record<string, unknown> = {};\n\n for (const attributeName of Object.keys(attributes)) {\n const attribute = attributes[attributeName];\n\n if (types.isScalarAttribute(attribute)) {\n const field = createField(attribute);\n\n if (isUndefined(data[attributeName])) {\n if (!isUndefined(attribute.default) && withDefaults) {\n if (typeof attribute.default === 'function') {\n obj[attributeName] = attribute.default();\n } else {\n obj[attributeName] = attribute.default;\n }\n }\n continue;\n }\n\n if (\n 'validate' in field &&\n typeof field.validate === 'function' &&\n data[attributeName] !== null\n ) {\n field.validate(data[attributeName]);\n }\n\n const val = data[attributeName] === null ? null : field.toDB(data[attributeName]);\n\n obj[attributeName] = val;\n }\n\n if (types.isRelationalAttribute(attribute)) {\n // oneToOne & manyToOnLine truncated
File diff suppressed because it is too large. Load diff
@@ -0,0 +1 @@
{"version":3,"file":"index.mjs","sources":["../../src/entity-manager/index.ts"],"sourcesContent":["import {\n castArray,\n compact,\n difference,\n differenceWith,\n has,\n isArray,\n isEmpty,\n isEqual,\n isInteger,\n isNil,\n isNull,\n isNumber,\n isObject,\n isPlainObject,\n isString,\n isUndefined,\n map,\n pick,\n uniqBy,\n uniqWith,\n} from 'lodash/fp';\n\nimport * as types from '../utils/types';\nimport { createField } from '../fields';\nimport { createQueryBuilder } from '../query';\nimport { createRepository } from './entity-repository';\nimport {\n deleteRelatedMorphOneRelationsAfterMorphToManyUpdate,\n encodePolymorphicRelation,\n encodePolymorphicId,\n} from './morph-relations';\nimport {\n isBidirectional,\n isAnyToOne,\n isOneToAny,\n hasOrderColumn,\n hasInverseOrderColumn,\n} from '../metadata';\nimport {\n deletePreviousOneToAnyRelations,\n deletePreviousAnyToOneRelations,\n deleteRelations,\n cleanOrderColumns,\n} from './regular-relations';\nimport { relationsOrderer } from './relations-orderer';\nimport type { Database } from '..';\nimport type { Meta } from '../metadata';\nimport type { ID } from '../types';\nimport { EntityManager, Repository, Entity } from './types';\n\nexport * from './types';\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n isObject(value) && !isNil(value);\n\nconst toId = (value: unknown | { id: unknown }): ID => {\n if (isRecord(value) && 'id' in value && isValidId(value.id)) {\n return value.id;\n }\n\n if (isValidId(value)) {\n return value;\n }\n\n throw new Error(`Invalid id, expected a string or integer, got ${JSON.stringify(value)}`);\n};\nconst toIds = (value: unknown): ID[] => castArray(value || []).map(toId);\n\nconst isValidId = (value: unknown): value is ID => isString(value) || isInteger(value);\n\nconst isValidObjectId = (value: unknown): value is Entity =>\n isRecord(value) && 'id' in value && isValidId(value.id);\n\nconst toIdArray = (\n data: unknown\n): {\n id: ID;\n __pivot?: { [key: string]: any };\n [key: string]: any;\n}[] => {\n const array = castArray(data)\n .filter((datum) => !isNil(datum))\n .map((datum) => {\n // if it is a string or an integer return an obj with id = to datum\n if (isValidId(datum)) {\n return { id: datum, __pivot: {} };\n }\n\n // if it is an object check it has at least a valid id\n if (!isValidObjectId(datum)) {\n throw new Error(`Invalid id, expected a string or integer, got ${datum}`);\n }\n\n return datum;\n });\n\n return uniqWith(isEqual, array);\n};\n\ntype ScalarAssoc = string | number | null;\ntype Assocs =\n | ScalarAssoc\n | { id: ScalarAssoc | Array<ScalarAssoc> }\n | Array<ScalarAssoc>\n | {\n set?: Array<ScalarAssoc> | null;\n options?: { strict?: boolean };\n connect?: Array<{\n id: ScalarAssoc;\n position?: { start?: boolean; end?: boolean; before?: ID; after?: ID };\n __pivot?: any;\n __type?: any;\n }> | null;\n disconnect?: Array<ScalarAssoc> | null;\n };\n\nconst toAssocs = (data: Assocs) => {\n if (\n isArray(data) ||\n isString(data) ||\n isNumber(data) ||\n isNull(data) ||\n (isRecord(data) && 'id' in data)\n ) {\n return {\n set: isNull(data) ? data : toIdArray(data),\n };\n }\n\n if (data?.set) {\n return {\n set: isNull(data.set) ? data.set : toIdArray(data.set),\n };\n }\n\n return {\n options: {\n strict: data?.options?.strict,\n },\n connect: toIdArray(data?.connect).map((elm) => ({\n id: elm.id,\n position: elm.position ? elm.position : { end: true },\n __pivot: elm.__pivot ?? {},\n __type: elm.__type,\n })),\n disconnect: toIdArray(data?.disconnect),\n };\n};\n\nconst processData = (\n metadata: Meta,\n data: Record<string, unknown> = {},\n { withDefaults = false } = {}\n) => {\n const { attributes } = metadata;\n\n const obj: Record<string, unknown> = {};\n\n for (const attributeName of Object.keys(attributes)) {\n const attribute = attributes[attributeName];\n\n if (types.isScalarAttribute(attribute)) {\n const field = createField(attribute);\n\n if (isUndefined(data[attributeName])) {\n if (!isUndefined(attribute.default) && withDefaults) {\n if (typeof attribute.default === 'function') {\n obj[attributeName] = attribute.default();\n } else {\n obj[attributeName] = attribute.default;\n }\n }\n continue;\n }\n\n if (\n 'validate' in field &&\n typeof field.validate === 'function' &&\n data[attributeName] !== null\n ) {\n field.validate(data[attributeName]);\n }\n\n const val = data[attributeName] === null ? null : field.toDB(data[attributeName]);\n\n obj[attributeName] = val;\n }\n\n if (types.isRelationalAttribute(attribute)) {\n // oneToOne & manyToOLine truncated
@@ -0,0 +1,25 @@
/// <reference types="lodash" />
import type { Knex } from 'knex';
import type { Database } from '..';
import type { MorphJoinTable } from '../types';
type Rows = Record<string, unknown>[];
export declare const deleteRelatedMorphOneRelationsAfterMorphToManyUpdate: (rows: Rows, { uid, attributeName, joinTable, db, transaction: trx, }: {
uid: string;
attributeName: string;
joinTable: MorphJoinTable;
db: Database;
transaction?: Knex.Transaction;
}) => Promise<void>;
/**
* Encoding utilities for polymorphic relations.
*
* In some scenarios is useful to encode both the id & __type of the relation
* to have a unique identifier for the relation. (e.g. relations reordering)
*/
export declare const encodePolymorphicId: (id: number | string, __type: string) => string;
export declare const encodePolymorphicRelation: import("lodash").CurriedFunction2<{
idColumn: any;
typeColumn: any;
}, any, any>;
export {};
//# sourceMappingURL=morph-relations.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"morph-relations.d.ts","sourceRoot":"","sources":["../../src/entity-manager/morph-relations.ts"],"names":[],"mappings":";AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAGjC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AACnC,OAAO,KAAK,EAAE,cAAc,EAAY,MAAM,UAAU,CAAC;AAEzD,KAAK,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;AA8BtC,eAAO,MAAM,oDAAoD,yEAQ5D;IACD,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,cAAc,CAAC;IAC1B,EAAE,EAAE,QAAQ,CAAC;IACb,WAAW,CAAC,EAAE,KAAK,WAAW,CAAC;CAChC,kBAoCF,CAAC;AAEF;;;;;GAKG;AAEH,eAAO,MAAM,mBAAmB,OAAQ,MAAM,GAAG,MAAM,UAAU,MAAM,WAEtE,CAAC;AAEF,eAAO,MAAM,yBAAyB;;;YAkBpC,CAAC"}
@@ -0,0 +1,73 @@
'use strict';
var _ = require('lodash/fp');
var queryBuilder = require('../query/query-builder.js');
/* eslint-disable @typescript-eslint/naming-convention */ // allow __type
const getMorphToManyRowsLinkedToMorphOne = (rows, { uid, attributeName, typeColumn, db })=>rows.filter((row)=>{
const relatedType = row[typeColumn.name];
const field = row.field;
const targetAttribute = db.metadata.get(relatedType).attributes[field];
// ensure targeted field is the right one + check if it is a morphOne
return targetAttribute?.target === uid && targetAttribute?.morphBy === attributeName && targetAttribute?.relation === 'morphOne';
});
const deleteRelatedMorphOneRelationsAfterMorphToManyUpdate = async (rows, { uid, attributeName, joinTable, db, transaction: trx })=>{
const { morphColumn } = joinTable;
const { idColumn, typeColumn } = morphColumn;
const morphOneRows = getMorphToManyRowsLinkedToMorphOne(rows, {
uid,
attributeName,
typeColumn,
db
});
const groupByType = _.groupBy(typeColumn.name);
const groupByField = _.groupBy('field');
const typeAndFieldIdsGrouped = _.pipe(groupByType, _.mapValues(groupByField))(morphOneRows);
const orWhere = [];
for (const [type, v] of Object.entries(typeAndFieldIdsGrouped)){
for (const [field, arr] of Object.entries(v)){
orWhere.push({
[typeColumn.name]: type,
field,
[idColumn.name]: {
$in: _.map(idColumn.name, arr)
}
});
}
}
if (!_.isEmpty(orWhere)) {
await queryBuilder(joinTable.name, db).delete().where({
$or: orWhere
}).transacting(trx).execute();
}
};
/**
* Encoding utilities for polymorphic relations.
*
* In some scenarios is useful to encode both the id & __type of the relation
* to have a unique identifier for the relation. (e.g. relations reordering)
*/ const encodePolymorphicId = (id, __type)=>{
return `${id}:::${__type}`;
};
const encodePolymorphicRelation = _.curry(({ idColumn, typeColumn }, relation)=>{
// Encode the id of the relation and the positional argument if it exist
const newRelation = {
...relation,
[idColumn]: encodePolymorphicId(relation[idColumn], relation[typeColumn])
};
if (relation.position) {
const { before, after } = relation.position;
const __type = relation.position.__type || relation.__type;
newRelation.position = {
...relation.position
};
if (before) newRelation.position.before = encodePolymorphicId(before, __type);
if (after) newRelation.position.after = encodePolymorphicId(after, __type);
}
return newRelation;
});
exports.deleteRelatedMorphOneRelationsAfterMorphToManyUpdate = deleteRelatedMorphOneRelationsAfterMorphToManyUpdate;
exports.encodePolymorphicId = encodePolymorphicId;
exports.encodePolymorphicRelation = encodePolymorphicRelation;
//# sourceMappingURL=morph-relations.js.map
@@ -0,0 +1 @@
{"version":3,"file":"morph-relations.js","sources":["../../src/entity-manager/morph-relations.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/naming-convention */ // allow __type\nimport { curry, groupBy, pipe, mapValues, map, isEmpty } from 'lodash/fp';\nimport type { Knex } from 'knex';\n\nimport { createQueryBuilder } from '../query';\nimport type { Database } from '..';\nimport type { MorphJoinTable, Relation } from '../types';\n\ntype Rows = Record<string, unknown>[];\n\nconst getMorphToManyRowsLinkedToMorphOne = (\n rows: Rows,\n {\n uid,\n attributeName,\n typeColumn,\n db,\n }: {\n uid: string;\n attributeName: string;\n typeColumn: { name: string };\n db: Database;\n }\n) =>\n rows.filter((row) => {\n const relatedType = row[typeColumn.name] as string;\n const field = row.field as any;\n\n const targetAttribute = db.metadata.get(relatedType).attributes[field] as Relation.MorphOne;\n\n // ensure targeted field is the right one + check if it is a morphOne\n return (\n targetAttribute?.target === uid &&\n targetAttribute?.morphBy === attributeName &&\n targetAttribute?.relation === 'morphOne'\n );\n });\n\nexport const deleteRelatedMorphOneRelationsAfterMorphToManyUpdate = async (\n rows: Rows,\n {\n uid,\n attributeName,\n joinTable,\n db,\n transaction: trx,\n }: {\n uid: string;\n attributeName: string;\n joinTable: MorphJoinTable;\n db: Database;\n transaction?: Knex.Transaction;\n }\n) => {\n const { morphColumn } = joinTable;\n const { idColumn, typeColumn } = morphColumn;\n\n const morphOneRows = getMorphToManyRowsLinkedToMorphOne(rows, {\n uid,\n attributeName,\n typeColumn,\n db,\n });\n\n const groupByType = groupBy(typeColumn.name);\n const groupByField = groupBy('field');\n\n const typeAndFieldIdsGrouped = pipe(groupByType, mapValues(groupByField))(morphOneRows);\n\n const orWhere: object[] = [];\n\n for (const [type, v] of Object.entries(typeAndFieldIdsGrouped)) {\n for (const [field, arr] of Object.entries(v)) {\n orWhere.push({\n [typeColumn.name]: type,\n field,\n [idColumn.name]: { $in: map(idColumn.name, arr) },\n });\n }\n }\n\n if (!isEmpty(orWhere)) {\n await createQueryBuilder(joinTable.name, db)\n .delete()\n .where({ $or: orWhere })\n .transacting(trx)\n .execute();\n }\n};\n\n/**\n * Encoding utilities for polymorphic relations.\n *\n * In some scenarios is useful to encode both the id & __type of the relation\n * to have a unique identifier for the relation. (e.g. relations reordering)\n */\n\nexport const encodePolymorphicId = (id: number | string, __type: string) => {\n return `${id}:::${__type}`;\n};\n\nexport const encodePolymorphicRelation = curry(({ idColumn, typeColumn }, relation?: any): any => {\n // Encode the id of the relation and the positional argument if it exist\n const newRelation = {\n ...relation,\n [idColumn]: encodePolymorphicId(relation[idColumn], relation[typeColumn]),\n };\n\n if (relation.position) {\n const { before, after } = relation.position;\n const __type = relation.position.__type || relation.__type;\n\n newRelation.position = { ...relation.position };\n\n if (before) newRelation.position.before = encodePolymorphicId(before, __type);\n if (after) newRelation.position.after = encodePolymorphicId(after, __type);\n }\n\n return newRelation;\n});\n"],"names":["getMorphToManyRowsLinkedToMorphOne","rows","uid","attributeName","typeColumn","db","filter","row","relatedType","name","field","targetAttribute","metadata","get","attributes","target","morphBy","relation","deleteRelatedMorphOneRelationsAfterMorphToManyUpdate","joinTable","transaction","trx","morphColumn","idColumn","morphOneRows","groupByType","groupBy","groupByField","typeAndFieldIdsGrouped","pipe","mapValues","orWhere","type","v","Object","entries","arr","push","$in","map","isEmpty","createQueryBuilder","delete","where","$or","transacting","execute","encodePolymorphicId","id","__type","encodePolymorphicRelation","curry","newRelation","position","before","after"],"mappings":";;;;;AAAA;AAUA,MAAMA,qCAAqC,CACzCC,IAAAA,EACA,EACEC,GAAG,EACHC,aAAa,EACbC,UAAU,EACVC,EAAE,EAMH,GAEDJ,IAAKK,CAAAA,MAAM,CAAC,CAACC,GAAAA,GAAAA;AACX,QAAA,MAAMC,WAAcD,GAAAA,GAAG,CAACH,UAAAA,CAAWK,IAAI,CAAC;QACxC,MAAMC,KAAAA,GAAQH,IAAIG,KAAK;QAEvB,MAAMC,eAAAA,GAAkBN,GAAGO,QAAQ,CAACC,GAAG,CAACL,WAAAA,CAAAA,CAAaM,UAAU,CAACJ,KAAM,CAAA;;AAGtE,QAAA,OACEC,iBAAiBI,MAAWb,KAAAA,GAAAA,IAC5BS,iBAAiBK,OAAYb,KAAAA,aAAAA,IAC7BQ,iBAAiBM,QAAa,KAAA,UAAA;AAElC,KAAA,CAAA;AAEWC,MAAAA,oDAAAA,GAAuD,OAClEjB,IAAAA,EACA,EACEC,GAAG,EACHC,aAAa,EACbgB,SAAS,EACTd,EAAE,EACFe,WAAAA,EAAaC,GAAG,EAOjB,GAAA;IAED,MAAM,EAAEC,WAAW,EAAE,GAAGH,SAAAA;AACxB,IAAA,MAAM,EAAEI,QAAQ,EAAEnB,UAAU,EAAE,GAAGkB,WAAAA;IAEjC,MAAME,YAAAA,GAAexB,mCAAmCC,IAAM,EAAA;AAC5DC,QAAAA,GAAAA;AACAC,QAAAA,aAAAA;AACAC,QAAAA,UAAAA;AACAC,QAAAA;AACF,KAAA,CAAA;IAEA,MAAMoB,WAAAA,Line truncated
@@ -0,0 +1,69 @@
import { curry, groupBy, pipe, mapValues, map, isEmpty } from 'lodash/fp';
import createQueryBuilder from '../query/query-builder.mjs';
/* eslint-disable @typescript-eslint/naming-convention */ // allow __type
const getMorphToManyRowsLinkedToMorphOne = (rows, { uid, attributeName, typeColumn, db })=>rows.filter((row)=>{
const relatedType = row[typeColumn.name];
const field = row.field;
const targetAttribute = db.metadata.get(relatedType).attributes[field];
// ensure targeted field is the right one + check if it is a morphOne
return targetAttribute?.target === uid && targetAttribute?.morphBy === attributeName && targetAttribute?.relation === 'morphOne';
});
const deleteRelatedMorphOneRelationsAfterMorphToManyUpdate = async (rows, { uid, attributeName, joinTable, db, transaction: trx })=>{
const { morphColumn } = joinTable;
const { idColumn, typeColumn } = morphColumn;
const morphOneRows = getMorphToManyRowsLinkedToMorphOne(rows, {
uid,
attributeName,
typeColumn,
db
});
const groupByType = groupBy(typeColumn.name);
const groupByField = groupBy('field');
const typeAndFieldIdsGrouped = pipe(groupByType, mapValues(groupByField))(morphOneRows);
const orWhere = [];
for (const [type, v] of Object.entries(typeAndFieldIdsGrouped)){
for (const [field, arr] of Object.entries(v)){
orWhere.push({
[typeColumn.name]: type,
field,
[idColumn.name]: {
$in: map(idColumn.name, arr)
}
});
}
}
if (!isEmpty(orWhere)) {
await createQueryBuilder(joinTable.name, db).delete().where({
$or: orWhere
}).transacting(trx).execute();
}
};
/**
* Encoding utilities for polymorphic relations.
*
* In some scenarios is useful to encode both the id & __type of the relation
* to have a unique identifier for the relation. (e.g. relations reordering)
*/ const encodePolymorphicId = (id, __type)=>{
return `${id}:::${__type}`;
};
const encodePolymorphicRelation = curry(({ idColumn, typeColumn }, relation)=>{
// Encode the id of the relation and the positional argument if it exist
const newRelation = {
...relation,
[idColumn]: encodePolymorphicId(relation[idColumn], relation[typeColumn])
};
if (relation.position) {
const { before, after } = relation.position;
const __type = relation.position.__type || relation.__type;
newRelation.position = {
...relation.position
};
if (before) newRelation.position.before = encodePolymorphicId(before, __type);
if (after) newRelation.position.after = encodePolymorphicId(after, __type);
}
return newRelation;
});
export { deleteRelatedMorphOneRelationsAfterMorphToManyUpdate, encodePolymorphicId, encodePolymorphicRelation };
//# sourceMappingURL=morph-relations.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"morph-relations.mjs","sources":["../../src/entity-manager/morph-relations.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/naming-convention */ // allow __type\nimport { curry, groupBy, pipe, mapValues, map, isEmpty } from 'lodash/fp';\nimport type { Knex } from 'knex';\n\nimport { createQueryBuilder } from '../query';\nimport type { Database } from '..';\nimport type { MorphJoinTable, Relation } from '../types';\n\ntype Rows = Record<string, unknown>[];\n\nconst getMorphToManyRowsLinkedToMorphOne = (\n rows: Rows,\n {\n uid,\n attributeName,\n typeColumn,\n db,\n }: {\n uid: string;\n attributeName: string;\n typeColumn: { name: string };\n db: Database;\n }\n) =>\n rows.filter((row) => {\n const relatedType = row[typeColumn.name] as string;\n const field = row.field as any;\n\n const targetAttribute = db.metadata.get(relatedType).attributes[field] as Relation.MorphOne;\n\n // ensure targeted field is the right one + check if it is a morphOne\n return (\n targetAttribute?.target === uid &&\n targetAttribute?.morphBy === attributeName &&\n targetAttribute?.relation === 'morphOne'\n );\n });\n\nexport const deleteRelatedMorphOneRelationsAfterMorphToManyUpdate = async (\n rows: Rows,\n {\n uid,\n attributeName,\n joinTable,\n db,\n transaction: trx,\n }: {\n uid: string;\n attributeName: string;\n joinTable: MorphJoinTable;\n db: Database;\n transaction?: Knex.Transaction;\n }\n) => {\n const { morphColumn } = joinTable;\n const { idColumn, typeColumn } = morphColumn;\n\n const morphOneRows = getMorphToManyRowsLinkedToMorphOne(rows, {\n uid,\n attributeName,\n typeColumn,\n db,\n });\n\n const groupByType = groupBy(typeColumn.name);\n const groupByField = groupBy('field');\n\n const typeAndFieldIdsGrouped = pipe(groupByType, mapValues(groupByField))(morphOneRows);\n\n const orWhere: object[] = [];\n\n for (const [type, v] of Object.entries(typeAndFieldIdsGrouped)) {\n for (const [field, arr] of Object.entries(v)) {\n orWhere.push({\n [typeColumn.name]: type,\n field,\n [idColumn.name]: { $in: map(idColumn.name, arr) },\n });\n }\n }\n\n if (!isEmpty(orWhere)) {\n await createQueryBuilder(joinTable.name, db)\n .delete()\n .where({ $or: orWhere })\n .transacting(trx)\n .execute();\n }\n};\n\n/**\n * Encoding utilities for polymorphic relations.\n *\n * In some scenarios is useful to encode both the id & __type of the relation\n * to have a unique identifier for the relation. (e.g. relations reordering)\n */\n\nexport const encodePolymorphicId = (id: number | string, __type: string) => {\n return `${id}:::${__type}`;\n};\n\nexport const encodePolymorphicRelation = curry(({ idColumn, typeColumn }, relation?: any): any => {\n // Encode the id of the relation and the positional argument if it exist\n const newRelation = {\n ...relation,\n [idColumn]: encodePolymorphicId(relation[idColumn], relation[typeColumn]),\n };\n\n if (relation.position) {\n const { before, after } = relation.position;\n const __type = relation.position.__type || relation.__type;\n\n newRelation.position = { ...relation.position };\n\n if (before) newRelation.position.before = encodePolymorphicId(before, __type);\n if (after) newRelation.position.after = encodePolymorphicId(after, __type);\n }\n\n return newRelation;\n});\n"],"names":["getMorphToManyRowsLinkedToMorphOne","rows","uid","attributeName","typeColumn","db","filter","row","relatedType","name","field","targetAttribute","metadata","get","attributes","target","morphBy","relation","deleteRelatedMorphOneRelationsAfterMorphToManyUpdate","joinTable","transaction","trx","morphColumn","idColumn","morphOneRows","groupByType","groupBy","groupByField","typeAndFieldIdsGrouped","pipe","mapValues","orWhere","type","v","Object","entries","arr","push","$in","map","isEmpty","createQueryBuilder","delete","where","$or","transacting","execute","encodePolymorphicId","id","__type","encodePolymorphicRelation","curry","newRelation","position","before","after"],"mappings":";;;AAAA;AAUA,MAAMA,qCAAqC,CACzCC,IAAAA,EACA,EACEC,GAAG,EACHC,aAAa,EACbC,UAAU,EACVC,EAAE,EAMH,GAEDJ,IAAKK,CAAAA,MAAM,CAAC,CAACC,GAAAA,GAAAA;AACX,QAAA,MAAMC,WAAcD,GAAAA,GAAG,CAACH,UAAAA,CAAWK,IAAI,CAAC;QACxC,MAAMC,KAAAA,GAAQH,IAAIG,KAAK;QAEvB,MAAMC,eAAAA,GAAkBN,GAAGO,QAAQ,CAACC,GAAG,CAACL,WAAAA,CAAAA,CAAaM,UAAU,CAACJ,KAAM,CAAA;;AAGtE,QAAA,OACEC,iBAAiBI,MAAWb,KAAAA,GAAAA,IAC5BS,iBAAiBK,OAAYb,KAAAA,aAAAA,IAC7BQ,iBAAiBM,QAAa,KAAA,UAAA;AAElC,KAAA,CAAA;AAEWC,MAAAA,oDAAAA,GAAuD,OAClEjB,IAAAA,EACA,EACEC,GAAG,EACHC,aAAa,EACbgB,SAAS,EACTd,EAAE,EACFe,WAAAA,EAAaC,GAAG,EAOjB,GAAA;IAED,MAAM,EAAEC,WAAW,EAAE,GAAGH,SAAAA;AACxB,IAAA,MAAM,EAAEI,QAAQ,EAAEnB,UAAU,EAAE,GAAGkB,WAAAA;IAEjC,MAAME,YAAAA,GAAexB,mCAAmCC,IAAM,EAAA;AAC5DC,QAAAA,GAAAA;AACAC,QAAAA,aAAAA;AACAC,QAAAA,UAAAA;AACAC,QAAAA;AACF,KAAA,CAAA;IAEA,MAAMoB,WAAAA,GLine truncated
@@ -0,0 +1,58 @@
/// <reference types="node" />
/// <reference types="node" />
/// <reference types="node" />
/// <reference types="node" />
/// <reference types="node" />
import type { Knex } from 'knex';
import type { Database } from '..';
import type { ID, Relation } from '../types';
declare module 'knex' {
namespace Knex {
interface ChainableInterface {
transacting(trx?: Knex.Transaction): this;
}
}
}
/**
* If some relations currently exist for this oneToX relation, on the one side, this function removes them and update the inverse order if needed.
*/
declare const deletePreviousOneToAnyRelations: ({ id, attribute, relIdsToadd, db, transaction: trx, }: {
id: ID;
attribute: Relation.Bidirectional;
relIdsToadd: ID[];
db: Database;
transaction?: Knex.Transaction;
}) => Promise<void>;
/**
* If a relation currently exists for this xToOne relations, this function removes it and update the inverse order if needed.
*/
declare const deletePreviousAnyToOneRelations: ({ id, attribute, relIdToadd, db, transaction: trx, }: {
id: ID;
attribute: Relation.Bidirectional;
relIdToadd: ID;
db: Database;
transaction?: Knex.Transaction;
}) => Promise<void>;
/**
* Delete all or some relations of entity field
*/
declare const deleteRelations: ({ id, attribute, db, relIdsToNotDelete, relIdsToDelete, transaction: trx, }: {
id: ID;
attribute: Relation.Bidirectional;
db: Database;
relIdsToNotDelete?: ID[];
relIdsToDelete?: ID[] | 'all';
transaction?: Knex.Transaction;
}) => Promise<void>;
/**
* Clean the order columns by ensuring the order value are continuous (ex: 1, 2, 3 and not 1, 5, 10)
*/
declare const cleanOrderColumns: ({ id, attribute, db, inverseRelIds, transaction: trx, }: {
id?: ID;
attribute: Relation.Bidirectional;
db: Database;
inverseRelIds?: ID[];
transaction?: Knex.Transaction;
}) => Promise<[void, void] | undefined>;
export { deletePreviousOneToAnyRelations, deletePreviousAnyToOneRelations, deleteRelations, cleanOrderColumns, };
//# sourceMappingURL=regular-relations.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"regular-relations.d.ts","sourceRoot":"","sources":["../../src/entity-manager/regular-relations.ts"],"names":[],"mappings":";;;;;AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAYjC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AACnC,OAAO,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAS,MAAM,UAAU,CAAC;AAEpD,OAAO,QAAQ,MAAM,CAAC;IACpB,UAAU,IAAI,CAAC;QACb,UAAU,kBAAkB;YAC1B,WAAW,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;SAC3C;KACF;CACF;AAiCD;;GAEG;AACH,QAAA,MAAM,+BAA+B,0DAMlC;IACD,EAAE,EAAE,EAAE,CAAC;IACP,SAAS,EAAE,SAAS,aAAa,CAAC;IAClC,WAAW,EAAE,EAAE,EAAE,CAAC;IAClB,EAAE,EAAE,QAAQ,CAAC;IACb,WAAW,CAAC,EAAE,KAAK,WAAW,CAAC;CAChC,kBAsBA,CAAC;AAEF;;GAEG;AACH,QAAA,MAAM,+BAA+B,yDAMlC;IACD,EAAE,EAAE,EAAE,CAAC;IACP,SAAS,EAAE,SAAS,aAAa,CAAC;IAClC,UAAU,EAAE,EAAE,CAAC;IACf,EAAE,EAAE,QAAQ,CAAC;IACb,WAAW,CAAC,EAAE,KAAK,WAAW,CAAC;CAChC,kBAkDA,CAAC;AAEF;;GAEG;AACH,QAAA,MAAM,eAAe,gFAOlB;IACD,EAAE,EAAE,EAAE,CAAC;IACP,SAAS,EAAE,SAAS,aAAa,CAAC;IAClC,EAAE,EAAE,QAAQ,CAAC;IACb,iBAAiB,CAAC,EAAE,EAAE,EAAE,CAAC;IACzB,cAAc,CAAC,EAAE,EAAE,EAAE,GAAG,KAAK,CAAC;IAC9B,WAAW,CAAC,EAAE,KAAK,WAAW,CAAC;CAChC,kBAsDA,CAAC;AAEF;;GAEG;AACH,QAAA,MAAM,iBAAiB,4DAMpB;IACD,EAAE,CAAC,EAAE,EAAE,CAAC;IACR,SAAS,EAAE,SAAS,aAAa,CAAC;IAClC,EAAE,EAAE,QAAQ,CAAC;IACb,aAAa,CAAC,EAAE,EAAE,EAAE,CAAC;IACrB,WAAW,CAAC,EAAE,KAAK,WAAW,CAAC;CAChC,sCAiIA,CAAC;AAEF,OAAO,EACL,+BAA+B,EAC/B,+BAA+B,EAC/B,eAAe,EACf,iBAAiB,GAClB,CAAC"}
@@ -0,0 +1,247 @@
'use strict';
var _ = require('lodash/fp');
var relations = require('../metadata/relations.js');
require('../utils/identifiers/index.js');
var queryBuilder = require('../query/query-builder.js');
var knex = require('../utils/knex.js');
// TODO: This is a short term solution, to not steal relations from the same document.
const getDocumentSiblingIdsQuery = (tableName, id)=>{
// Find if the model is a content type or something else (e.g. component)
// to only get the documentId if it's a content type
const models = Array.from(strapi.db.metadata.values());
const isContentType = models.find((model)=>{
return model.tableName === tableName && model.attributes.documentId;
});
if (!isContentType) {
return [
id
];
}
// NOTE: SubQueries are wrapped in a function to not reuse the same connection,
// which causes infinite self references
return function(query) {
query.select('id').from(tableName)// Get all child ids of the document id
.whereIn('document_id', (documentIDSubQuery)=>{
documentIDSubQuery.from(tableName)// get document id related to the current id
.select('document_id').where('id', id);
});
};
};
/**
* If some relations currently exist for this oneToX relation, on the one side, this function removes them and update the inverse order if needed.
*/ const deletePreviousOneToAnyRelations = async ({ id, attribute, relIdsToadd, db, transaction: trx })=>{
if (!(relations.isBidirectional(attribute) && relations.isOneToAny(attribute))) {
throw new Error('deletePreviousOneToAnyRelations can only be called for bidirectional oneToAny relations');
}
const { joinTable } = attribute;
const { joinColumn, inverseJoinColumn } = joinTable;
const con = db.getConnection();
await con.delete().from(joinTable.name)// Exclude the ids of the current document
.whereNotIn(joinColumn.name, getDocumentSiblingIdsQuery(joinColumn.referencedTable, id))// Include all the ids that are being connected
.whereIn(inverseJoinColumn.name, relIdsToadd).where(joinTable.on || {}).transacting(trx);
await cleanOrderColumns({
attribute,
db,
inverseRelIds: relIdsToadd,
transaction: trx
});
};
/**
* If a relation currently exists for this xToOne relations, this function removes it and update the inverse order if needed.
*/ const deletePreviousAnyToOneRelations = async ({ id, attribute, relIdToadd, db, transaction: trx })=>{
const { joinTable } = attribute;
const { joinColumn, inverseJoinColumn } = joinTable;
const con = db.getConnection();
if (!relations.isAnyToOne(attribute)) {
throw new Error('deletePreviousAnyToOneRelations can only be called for anyToOne relations');
}
// handling manyToOne
if (relations.isManyToAny(attribute)) {
// if the database integrity was not broken relsToDelete is supposed to be of length 1
const relsToDelete = await con.select(inverseJoinColumn.name).from(joinTable.name).where(joinColumn.name, id).whereNotIn(inverseJoinColumn.name, getDocumentSiblingIdsQuery(inverseJoinColumn.referencedTable, relIdToadd)).where(joinTable.on || {}).transacting(trx);
const relIdsToDelete = _.map(inverseJoinColumn.name, relsToDelete);
await queryBuilder(joinTable.name, db).delete().where({
[joinColumn.name]: id,
[inverseJoinColumn.name]: {
$in: relIdsToDelete
}
}).where(joinTable.on || {}).transacting(trx).execute();
await cleanOrderColumns({
attribute,
db,
inverseRelIds: relIdsToDelete,
transaction: trx
});
// handling oneToOne
} else {
await con.delete().from(joinTable.name).where(joinColumn.name, id)// Exclude the ids of the current document
.whereNotIn(inverseJoinColumn.name, getDocumentSiblingIdsQuery(inverseJoinColumn.referencedTable, relIdToadd)).where(joinTable.on || {}).transacting(trx);
}
};
/**
* Delete all or some relations of entity field
*/ const deleteRelations = async ({ id, attribute, db, relIdsToNotDelete = [], relIdsToDelete = [], transaction: trx })=>{
const { joinTable } = attribute;
const { joinColumn, inverseJoinColumn } = joinTable;
const all = relIdsToDelete === 'all';
if (relations.hasOrderColumn(attribute) || relations.hasInverseOrderColumn(attribute)) {
let lastId = 0;
let done = false;
const batchSize = 100;
while(!done){
const batchToDelete = await queryBuilder(joinTable.name, db).select(inverseJoinColumn.name).where({
[joinColumn.name]: id,
id: {
$gt: lastId
},
[inverseJoinColumn.name]: {
$notIn: relIdsToNotDelete
},
...all ? {} : {
[inverseJoinColumn.name]: {
$in: relIdsToDelete
}
}
}).where(joinTable.on || {}).orderBy('id').limit(batchSize).transacting(trx).execute();
done = batchToDelete.length < batchSize;
lastId = batchToDelete[batchToDelete.length - 1]?.id || 0;
const batchIds = _.map(inverseJoinColumn.name, batchToDelete);
await queryBuilder(joinTable.name, db).delete().where({
[joinColumn.name]: id,
[inverseJoinColumn.name]: {
$in: batchIds
}
}).where(joinTable.on || {}).transacting(trx).execute();
await cleanOrderColumns({
attribute,
db,
id,
inverseRelIds: batchIds,
transaction: trx
});
}
} else {
await queryBuilder(joinTable.name, db).delete().where({
[joinColumn.name]: id,
[inverseJoinColumn.name]: {
$notIn: relIdsToNotDelete
},
...all ? {} : {
[inverseJoinColumn.name]: {
$in: relIdsToDelete
}
}
}).where(joinTable.on || {}).transacting(trx).execute();
}
};
/**
* Clean the order columns by ensuring the order value are continuous (ex: 1, 2, 3 and not 1, 5, 10)
*/ const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds = [], transaction: trx })=>{
if (!(relations.hasOrderColumn(attribute) && id) && !(relations.hasInverseOrderColumn(attribute) && !_.isEmpty(inverseRelIds))) {
return;
}
const { joinTable } = attribute;
const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable;
/**
UPDATE :joinTable: as a,
(
SELECT
id,
ROW_NUMBER() OVER ( PARTITION BY :joinColumn: ORDER BY :orderColumn:) AS src_order,
FROM :joinTable:
WHERE :joinColumn: = :id
) AS b
SET :orderColumn: = b.src_order
WHERE b.id = a.id;
*/ const updateOrderColumn = async ()=>{
if (!relations.hasOrderColumn(attribute) || !id) {
return;
}
const selectRowsToOrder = (joinTableName)=>db.connection(joinTableName).select('id').rowNumber('src_order', orderColumnName, joinColumn.name).where(joinColumn.name, id).toSQL();
switch(strapi.db.dialect.client){
case 'mysql':
{
// Here it's MariaDB and MySQL 8
const select = selectRowsToOrder(joinTable.name);
await db.getConnection().raw(`UPDATE ?? as a, ( ${select.sql} ) AS b
SET ?? = b.src_order
WHERE b.id = a.id`, [
joinTable.name,
...select.bindings,
orderColumnName
]).transacting(trx);
break;
}
default:
{
const joinTableName = knex.addSchema(db, joinTable.name);
const select = selectRowsToOrder(joinTableName);
// raw query as knex doesn't allow updating from a subquery
await db.connection.raw(`UPDATE ?? as a
SET ?? = b.src_order
FROM ( ${select.sql} ) AS b
WHERE b.id = a.id`, [
joinTableName,
orderColumnName,
...select.bindings
]).transacting(trx);
}
}
};
/**
UPDATE :joinTable: as a,
(
SELECT
id,
ROW_NUMBER() OVER ( PARTITION BY :inverseJoinColumn: ORDER BY :inverseOrderColumn:) AS inv_order
FROM :joinTable:
WHERE :inverseJoinColumn: IN (:inverseRelIds)
) AS b
SET :inverseOrderColumn: = b.inv_order
WHERE b.id = a.id;
*/ const updateInverseOrderColumn = async ()=>{
if (!relations.hasInverseOrderColumn(attribute) || _.isEmpty(inverseRelIds)) return;
const selectRowsToOrder = (joinTableName)=>db.connection(joinTableName).select('id').rowNumber('inv_order', inverseOrderColumnName, inverseJoinColumn.name).where(inverseJoinColumn.name, 'in', inverseRelIds).toSQL();
switch(strapi.db.dialect.client){
case 'mysql':
{
// Here it's MariaDB and MySQL 8
const select = selectRowsToOrder(joinTable.name);
await db.getConnection().raw(`UPDATE ?? as a, ( ${select.sql} ) AS b
SET ?? = b.inv_order
WHERE b.id = a.id`, [
joinTable.name,
...select.bindings,
inverseOrderColumnName
]).transacting(trx);
break;
}
default:
{
const joinTableName = knex.addSchema(db, joinTable.name);
const select = selectRowsToOrder(joinTableName);
// raw query as knex doesn't allow updating from a subquery
await db.connection.raw(`UPDATE ?? as a
SET ?? = b.inv_order
FROM ( ${select.sql} ) AS b
WHERE b.id = a.id`, [
joinTableName,
inverseOrderColumnName,
...select.bindings
]).transacting(trx);
}
}
};
return Promise.all([
updateOrderColumn(),
updateInverseOrderColumn()
]);
};
exports.cleanOrderColumns = cleanOrderColumns;
exports.deletePreviousAnyToOneRelations = deletePreviousAnyToOneRelations;
exports.deletePreviousOneToAnyRelations = deletePreviousOneToAnyRelations;
exports.deleteRelations = deleteRelations;
//# sourceMappingURL=regular-relations.js.map
@@ -0,0 +1 @@
{"version":3,"file":"regular-relations.js","sources":["../../src/entity-manager/regular-relations.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-namespace */\nimport { map, isEmpty } from 'lodash/fp';\nimport type { Knex } from 'knex';\n\nimport {\n isBidirectional,\n isOneToAny,\n isManyToAny,\n isAnyToOne,\n hasOrderColumn,\n hasInverseOrderColumn,\n} from '../metadata';\nimport { createQueryBuilder } from '../query';\nimport { addSchema } from '../utils/knex';\nimport type { Database } from '..';\nimport type { ID, Relation, Model } from '../types';\n\ndeclare module 'knex' {\n namespace Knex {\n interface ChainableInterface {\n transacting(trx?: Knex.Transaction): this;\n }\n }\n}\n\n// TODO: This is a short term solution, to not steal relations from the same document.\nconst getDocumentSiblingIdsQuery = (tableName: string, id: ID) => {\n // Find if the model is a content type or something else (e.g. component)\n // to only get the documentId if it's a content type\n const models: Model[] = Array.from(strapi.db.metadata.values());\n\n const isContentType = models.find((model) => {\n return model.tableName === tableName && model.attributes.documentId;\n });\n\n if (!isContentType) {\n return [id];\n }\n\n // NOTE: SubQueries are wrapped in a function to not reuse the same connection,\n // which causes infinite self references\n return function (query) {\n query\n .select('id')\n .from(tableName)\n // Get all child ids of the document id\n .whereIn('document_id', (documentIDSubQuery) => {\n documentIDSubQuery\n .from(tableName)\n // get document id related to the current id\n .select('document_id')\n .where('id', id);\n });\n } satisfies Knex.QueryCallback;\n};\n\n/**\n * If some relations currently exist for this oneToX relation, on the one side, this function removes them and update the inverse order if needed.\n */\nconst deletePreviousOneToAnyRelations = async ({\n id,\n attribute,\n relIdsToadd,\n db,\n transaction: trx,\n}: {\n id: ID;\n attribute: Relation.Bidirectional;\n relIdsToadd: ID[];\n db: Database;\n transaction?: Knex.Transaction;\n}) => {\n if (!(isBidirectional(attribute) && isOneToAny(attribute))) {\n throw new Error(\n 'deletePreviousOneToAnyRelations can only be called for bidirectional oneToAny relations'\n );\n }\n const { joinTable } = attribute;\n const { joinColumn, inverseJoinColumn } = joinTable;\n\n const con = db.getConnection();\n\n await con\n .delete()\n .from(joinTable.name)\n // Exclude the ids of the current document\n .whereNotIn(joinColumn.name, getDocumentSiblingIdsQuery(joinColumn.referencedTable!, id))\n // Include all the ids that are being connected\n .whereIn(inverseJoinColumn.name, relIdsToadd)\n .where(joinTable.on || {})\n .transacting(trx);\n\n await cleanOrderColumns({ attribute, db, inverseRelIds: relIdsToadd, transaction: trx });\n};\n\n/**\n * If a relation currently exists for this xToOne relations, this function removes it and update the inverse order if needed.\n */\nconst deletePreviousAnyToOneRelations = async ({\n id,\n attribute,\n relIdToadd,\n db,\n transaction: trx,\n}: {\n id: ID;\n attribute: Relation.Bidirectional;\n relIdToadd: ID;\n db: Database;\n transaction?: Knex.Transaction;\n}) => {\n const { joinTable } = attribute;\n const { joinColumn, inverseJoinColumn } = joinTable;\n const con = db.getConnection();\n\n if (!isAnyToOne(attribute)) {\n throw new Error('deletePreviousAnyToOneRelations can only be called for anyToOne relations');\n }\n // handling manyToOne\n if (isManyToAny(attribute)) {\n // if the database integrity was not broken relsToDelete is supposed to be of length 1\n const relsToDelete = await con\n .select(inverseJoinColumn.name)\n .from(joinTable.name)\n .where(joinColumn.name, id)\n .whereNotIn(\n inverseJoinColumn.name,\n getDocumentSiblingIdsQuery(inverseJoinColumn.referencedTable!, relIdToadd)\n )\n .where(joinTable.on || {})\n .transacting(trx);\n\n const relIdsToDelete = map(inverseJoinColumn.name, relsToDelete);\n\n await createQueryBuilder(joinTable.name, db)\n .delete()\n .where({\n [joinColumn.name]: id,\n [inverseJoinColumn.name]: { $in: relIdsToDelete },\n })\n .where(joinTable.on || {})\n .transacting(trx)\n .execute();\n\n await cleanOrderColumns({ attribute, db, inverseRelIds: relIdsToDelete, transaction: trx });\n\n // handling oneToOne\n } else {\n await con\n .delete()\n .from(joinTable.name)\n .where(joinColumn.name, id)\n // Exclude the ids of the current document\n .whereNotIn(\n inverseJoinColumn.name,\n getDocumentSiblingIdsQuery(inverseJoinColumn.referencedTable!, relIdToadd)\n )\n .where(joinTable.on || {})\n .transacting(trx);\n }\Line truncated
@@ -0,0 +1,242 @@
import { map, isEmpty } from 'lodash/fp';
import { isBidirectional, isOneToAny, isAnyToOne, isManyToAny, hasOrderColumn, hasInverseOrderColumn } from '../metadata/relations.mjs';
import '../utils/identifiers/index.mjs';
import createQueryBuilder from '../query/query-builder.mjs';
import { addSchema } from '../utils/knex.mjs';
// TODO: This is a short term solution, to not steal relations from the same document.
const getDocumentSiblingIdsQuery = (tableName, id)=>{
// Find if the model is a content type or something else (e.g. component)
// to only get the documentId if it's a content type
const models = Array.from(strapi.db.metadata.values());
const isContentType = models.find((model)=>{
return model.tableName === tableName && model.attributes.documentId;
});
if (!isContentType) {
return [
id
];
}
// NOTE: SubQueries are wrapped in a function to not reuse the same connection,
// which causes infinite self references
return function(query) {
query.select('id').from(tableName)// Get all child ids of the document id
.whereIn('document_id', (documentIDSubQuery)=>{
documentIDSubQuery.from(tableName)// get document id related to the current id
.select('document_id').where('id', id);
});
};
};
/**
* If some relations currently exist for this oneToX relation, on the one side, this function removes them and update the inverse order if needed.
*/ const deletePreviousOneToAnyRelations = async ({ id, attribute, relIdsToadd, db, transaction: trx })=>{
if (!(isBidirectional(attribute) && isOneToAny(attribute))) {
throw new Error('deletePreviousOneToAnyRelations can only be called for bidirectional oneToAny relations');
}
const { joinTable } = attribute;
const { joinColumn, inverseJoinColumn } = joinTable;
const con = db.getConnection();
await con.delete().from(joinTable.name)// Exclude the ids of the current document
.whereNotIn(joinColumn.name, getDocumentSiblingIdsQuery(joinColumn.referencedTable, id))// Include all the ids that are being connected
.whereIn(inverseJoinColumn.name, relIdsToadd).where(joinTable.on || {}).transacting(trx);
await cleanOrderColumns({
attribute,
db,
inverseRelIds: relIdsToadd,
transaction: trx
});
};
/**
* If a relation currently exists for this xToOne relations, this function removes it and update the inverse order if needed.
*/ const deletePreviousAnyToOneRelations = async ({ id, attribute, relIdToadd, db, transaction: trx })=>{
const { joinTable } = attribute;
const { joinColumn, inverseJoinColumn } = joinTable;
const con = db.getConnection();
if (!isAnyToOne(attribute)) {
throw new Error('deletePreviousAnyToOneRelations can only be called for anyToOne relations');
}
// handling manyToOne
if (isManyToAny(attribute)) {
// if the database integrity was not broken relsToDelete is supposed to be of length 1
const relsToDelete = await con.select(inverseJoinColumn.name).from(joinTable.name).where(joinColumn.name, id).whereNotIn(inverseJoinColumn.name, getDocumentSiblingIdsQuery(inverseJoinColumn.referencedTable, relIdToadd)).where(joinTable.on || {}).transacting(trx);
const relIdsToDelete = map(inverseJoinColumn.name, relsToDelete);
await createQueryBuilder(joinTable.name, db).delete().where({
[joinColumn.name]: id,
[inverseJoinColumn.name]: {
$in: relIdsToDelete
}
}).where(joinTable.on || {}).transacting(trx).execute();
await cleanOrderColumns({
attribute,
db,
inverseRelIds: relIdsToDelete,
transaction: trx
});
// handling oneToOne
} else {
await con.delete().from(joinTable.name).where(joinColumn.name, id)// Exclude the ids of the current document
.whereNotIn(inverseJoinColumn.name, getDocumentSiblingIdsQuery(inverseJoinColumn.referencedTable, relIdToadd)).where(joinTable.on || {}).transacting(trx);
}
};
/**
* Delete all or some relations of entity field
*/ const deleteRelations = async ({ id, attribute, db, relIdsToNotDelete = [], relIdsToDelete = [], transaction: trx })=>{
const { joinTable } = attribute;
const { joinColumn, inverseJoinColumn } = joinTable;
const all = relIdsToDelete === 'all';
if (hasOrderColumn(attribute) || hasInverseOrderColumn(attribute)) {
let lastId = 0;
let done = false;
const batchSize = 100;
while(!done){
const batchToDelete = await createQueryBuilder(joinTable.name, db).select(inverseJoinColumn.name).where({
[joinColumn.name]: id,
id: {
$gt: lastId
},
[inverseJoinColumn.name]: {
$notIn: relIdsToNotDelete
},
...all ? {} : {
[inverseJoinColumn.name]: {
$in: relIdsToDelete
}
}
}).where(joinTable.on || {}).orderBy('id').limit(batchSize).transacting(trx).execute();
done = batchToDelete.length < batchSize;
lastId = batchToDelete[batchToDelete.length - 1]?.id || 0;
const batchIds = map(inverseJoinColumn.name, batchToDelete);
await createQueryBuilder(joinTable.name, db).delete().where({
[joinColumn.name]: id,
[inverseJoinColumn.name]: {
$in: batchIds
}
}).where(joinTable.on || {}).transacting(trx).execute();
await cleanOrderColumns({
attribute,
db,
id,
inverseRelIds: batchIds,
transaction: trx
});
}
} else {
await createQueryBuilder(joinTable.name, db).delete().where({
[joinColumn.name]: id,
[inverseJoinColumn.name]: {
$notIn: relIdsToNotDelete
},
...all ? {} : {
[inverseJoinColumn.name]: {
$in: relIdsToDelete
}
}
}).where(joinTable.on || {}).transacting(trx).execute();
}
};
/**
* Clean the order columns by ensuring the order value are continuous (ex: 1, 2, 3 and not 1, 5, 10)
*/ const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds = [], transaction: trx })=>{
if (!(hasOrderColumn(attribute) && id) && !(hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds))) {
return;
}
const { joinTable } = attribute;
const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable;
/**
UPDATE :joinTable: as a,
(
SELECT
id,
ROW_NUMBER() OVER ( PARTITION BY :joinColumn: ORDER BY :orderColumn:) AS src_order,
FROM :joinTable:
WHERE :joinColumn: = :id
) AS b
SET :orderColumn: = b.src_order
WHERE b.id = a.id;
*/ const updateOrderColumn = async ()=>{
if (!hasOrderColumn(attribute) || !id) {
return;
}
const selectRowsToOrder = (joinTableName)=>db.connection(joinTableName).select('id').rowNumber('src_order', orderColumnName, joinColumn.name).where(joinColumn.name, id).toSQL();
switch(strapi.db.dialect.client){
case 'mysql':
{
// Here it's MariaDB and MySQL 8
const select = selectRowsToOrder(joinTable.name);
await db.getConnection().raw(`UPDATE ?? as a, ( ${select.sql} ) AS b
SET ?? = b.src_order
WHERE b.id = a.id`, [
joinTable.name,
...select.bindings,
orderColumnName
]).transacting(trx);
break;
}
default:
{
const joinTableName = addSchema(db, joinTable.name);
const select = selectRowsToOrder(joinTableName);
// raw query as knex doesn't allow updating from a subquery
await db.connection.raw(`UPDATE ?? as a
SET ?? = b.src_order
FROM ( ${select.sql} ) AS b
WHERE b.id = a.id`, [
joinTableName,
orderColumnName,
...select.bindings
]).transacting(trx);
}
}
};
/**
UPDATE :joinTable: as a,
(
SELECT
id,
ROW_NUMBER() OVER ( PARTITION BY :inverseJoinColumn: ORDER BY :inverseOrderColumn:) AS inv_order
FROM :joinTable:
WHERE :inverseJoinColumn: IN (:inverseRelIds)
) AS b
SET :inverseOrderColumn: = b.inv_order
WHERE b.id = a.id;
*/ const updateInverseOrderColumn = async ()=>{
if (!hasInverseOrderColumn(attribute) || isEmpty(inverseRelIds)) return;
const selectRowsToOrder = (joinTableName)=>db.connection(joinTableName).select('id').rowNumber('inv_order', inverseOrderColumnName, inverseJoinColumn.name).where(inverseJoinColumn.name, 'in', inverseRelIds).toSQL();
switch(strapi.db.dialect.client){
case 'mysql':
{
// Here it's MariaDB and MySQL 8
const select = selectRowsToOrder(joinTable.name);
await db.getConnection().raw(`UPDATE ?? as a, ( ${select.sql} ) AS b
SET ?? = b.inv_order
WHERE b.id = a.id`, [
joinTable.name,
...select.bindings,
inverseOrderColumnName
]).transacting(trx);
break;
}
default:
{
const joinTableName = addSchema(db, joinTable.name);
const select = selectRowsToOrder(joinTableName);
// raw query as knex doesn't allow updating from a subquery
await db.connection.raw(`UPDATE ?? as a
SET ?? = b.inv_order
FROM ( ${select.sql} ) AS b
WHERE b.id = a.id`, [
joinTableName,
inverseOrderColumnName,
...select.bindings
]).transacting(trx);
}
}
};
return Promise.all([
updateOrderColumn(),
updateInverseOrderColumn()
]);
};
export { cleanOrderColumns, deletePreviousAnyToOneRelations, deletePreviousOneToAnyRelations, deleteRelations };
//# sourceMappingURL=regular-relations.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"regular-relations.mjs","sources":["../../src/entity-manager/regular-relations.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-namespace */\nimport { map, isEmpty } from 'lodash/fp';\nimport type { Knex } from 'knex';\n\nimport {\n isBidirectional,\n isOneToAny,\n isManyToAny,\n isAnyToOne,\n hasOrderColumn,\n hasInverseOrderColumn,\n} from '../metadata';\nimport { createQueryBuilder } from '../query';\nimport { addSchema } from '../utils/knex';\nimport type { Database } from '..';\nimport type { ID, Relation, Model } from '../types';\n\ndeclare module 'knex' {\n namespace Knex {\n interface ChainableInterface {\n transacting(trx?: Knex.Transaction): this;\n }\n }\n}\n\n// TODO: This is a short term solution, to not steal relations from the same document.\nconst getDocumentSiblingIdsQuery = (tableName: string, id: ID) => {\n // Find if the model is a content type or something else (e.g. component)\n // to only get the documentId if it's a content type\n const models: Model[] = Array.from(strapi.db.metadata.values());\n\n const isContentType = models.find((model) => {\n return model.tableName === tableName && model.attributes.documentId;\n });\n\n if (!isContentType) {\n return [id];\n }\n\n // NOTE: SubQueries are wrapped in a function to not reuse the same connection,\n // which causes infinite self references\n return function (query) {\n query\n .select('id')\n .from(tableName)\n // Get all child ids of the document id\n .whereIn('document_id', (documentIDSubQuery) => {\n documentIDSubQuery\n .from(tableName)\n // get document id related to the current id\n .select('document_id')\n .where('id', id);\n });\n } satisfies Knex.QueryCallback;\n};\n\n/**\n * If some relations currently exist for this oneToX relation, on the one side, this function removes them and update the inverse order if needed.\n */\nconst deletePreviousOneToAnyRelations = async ({\n id,\n attribute,\n relIdsToadd,\n db,\n transaction: trx,\n}: {\n id: ID;\n attribute: Relation.Bidirectional;\n relIdsToadd: ID[];\n db: Database;\n transaction?: Knex.Transaction;\n}) => {\n if (!(isBidirectional(attribute) && isOneToAny(attribute))) {\n throw new Error(\n 'deletePreviousOneToAnyRelations can only be called for bidirectional oneToAny relations'\n );\n }\n const { joinTable } = attribute;\n const { joinColumn, inverseJoinColumn } = joinTable;\n\n const con = db.getConnection();\n\n await con\n .delete()\n .from(joinTable.name)\n // Exclude the ids of the current document\n .whereNotIn(joinColumn.name, getDocumentSiblingIdsQuery(joinColumn.referencedTable!, id))\n // Include all the ids that are being connected\n .whereIn(inverseJoinColumn.name, relIdsToadd)\n .where(joinTable.on || {})\n .transacting(trx);\n\n await cleanOrderColumns({ attribute, db, inverseRelIds: relIdsToadd, transaction: trx });\n};\n\n/**\n * If a relation currently exists for this xToOne relations, this function removes it and update the inverse order if needed.\n */\nconst deletePreviousAnyToOneRelations = async ({\n id,\n attribute,\n relIdToadd,\n db,\n transaction: trx,\n}: {\n id: ID;\n attribute: Relation.Bidirectional;\n relIdToadd: ID;\n db: Database;\n transaction?: Knex.Transaction;\n}) => {\n const { joinTable } = attribute;\n const { joinColumn, inverseJoinColumn } = joinTable;\n const con = db.getConnection();\n\n if (!isAnyToOne(attribute)) {\n throw new Error('deletePreviousAnyToOneRelations can only be called for anyToOne relations');\n }\n // handling manyToOne\n if (isManyToAny(attribute)) {\n // if the database integrity was not broken relsToDelete is supposed to be of length 1\n const relsToDelete = await con\n .select(inverseJoinColumn.name)\n .from(joinTable.name)\n .where(joinColumn.name, id)\n .whereNotIn(\n inverseJoinColumn.name,\n getDocumentSiblingIdsQuery(inverseJoinColumn.referencedTable!, relIdToadd)\n )\n .where(joinTable.on || {})\n .transacting(trx);\n\n const relIdsToDelete = map(inverseJoinColumn.name, relsToDelete);\n\n await createQueryBuilder(joinTable.name, db)\n .delete()\n .where({\n [joinColumn.name]: id,\n [inverseJoinColumn.name]: { $in: relIdsToDelete },\n })\n .where(joinTable.on || {})\n .transacting(trx)\n .execute();\n\n await cleanOrderColumns({ attribute, db, inverseRelIds: relIdsToDelete, transaction: trx });\n\n // handling oneToOne\n } else {\n await con\n .delete()\n .from(joinTable.name)\n .where(joinColumn.name, id)\n // Exclude the ids of the current document\n .whereNotIn(\n inverseJoinColumn.name,\n getDocumentSiblingIdsQuery(inverseJoinColumn.referencedTable!, relIdToadd)\n )\n .where(joinTable.on || {})\n .transacting(trx);\n }Line truncated
@@ -0,0 +1,74 @@
import type { ID } from '../types';
interface Link {
id: ID;
position?: {
before?: ID;
after?: ID;
start?: true;
end?: true;
};
order?: number;
__component?: string;
}
interface OrderedLink extends Link {
init?: boolean;
order: number;
}
/**
* When connecting relations, the order you connect them matters.
*
* Example, if you connect the following relations:
* { id: 5, position: { before: 1 } }
* { id: 1, position: { before: 2 } }
* { id: 2, position: { end: true } }
*
* Going through the connect array, id 5 has to be connected before id 1,
* so the order of id5 = id1 - 1. But the order value of id 1 is unknown.
* The only way to know the order of id 1 is to connect it first.
*
* This function makes sure the relations are connected in the right order:
* { id: 2, position: { end: true } }
* { id: 1, position: { before: 2 } }
* { id: 5, position: { before: 1 } }
*
*/
declare const sortConnectArray: (connectArr: Link[], initialArr?: Link[], strictSort?: boolean) => Link[];
/**
* Responsible for calculating the relations order when connecting them.
*
* The connect method takes an array of relations with positional attributes:
* - before: the id of the relation to connect before
* - after: the id of the relation to connect after
* - end: it should be at the end
* - start: it should be at the start
*
* Example:
* - Having a connect array like:
* [ { id: 4, before: 2 }, { id: 4, before: 3}, {id: 5, before: 4} ]
* - With the initial relations:
* [ { id: 2, order: 4 }, { id: 3, order: 10 } ]
* - Step by step, going through the connect array, the array of relations would be:
* [ { id: 4, order: 3.5 }, { id: 2, order: 4 }, { id: 3, order: 10 } ]
* [ { id: 2, order: 4 }, { id: 4, order: 3.5 }, { id: 3, order: 10 } ]
* [ { id: 2, order: 4 }, { id: 5, order: 3.5 }, { id: 4, order: 3.5 }, { id: 3, order: 10 } ]
* - The final step would be to recalculate fractional order values.
* [ { id: 2, order: 4 }, { id: 5, order: 3.33 }, { id: 4, order: 3.66 }, { id: 3, order: 10 } ]
*
* @param {Array<*>} initArr - array of relations to initialize the class with
* @param {string} idColumn - the column name of the id
* @param {string} orderColumn - the column name of the order
* @param {boolean} strict - if true, will throw an error if a relation is connected adjacent to
* another one that does not exist
* @return {*}
*/
declare const relationsOrderer: <TRelation extends Record<string, ID | null>>(initArr: TRelation[], idColumn: keyof TRelation, orderColumn: keyof TRelation, strict?: boolean) => {
disconnect(relations: Link | Link[]): any;
connect(relations: Link | Link[]): any;
get(): OrderedLink[];
/**
* Get a map between the relation id and its order
*/
getOrderMap(): Record<ID, number>;
};
export { relationsOrderer, sortConnectArray };
//# sourceMappingURL=relations-orderer.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"relations-orderer.d.ts","sourceRoot":"","sources":["../../src/entity-manager/relations-orderer.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,UAAU,CAAC;AAEnC,UAAU,IAAI;IACZ,EAAE,EAAE,EAAE,CAAC;IACP,QAAQ,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,IAAI,CAAC;QAAC,GAAG,CAAC,EAAE,IAAI,CAAA;KAAE,CAAC;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,UAAU,WAAY,SAAQ,IAAI;IAChC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,QAAA,MAAM,gBAAgB,eAAgB,IAAI,EAAE,eAAc,IAAI,EAAE,iCA0G/D,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,QAAA,MAAM,gBAAgB,yDACX,SAAS,EAAE,YACV,MAAM,SAAS,eACZ,MAAM,SAAS,WACnB,OAAO;0BAuDQ,IAAI,GAAG,IAAI,EAAE;uBAMhB,IAAI,GAAG,IAAI,EAAE;;IAqBhC;;OAEG;;CAgBN,CAAC;AAEF,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,CAAC"}
@@ -0,0 +1,221 @@
'use strict';
var _ = require('lodash/fp');
var _$1 = require('lodash');
var invalidRelation = require('../errors/invalid-relation.js');
/**
* When connecting relations, the order you connect them matters.
*
* Example, if you connect the following relations:
* { id: 5, position: { before: 1 } }
* { id: 1, position: { before: 2 } }
* { id: 2, position: { end: true } }
*
* Going through the connect array, id 5 has to be connected before id 1,
* so the order of id5 = id1 - 1. But the order value of id 1 is unknown.
* The only way to know the order of id 1 is to connect it first.
*
* This function makes sure the relations are connected in the right order:
* { id: 2, position: { end: true } }
* { id: 1, position: { before: 2 } }
* { id: 5, position: { before: 1 } }
*
*/ const sortConnectArray = (connectArr, initialArr = [], strictSort = true)=>{
const sortedConnect = [];
// Boolean to know if we have to recalculate the order of the relations
let needsSorting = false;
// Map to validate if relation is already in sortedConnect or DB.
const relationInInitialArray = initialArr.reduce((acc, rel)=>({
...acc,
[rel.id]: true
}), {});
// Map to store the first index where a relation id is connected
const mappedRelations = connectArr.reduce((mapper, relation)=>{
const adjacentRelId = relation.position?.before || relation.position?.after;
if (!adjacentRelId || !relationInInitialArray[adjacentRelId] && !mapper[adjacentRelId]) {
needsSorting = true;
}
/**
* We do not allow duplicate relations to be connected, so we need to check for uniqueness with components
* Note that the id here includes the uid for polymorphic relations
*
* So for normal relations, the same id means the same relation
* For component relations, it means the unique combo of (id, component name)
*/ // Check if there's an existing relation with this id
const existingRelation = mapper[relation.id];
// Check if existing relation has a component or not
const hasNoComponent = existingRelation && !('__component' in existingRelation);
// Check if the existing relation has the same component as the new relation
const hasSameComponent = existingRelation && existingRelation.__component === relation.__component;
// If we have an existing relation that is not unique (no component or same component) we won't accept it
if (existingRelation && (hasNoComponent || hasSameComponent)) {
throw new invalidRelation(`The relation with id ${relation.id} is already connected. ` + 'You cannot connect the same relation twice.');
}
return {
[relation.id]: {
...relation,
computed: false
},
...mapper
};
}, {});
// If we don't need to sort the connect array, we can return it as is
if (!needsSorting) return connectArr;
// Recursively compute in which order the relation should be connected
const computeRelation = (relation, relationsSeenInBranch)=>{
const adjacentRelId = relation.position?.before || relation.position?.after;
const adjacentRelation = mappedRelations[adjacentRelId];
// If the relation has already been seen in the current branch,
// it means there is a circular reference
if (adjacentRelId && relationsSeenInBranch[adjacentRelId]) {
throw new invalidRelation('A circular reference was found in the connect array. ' + 'One relation is trying to connect before/after another one that is trying to connect before/after it');
}
// This relation has already been computed
if (mappedRelations[relation.id]?.computed) {
return;
}
mappedRelations[relation.id].computed = true;
// Relation does not have a before or after attribute or is in the initial array
if (!adjacentRelId || relationInInitialArray[adjacentRelId]) {
sortedConnect.push(relation);
return;
}
// Look if id is referenced elsewhere in the array
if (mappedRelations[adjacentRelId]) {
computeRelation(adjacentRelation, {
...relationsSeenInBranch,
[relation.id]: true
});
sortedConnect.push(relation);
} else if (strictSort) {
// If we reach this point, it means that the adjacent relation is not in the connect array
// and it is not in the database.
throw new invalidRelation(`There was a problem connecting relation with id ${relation.id} at position ${JSON.stringify(relation.position)}. The relation with id ${adjacentRelId} needs to be connected first.`);
} else {
// We are in non-strict mode so we can push the relation.
sortedConnect.push({
id: relation.id,
position: {
end: true
}
});
}
};
// Iterate over connectArr and populate sortedConnect
connectArr.forEach((relation)=>computeRelation(relation, {}));
return sortedConnect;
};
/**
* Responsible for calculating the relations order when connecting them.
*
* The connect method takes an array of relations with positional attributes:
* - before: the id of the relation to connect before
* - after: the id of the relation to connect after
* - end: it should be at the end
* - start: it should be at the start
*
* Example:
* - Having a connect array like:
* [ { id: 4, before: 2 }, { id: 4, before: 3}, {id: 5, before: 4} ]
* - With the initial relations:
* [ { id: 2, order: 4 }, { id: 3, order: 10 } ]
* - Step by step, going through the connect array, the array of relations would be:
* [ { id: 4, order: 3.5 }, { id: 2, order: 4 }, { id: 3, order: 10 } ]
* [ { id: 2, order: 4 }, { id: 4, order: 3.5 }, { id: 3, order: 10 } ]
* [ { id: 2, order: 4 }, { id: 5, order: 3.5 }, { id: 4, order: 3.5 }, { id: 3, order: 10 } ]
* - The final step would be to recalculate fractional order values.
* [ { id: 2, order: 4 }, { id: 5, order: 3.33 }, { id: 4, order: 3.66 }, { id: 3, order: 10 } ]
*
* @param {Array<*>} initArr - array of relations to initialize the class with
* @param {string} idColumn - the column name of the id
* @param {string} orderColumn - the column name of the order
* @param {boolean} strict - if true, will throw an error if a relation is connected adjacent to
* another one that does not exist
* @return {*}
*/ const relationsOrderer = (initArr, idColumn, orderColumn, strict)=>{
const computedRelations = _.castArray(initArr ?? []).map((r)=>({
init: true,
id: r[idColumn],
order: Number(r[orderColumn]) || 1
}));
const maxOrder = _.maxBy('order', computedRelations)?.order || 0;
const findRelation = (id)=>{
const idx = computedRelations.findIndex((r)=>r.id === id);
return {
idx,
relation: computedRelations[idx]
};
};
const removeRelation = (r)=>{
const { idx } = findRelation(r.id);
if (idx >= 0) {
computedRelations.splice(idx, 1);
}
};
const insertRelation = (r)=>{
let idx;
if (r.position?.before) {
const { idx: _idx, relation } = findRelation(r.position.before);
if (relation.init) {
r.order = relation.order - 0.5;
} else {
r.order = relation.order;
}
idx = _idx;
} else if (r.position?.after) {
const { idx: _idx, relation } = findRelation(r.position.after);
if (relation.init) {
r.order = relation.order + 0.5;
} else {
r.order = relation.order;
}
idx = _idx + 1;
} else if (r.position?.start) {
r.order = 0.5;
idx = 0;
} else {
r.order = maxOrder + 0.5;
idx = computedRelations.length;
}
// Insert the relation in the array
computedRelations.splice(idx, 0, r);
};
return {
disconnect (relations) {
_.castArray(relations).forEach((relation)=>{
removeRelation(relation);
});
return this;
},
connect (relations) {
sortConnectArray(_.castArray(relations), computedRelations, strict).forEach((relation)=>{
this.disconnect(relation);
try {
insertRelation(relation);
} catch (err) {
throw new Error(`There was a problem connecting relation with id ${relation.id} at position ${JSON.stringify(relation.position)}. The list of connect relations is not valid`);
}
});
return this;
},
get () {
return computedRelations;
},
/**
* Get a map between the relation id and its order
*/ getOrderMap () {
return _$1(computedRelations).groupBy('order').reduce((acc, relations)=>{
if (relations[0]?.init) return acc;
relations.forEach((relation, idx)=>{
acc[relation.id] = Math.floor(relation.order) + (idx + 1) / (relations.length + 1);
});
return acc;
}, {});
}
};
};
exports.relationsOrderer = relationsOrderer;
exports.sortConnectArray = sortConnectArray;
//# sourceMappingURL=relations-orderer.js.map
@@ -0,0 +1 @@
{"version":3,"file":"relations-orderer.js","sources":["../../src/entity-manager/relations-orderer.ts"],"sourcesContent":["import { castArray, maxBy } from 'lodash/fp';\nimport _ from 'lodash';\n\nimport { InvalidRelationError } from '../errors';\nimport type { ID } from '../types';\n\ninterface Link {\n id: ID;\n position?: { before?: ID; after?: ID; start?: true; end?: true };\n order?: number;\n __component?: string;\n}\n\ninterface OrderedLink extends Link {\n init?: boolean;\n order: number;\n}\n\n/**\n * When connecting relations, the order you connect them matters.\n *\n * Example, if you connect the following relations:\n * { id: 5, position: { before: 1 } }\n * { id: 1, position: { before: 2 } }\n * { id: 2, position: { end: true } }\n *\n * Going through the connect array, id 5 has to be connected before id 1,\n * so the order of id5 = id1 - 1. But the order value of id 1 is unknown.\n * The only way to know the order of id 1 is to connect it first.\n *\n * This function makes sure the relations are connected in the right order:\n * { id: 2, position: { end: true } }\n * { id: 1, position: { before: 2 } }\n * { id: 5, position: { before: 1 } }\n *\n */\nconst sortConnectArray = (connectArr: Link[], initialArr: Link[] = [], strictSort = true) => {\n const sortedConnect: Link[] = [];\n // Boolean to know if we have to recalculate the order of the relations\n let needsSorting = false;\n // Map to validate if relation is already in sortedConnect or DB.\n const relationInInitialArray = initialArr.reduce(\n (acc, rel) => ({ ...acc, [rel.id]: true }),\n {} as Record<ID, boolean>\n );\n // Map to store the first index where a relation id is connected\n const mappedRelations = connectArr.reduce(\n (mapper, relation: Link) => {\n const adjacentRelId = relation.position?.before || relation.position?.after;\n\n if (!adjacentRelId || (!relationInInitialArray[adjacentRelId] && !mapper[adjacentRelId])) {\n needsSorting = true;\n }\n\n /**\n * We do not allow duplicate relations to be connected, so we need to check for uniqueness with components\n * Note that the id here includes the uid for polymorphic relations\n *\n * So for normal relations, the same id means the same relation\n * For component relations, it means the unique combo of (id, component name)\n */\n\n // Check if there's an existing relation with this id\n const existingRelation = mapper[relation.id];\n\n // Check if existing relation has a component or not\n const hasNoComponent = existingRelation && !('__component' in existingRelation);\n\n // Check if the existing relation has the same component as the new relation\n const hasSameComponent =\n existingRelation && existingRelation.__component === relation.__component;\n\n // If we have an existing relation that is not unique (no component or same component) we won't accept it\n if (existingRelation && (hasNoComponent || hasSameComponent)) {\n throw new InvalidRelationError(\n `The relation with id ${relation.id} is already connected. ` +\n 'You cannot connect the same relation twice.'\n );\n }\n\n return {\n [relation.id]: { ...relation, computed: false },\n ...mapper,\n };\n },\n {} as Record<ID, Link & { computed: boolean }>\n );\n\n // If we don't need to sort the connect array, we can return it as is\n if (!needsSorting) return connectArr;\n\n // Recursively compute in which order the relation should be connected\n const computeRelation = (relation: Link, relationsSeenInBranch: Record<ID, boolean>) => {\n const adjacentRelId = relation.position?.before || relation.position?.after;\n const adjacentRelation = mappedRelations[adjacentRelId as ID];\n\n // If the relation has already been seen in the current branch,\n // it means there is a circular reference\n if (adjacentRelId && relationsSeenInBranch[adjacentRelId]) {\n throw new InvalidRelationError(\n 'A circular reference was found in the connect array. ' +\n 'One relation is trying to connect before/after another one that is trying to connect before/after it'\n );\n }\n\n // This relation has already been computed\n if (mappedRelations[relation.id]?.computed) {\n return;\n }\n\n mappedRelations[relation.id].computed = true;\n\n // Relation does not have a before or after attribute or is in the initial array\n if (!adjacentRelId || relationInInitialArray[adjacentRelId]) {\n sortedConnect.push(relation);\n return;\n }\n\n // Look if id is referenced elsewhere in the array\n if (mappedRelations[adjacentRelId]) {\n computeRelation(adjacentRelation, { ...relationsSeenInBranch, [relation.id]: true });\n sortedConnect.push(relation);\n } else if (strictSort) {\n // If we reach this point, it means that the adjacent relation is noLine truncated
@@ -0,0 +1,218 @@
import { castArray, maxBy } from 'lodash/fp';
import _ from 'lodash';
import InvalidRelationError from '../errors/invalid-relation.mjs';
/**
* When connecting relations, the order you connect them matters.
*
* Example, if you connect the following relations:
* { id: 5, position: { before: 1 } }
* { id: 1, position: { before: 2 } }
* { id: 2, position: { end: true } }
*
* Going through the connect array, id 5 has to be connected before id 1,
* so the order of id5 = id1 - 1. But the order value of id 1 is unknown.
* The only way to know the order of id 1 is to connect it first.
*
* This function makes sure the relations are connected in the right order:
* { id: 2, position: { end: true } }
* { id: 1, position: { before: 2 } }
* { id: 5, position: { before: 1 } }
*
*/ const sortConnectArray = (connectArr, initialArr = [], strictSort = true)=>{
const sortedConnect = [];
// Boolean to know if we have to recalculate the order of the relations
let needsSorting = false;
// Map to validate if relation is already in sortedConnect or DB.
const relationInInitialArray = initialArr.reduce((acc, rel)=>({
...acc,
[rel.id]: true
}), {});
// Map to store the first index where a relation id is connected
const mappedRelations = connectArr.reduce((mapper, relation)=>{
const adjacentRelId = relation.position?.before || relation.position?.after;
if (!adjacentRelId || !relationInInitialArray[adjacentRelId] && !mapper[adjacentRelId]) {
needsSorting = true;
}
/**
* We do not allow duplicate relations to be connected, so we need to check for uniqueness with components
* Note that the id here includes the uid for polymorphic relations
*
* So for normal relations, the same id means the same relation
* For component relations, it means the unique combo of (id, component name)
*/ // Check if there's an existing relation with this id
const existingRelation = mapper[relation.id];
// Check if existing relation has a component or not
const hasNoComponent = existingRelation && !('__component' in existingRelation);
// Check if the existing relation has the same component as the new relation
const hasSameComponent = existingRelation && existingRelation.__component === relation.__component;
// If we have an existing relation that is not unique (no component or same component) we won't accept it
if (existingRelation && (hasNoComponent || hasSameComponent)) {
throw new InvalidRelationError(`The relation with id ${relation.id} is already connected. ` + 'You cannot connect the same relation twice.');
}
return {
[relation.id]: {
...relation,
computed: false
},
...mapper
};
}, {});
// If we don't need to sort the connect array, we can return it as is
if (!needsSorting) return connectArr;
// Recursively compute in which order the relation should be connected
const computeRelation = (relation, relationsSeenInBranch)=>{
const adjacentRelId = relation.position?.before || relation.position?.after;
const adjacentRelation = mappedRelations[adjacentRelId];
// If the relation has already been seen in the current branch,
// it means there is a circular reference
if (adjacentRelId && relationsSeenInBranch[adjacentRelId]) {
throw new InvalidRelationError('A circular reference was found in the connect array. ' + 'One relation is trying to connect before/after another one that is trying to connect before/after it');
}
// This relation has already been computed
if (mappedRelations[relation.id]?.computed) {
return;
}
mappedRelations[relation.id].computed = true;
// Relation does not have a before or after attribute or is in the initial array
if (!adjacentRelId || relationInInitialArray[adjacentRelId]) {
sortedConnect.push(relation);
return;
}
// Look if id is referenced elsewhere in the array
if (mappedRelations[adjacentRelId]) {
computeRelation(adjacentRelation, {
...relationsSeenInBranch,
[relation.id]: true
});
sortedConnect.push(relation);
} else if (strictSort) {
// If we reach this point, it means that the adjacent relation is not in the connect array
// and it is not in the database.
throw new InvalidRelationError(`There was a problem connecting relation with id ${relation.id} at position ${JSON.stringify(relation.position)}. The relation with id ${adjacentRelId} needs to be connected first.`);
} else {
// We are in non-strict mode so we can push the relation.
sortedConnect.push({
id: relation.id,
position: {
end: true
}
});
}
};
// Iterate over connectArr and populate sortedConnect
connectArr.forEach((relation)=>computeRelation(relation, {}));
return sortedConnect;
};
/**
* Responsible for calculating the relations order when connecting them.
*
* The connect method takes an array of relations with positional attributes:
* - before: the id of the relation to connect before
* - after: the id of the relation to connect after
* - end: it should be at the end
* - start: it should be at the start
*
* Example:
* - Having a connect array like:
* [ { id: 4, before: 2 }, { id: 4, before: 3}, {id: 5, before: 4} ]
* - With the initial relations:
* [ { id: 2, order: 4 }, { id: 3, order: 10 } ]
* - Step by step, going through the connect array, the array of relations would be:
* [ { id: 4, order: 3.5 }, { id: 2, order: 4 }, { id: 3, order: 10 } ]
* [ { id: 2, order: 4 }, { id: 4, order: 3.5 }, { id: 3, order: 10 } ]
* [ { id: 2, order: 4 }, { id: 5, order: 3.5 }, { id: 4, order: 3.5 }, { id: 3, order: 10 } ]
* - The final step would be to recalculate fractional order values.
* [ { id: 2, order: 4 }, { id: 5, order: 3.33 }, { id: 4, order: 3.66 }, { id: 3, order: 10 } ]
*
* @param {Array<*>} initArr - array of relations to initialize the class with
* @param {string} idColumn - the column name of the id
* @param {string} orderColumn - the column name of the order
* @param {boolean} strict - if true, will throw an error if a relation is connected adjacent to
* another one that does not exist
* @return {*}
*/ const relationsOrderer = (initArr, idColumn, orderColumn, strict)=>{
const computedRelations = castArray(initArr ?? []).map((r)=>({
init: true,
id: r[idColumn],
order: Number(r[orderColumn]) || 1
}));
const maxOrder = maxBy('order', computedRelations)?.order || 0;
const findRelation = (id)=>{
const idx = computedRelations.findIndex((r)=>r.id === id);
return {
idx,
relation: computedRelations[idx]
};
};
const removeRelation = (r)=>{
const { idx } = findRelation(r.id);
if (idx >= 0) {
computedRelations.splice(idx, 1);
}
};
const insertRelation = (r)=>{
let idx;
if (r.position?.before) {
const { idx: _idx, relation } = findRelation(r.position.before);
if (relation.init) {
r.order = relation.order - 0.5;
} else {
r.order = relation.order;
}
idx = _idx;
} else if (r.position?.after) {
const { idx: _idx, relation } = findRelation(r.position.after);
if (relation.init) {
r.order = relation.order + 0.5;
} else {
r.order = relation.order;
}
idx = _idx + 1;
} else if (r.position?.start) {
r.order = 0.5;
idx = 0;
} else {
r.order = maxOrder + 0.5;
idx = computedRelations.length;
}
// Insert the relation in the array
computedRelations.splice(idx, 0, r);
};
return {
disconnect (relations) {
castArray(relations).forEach((relation)=>{
removeRelation(relation);
});
return this;
},
connect (relations) {
sortConnectArray(castArray(relations), computedRelations, strict).forEach((relation)=>{
this.disconnect(relation);
try {
insertRelation(relation);
} catch (err) {
throw new Error(`There was a problem connecting relation with id ${relation.id} at position ${JSON.stringify(relation.position)}. The list of connect relations is not valid`);
}
});
return this;
},
get () {
return computedRelations;
},
/**
* Get a map between the relation id and its order
*/ getOrderMap () {
return _(computedRelations).groupBy('order').reduce((acc, relations)=>{
if (relations[0]?.init) return acc;
relations.forEach((relation, idx)=>{
acc[relation.id] = Math.floor(relation.order) + (idx + 1) / (relations.length + 1);
});
return acc;
}, {});
}
};
};
export { relationsOrderer, sortConnectArray };
//# sourceMappingURL=relations-orderer.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"relations-orderer.mjs","sources":["../../src/entity-manager/relations-orderer.ts"],"sourcesContent":["import { castArray, maxBy } from 'lodash/fp';\nimport _ from 'lodash';\n\nimport { InvalidRelationError } from '../errors';\nimport type { ID } from '../types';\n\ninterface Link {\n id: ID;\n position?: { before?: ID; after?: ID; start?: true; end?: true };\n order?: number;\n __component?: string;\n}\n\ninterface OrderedLink extends Link {\n init?: boolean;\n order: number;\n}\n\n/**\n * When connecting relations, the order you connect them matters.\n *\n * Example, if you connect the following relations:\n * { id: 5, position: { before: 1 } }\n * { id: 1, position: { before: 2 } }\n * { id: 2, position: { end: true } }\n *\n * Going through the connect array, id 5 has to be connected before id 1,\n * so the order of id5 = id1 - 1. But the order value of id 1 is unknown.\n * The only way to know the order of id 1 is to connect it first.\n *\n * This function makes sure the relations are connected in the right order:\n * { id: 2, position: { end: true } }\n * { id: 1, position: { before: 2 } }\n * { id: 5, position: { before: 1 } }\n *\n */\nconst sortConnectArray = (connectArr: Link[], initialArr: Link[] = [], strictSort = true) => {\n const sortedConnect: Link[] = [];\n // Boolean to know if we have to recalculate the order of the relations\n let needsSorting = false;\n // Map to validate if relation is already in sortedConnect or DB.\n const relationInInitialArray = initialArr.reduce(\n (acc, rel) => ({ ...acc, [rel.id]: true }),\n {} as Record<ID, boolean>\n );\n // Map to store the first index where a relation id is connected\n const mappedRelations = connectArr.reduce(\n (mapper, relation: Link) => {\n const adjacentRelId = relation.position?.before || relation.position?.after;\n\n if (!adjacentRelId || (!relationInInitialArray[adjacentRelId] && !mapper[adjacentRelId])) {\n needsSorting = true;\n }\n\n /**\n * We do not allow duplicate relations to be connected, so we need to check for uniqueness with components\n * Note that the id here includes the uid for polymorphic relations\n *\n * So for normal relations, the same id means the same relation\n * For component relations, it means the unique combo of (id, component name)\n */\n\n // Check if there's an existing relation with this id\n const existingRelation = mapper[relation.id];\n\n // Check if existing relation has a component or not\n const hasNoComponent = existingRelation && !('__component' in existingRelation);\n\n // Check if the existing relation has the same component as the new relation\n const hasSameComponent =\n existingRelation && existingRelation.__component === relation.__component;\n\n // If we have an existing relation that is not unique (no component or same component) we won't accept it\n if (existingRelation && (hasNoComponent || hasSameComponent)) {\n throw new InvalidRelationError(\n `The relation with id ${relation.id} is already connected. ` +\n 'You cannot connect the same relation twice.'\n );\n }\n\n return {\n [relation.id]: { ...relation, computed: false },\n ...mapper,\n };\n },\n {} as Record<ID, Link & { computed: boolean }>\n );\n\n // If we don't need to sort the connect array, we can return it as is\n if (!needsSorting) return connectArr;\n\n // Recursively compute in which order the relation should be connected\n const computeRelation = (relation: Link, relationsSeenInBranch: Record<ID, boolean>) => {\n const adjacentRelId = relation.position?.before || relation.position?.after;\n const adjacentRelation = mappedRelations[adjacentRelId as ID];\n\n // If the relation has already been seen in the current branch,\n // it means there is a circular reference\n if (adjacentRelId && relationsSeenInBranch[adjacentRelId]) {\n throw new InvalidRelationError(\n 'A circular reference was found in the connect array. ' +\n 'One relation is trying to connect before/after another one that is trying to connect before/after it'\n );\n }\n\n // This relation has already been computed\n if (mappedRelations[relation.id]?.computed) {\n return;\n }\n\n mappedRelations[relation.id].computed = true;\n\n // Relation does not have a before or after attribute or is in the initial array\n if (!adjacentRelId || relationInInitialArray[adjacentRelId]) {\n sortedConnect.push(relation);\n return;\n }\n\n // Look if id is referenced elsewhere in the array\n if (mappedRelations[adjacentRelId]) {\n computeRelation(adjacentRelation, { ...relationsSeenInBranch, [relation.id]: true });\n sortedConnect.push(relation);\n } else if (strictSort) {\n // If we reach this point, it means that the adjacent relation is nLine truncated
+79
View File
@@ -0,0 +1,79 @@
import type { Knex } from 'knex';
import type { CountResult, ID } from '../types';
import { QueryBuilder } from '../query/query-builder';
export type Data = Record<string, unknown>;
export type Params = {
where?: any;
filters?: any;
select?: any;
populate?: any;
orderBy?: any;
_q?: string;
data?: any;
page?: number;
pageSize?: number;
limit?: number;
offset?: number;
count?: boolean;
};
export type FindOneParams = Pick<Params, 'where' | 'select' | 'populate' | '_q' | 'orderBy'>;
export interface Repository {
findOne(params?: FindOneParams): Promise<any>;
findMany(params?: Params): Promise<any[]>;
findWithCount(params?: Params): Promise<[any[], number]>;
findPage(params: Params): Promise<{
results: any[];
pagination: {
page: number;
pageSize: number;
pageCount: number;
total: number;
};
}>;
create(params: Params): Promise<any>;
createMany(params: Params): Promise<CountResult & {
ids: ID[];
}>;
update(params: Params): Promise<any>;
updateMany(params: Params): Promise<CountResult>;
delete(params: Params): Promise<any>;
deleteMany(params?: Params): Promise<CountResult>;
count(params?: Params): Promise<number>;
attachRelations(id: ID, data: Data): Promise<any>;
updateRelations(id: ID, data: Data): Promise<any>;
deleteRelations(id: ID): Promise<any>;
populate(entity: Entity, populate: Params['populate']): Promise<any>;
load(entity: any, field: string | string[], populate?: Params['populate']): Promise<any>;
loadPages<TField extends string>(entity: any, field: TField | TField[], populate?: Params['populate']): Promise<any>;
}
export type Entity = {
id: ID;
[key: string]: any;
};
export interface EntityManager {
findOne(uid: string, params: Params): Promise<any>;
findMany(uid: string, params: Params): Promise<any[]>;
count(uid: string, params?: Params): Promise<number>;
create(uid: string, params: Params): Promise<any>;
createMany(uid: string, params: Params): Promise<CountResult & {
ids: ID[];
}>;
update(uid: string, params: Params): Promise<any>;
updateMany(uid: string, params: Params): Promise<CountResult>;
delete(uid: string, params: Params): Promise<any>;
deleteMany(uid: string, params: Params): Promise<CountResult>;
populate(uid: string, entity: Entity, populate: Params['populate']): Promise<Entity>;
load(uid: string, entity: Entity, field: string | string[], populate?: Params['populate']): Promise<any>;
attachRelations(uid: string, id: ID, data: any, options?: {
transaction?: Knex.Transaction;
}): Promise<any>;
updateRelations(uid: string, id: ID, data: any, options?: {
transaction?: Knex.Transaction;
}): Promise<any>;
deleteRelations(uid: string, id: ID, options?: {
transaction?: Knex.Transaction;
}): Promise<void>;
createQueryBuilder(uid: string): QueryBuilder;
getRepository(uid: string): Repository;
}
//# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/entity-manager/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAEtD,MAAM,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE3C,MAAM,MAAM,MAAM,GAAG;IACnB,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,MAAM,EAAE,OAAO,GAAG,QAAQ,GAAG,UAAU,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;AAE7F,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAC9C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAC1C,aAAa,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;IACzD,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;QAChC,OAAO,EAAE,GAAG,EAAE,CAAC;QACf,UAAU,EAAE;YACV,IAAI,EAAE,MAAM,CAAC;YACb,QAAQ,EAAE,MAAM,CAAC;YACjB,SAAS,EAAE,MAAM,CAAC;YAClB,KAAK,EAAE,MAAM,CAAC;SACf,CAAC;KACH,CAAC,CAAC;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IACrC,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG;QAAE,GAAG,EAAE,EAAE,EAAE,CAAA;KAAE,CAAC,CAAC;IACjE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IACrC,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACjD,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IACrC,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAClD,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,eAAe,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAClD,eAAe,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAClD,eAAe,CAAC,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IACrE,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IACzF,SAAS,CAAC,MAAM,SAAS,MAAM,EAC7B,MAAM,EAAE,GAAG,EACX,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,GAC5B,OAAO,CAAC,GAAG,CAAC,CAAC;CACjB;AAED,MAAM,MAAM,MAAM,GAAG;IACnB,EAAE,EAAE,EAAE,CAAC;IACP,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB,CAAC;AAGF,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IACnD,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACtD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAClD,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG;QAAE,GAAG,EAAE,EAAE,EAAE,CAAA;KAAE,CAAC,CAAC;IAC9E,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAClD,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC9D,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAClD,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC9D,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrF,IAAI,CACF,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,GAC5B,OAAO,CAAC,GAAG,CAAC,CAAC;IAChB,eAAe,CACb,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,EAAE,EACN,IAAI,EAAE,GAAG,EACT,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,IAAI,CAAC,WAAW,CAAA;KAAE,GAC3C,OAAO,CAAC,GAAG,CAAC,CAAC;IAChB,eAAe,CACb,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,EAAE,EACN,IAAI,EAAE,GAAG,EACT,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,IAAI,CAAC,WAAW,CAAA;KAAE,GAC3C,OAAO,CAAC,GAAG,CAAC,CAAC;IAChB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,IAAI,CAAC,WAAW,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClG,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAAC;IAC9C,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAAC;CACxC"}