mirror of
https://github.com/strapi/strapi.git
synced 2025-11-01 18:33:55 +00:00
Merge pull request #15175 from strapi/deits/import-links
This commit is contained in:
commit
e7ec94dec7
@ -127,6 +127,21 @@ class LocalStrapiDestinationProvider implements IDestinationProvider {
|
||||
|
||||
throw new Error(`Invalid strategy supplied: "${strategy}"`);
|
||||
}
|
||||
|
||||
async getLinksStream(): Promise<Writable> {
|
||||
if (!this.strapi) {
|
||||
throw new Error('Not able to stream links. Strapi instance not found');
|
||||
}
|
||||
|
||||
const { strategy } = this.options;
|
||||
const mapID = (uid: string, id: number): number | undefined => this.#entitiesMapper[uid]?.[id];
|
||||
|
||||
if (strategy === 'restore') {
|
||||
return restore.createLinksWriteStream(mapID, this.strapi);
|
||||
}
|
||||
|
||||
throw new Error(`Invalid strategy supplied: "${strategy}"`);
|
||||
}
|
||||
}
|
||||
|
||||
export const createLocalStrapiDestinationProvider = (
|
||||
|
||||
@ -130,3 +130,4 @@ const useResults = (
|
||||
|
||||
export * from './entities';
|
||||
export * from './configuration';
|
||||
export * from './links';
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
import { Writable } from 'stream';
|
||||
import { ILink } from '../../../../../types';
|
||||
import { createLinkQuery } from '../../../shared/strapi/link';
|
||||
|
||||
export const createLinksWriteStream = (
|
||||
mapID: (uid: string, id: number) => number | undefined,
|
||||
strapi: Strapi.Strapi
|
||||
) => {
|
||||
return new Writable({
|
||||
objectMode: true,
|
||||
async write(link: ILink, _encoding, callback) {
|
||||
const { left, right } = link;
|
||||
const query = createLinkQuery(strapi);
|
||||
|
||||
// Map IDs if needed
|
||||
left.ref = mapID(left.type, left.ref) ?? left.ref;
|
||||
right.ref = mapID(right.type, right.ref) ?? right.ref;
|
||||
|
||||
try {
|
||||
await query().insert(link);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
return callback(e);
|
||||
}
|
||||
|
||||
return callback(
|
||||
new Error(`An error happened while trying to import a ${left.type} link. ${e}`)
|
||||
);
|
||||
}
|
||||
|
||||
callback(null);
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,10 +1,10 @@
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import { createLinksStream } from '../links';
|
||||
import { getDeepPopulateQuery } from '../links/utils';
|
||||
import { collect, getStrapiFactory } from '../../../__tests__/test-utils';
|
||||
|
||||
describe('Local Strapi Source Provider - Entities Streaming', () => {
|
||||
// TODO: entityService needs to be replaced with a mocked wrapper of db.connection and provide real metadata
|
||||
describe.skip('Local Strapi Source Provider - Entities Streaming', () => {
|
||||
describe('Create Links Stream', () => {
|
||||
test('should return an empty stream if no content types ', async () => {
|
||||
const strapi = getStrapiFactory({
|
||||
@ -326,99 +326,4 @@ describe('Local Strapi Source Provider - Entities Streaming', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Get Deep Populate Query', () => {
|
||||
test('Should return an empty object if there is nothing to populate', () => {
|
||||
const strapi = getStrapiFactory({
|
||||
contentTypes: {
|
||||
blog: {
|
||||
uid: 'blog',
|
||||
attributes: {
|
||||
title: { type: 'string' },
|
||||
age: { type: 'number' },
|
||||
},
|
||||
},
|
||||
},
|
||||
})();
|
||||
|
||||
const schema = strapi.contentTypes.blog;
|
||||
const populate = getDeepPopulateQuery(schema, strapi);
|
||||
|
||||
expect(populate).toEqual({});
|
||||
});
|
||||
|
||||
test("Should return an empty object if there are components or dynamic zones but they doesn't contain relations", () => {
|
||||
const strapi = getStrapiFactory({
|
||||
contentTypes: {
|
||||
blog: {
|
||||
uid: 'blog',
|
||||
attributes: {
|
||||
title: { type: 'string' },
|
||||
age: { type: 'number' },
|
||||
compo: { type: 'component', component: 'somecomponent' },
|
||||
dz: { type: 'dynamiczone', components: ['somecomponent'] },
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
somecomponent: {
|
||||
uid: 'somecomponent',
|
||||
attributes: {
|
||||
field: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
})();
|
||||
|
||||
const schema = strapi.contentTypes.blog;
|
||||
const populate = getDeepPopulateQuery(schema, strapi);
|
||||
|
||||
expect(populate).toEqual({});
|
||||
});
|
||||
|
||||
test('Should return a nested populate object for first level of relations', () => {
|
||||
const strapi = getStrapiFactory({
|
||||
contentTypes: {
|
||||
blog: {
|
||||
uid: 'blog',
|
||||
attributes: {
|
||||
title: { type: 'string' },
|
||||
age: { type: 'number' },
|
||||
compo: { type: 'component', component: 'somecomponent' },
|
||||
dz: { type: 'dynamiczone', components: ['somecomponent'] },
|
||||
blog: { type: 'relation', target: 'blog', relation: 'oneToOne' },
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
somecomponent: {
|
||||
uid: 'somecomponent',
|
||||
attributes: {
|
||||
field: { type: 'string' },
|
||||
blog: { type: 'relation', target: 'blog', relation: 'oneToOne' },
|
||||
},
|
||||
},
|
||||
},
|
||||
})();
|
||||
|
||||
const schema = strapi.contentTypes.blog;
|
||||
const populate = getDeepPopulateQuery(schema, strapi);
|
||||
|
||||
expect(populate).toMatchObject(
|
||||
expect.objectContaining({
|
||||
blog: {
|
||||
fields: ['id'],
|
||||
},
|
||||
compo: {
|
||||
fields: ['id'],
|
||||
populate: { blog: { fields: ['id'] } },
|
||||
},
|
||||
dz: {
|
||||
fields: ['id'],
|
||||
populate: { blog: { fields: ['id'] } },
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,6 +2,8 @@ import { chain } from 'stream-chain';
|
||||
import { Readable } from 'stream';
|
||||
import { set } from 'lodash/fp';
|
||||
|
||||
import type { IConfiguration } from '../../../types';
|
||||
|
||||
/**
|
||||
* Create a readable stream that export the Strapi app configuration
|
||||
*/
|
||||
@ -23,7 +25,7 @@ export const createConfigurationStream = (strapi: Strapi.Strapi): Readable => {
|
||||
|
||||
// Readable configuration stream
|
||||
return Readable.from(
|
||||
(async function* () {
|
||||
(async function* configurationGenerator(): AsyncGenerator<IConfiguration> {
|
||||
for (const stream of streams) {
|
||||
for await (const item of stream) {
|
||||
yield item;
|
||||
|
||||
@ -2,6 +2,7 @@ import type { ContentTypeSchema } from '@strapi/strapi';
|
||||
|
||||
import { isObject, isArray, isEmpty, size } from 'lodash/fp';
|
||||
import { Readable, PassThrough } from 'stream';
|
||||
import { IEntity } from '../../../types';
|
||||
|
||||
/**
|
||||
* Generate and consume content-types streams in order to stream each entity individually
|
||||
@ -26,7 +27,10 @@ export const createEntitiesStream = (strapi: Strapi.Strapi): Readable => {
|
||||
}
|
||||
|
||||
return Readable.from(
|
||||
(async function* () {
|
||||
(async function* entitiesGenerator(): AsyncGenerator<{
|
||||
entity: IEntity;
|
||||
contentType: ContentTypeSchema;
|
||||
}> {
|
||||
for await (const { stream, contentType } of contentTypeStreamGenerator()) {
|
||||
for await (const entity of stream) {
|
||||
yield { entity, contentType };
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import type { ILink } from '../../../types';
|
||||
import { createLinkQuery } from '../shared/strapi/link';
|
||||
|
||||
/**
|
||||
* Create a Readable which will stream all the links from a Strapi instance
|
||||
*/
|
||||
export const createLinksStream = (strapi: Strapi.Strapi): Readable => {
|
||||
const uids = [...Object.keys(strapi.contentTypes), ...Object.keys(strapi.components)] as string[];
|
||||
|
||||
// Async generator stream that returns every link from a Strapi instance
|
||||
return Readable.from(
|
||||
(async function* linkGenerator(): AsyncGenerator<ILink> {
|
||||
const query = createLinkQuery(strapi);
|
||||
|
||||
for (const uid of uids) {
|
||||
const generator = query().generateAll(uid);
|
||||
|
||||
for await (const link of generator) {
|
||||
yield link;
|
||||
}
|
||||
}
|
||||
})()
|
||||
);
|
||||
};
|
||||
@ -1,45 +0,0 @@
|
||||
import type { ContentTypeSchema } from '@strapi/strapi';
|
||||
|
||||
import { Readable } from 'stream';
|
||||
import { castArray } from 'lodash/fp';
|
||||
|
||||
import { getDeepPopulateQuery, parseEntityLinks } from './utils';
|
||||
|
||||
/**
|
||||
* Create a Duplex instance which will stream all the links from a Strapi instance
|
||||
*/
|
||||
export const createLinksStream = (strapi: Strapi.Strapi): Readable => {
|
||||
const schemas: ContentTypeSchema[] = Object.values(strapi.contentTypes);
|
||||
|
||||
// Destroy the Duplex stream instance
|
||||
const destroy = (): void => {
|
||||
if (!stream.destroyed) {
|
||||
stream.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
// Async generator stream that returns every link from a Strapi instance
|
||||
const stream = Readable.from(
|
||||
(async function* () {
|
||||
for (const schema of schemas) {
|
||||
const populate = getDeepPopulateQuery(schema, strapi);
|
||||
const query = { fields: ['id'], populate };
|
||||
|
||||
// TODO: Replace with the DB stream API
|
||||
const results = await strapi.entityService.findMany(schema.uid, query);
|
||||
|
||||
for (const entity of castArray(results)) {
|
||||
const links = parseEntityLinks(entity, populate, schema, strapi);
|
||||
|
||||
for (const link of links) {
|
||||
yield link;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy();
|
||||
})()
|
||||
);
|
||||
|
||||
return stream;
|
||||
};
|
||||
@ -1,190 +0,0 @@
|
||||
import type { RelationsType } from '@strapi/strapi';
|
||||
import { concat, set, isEmpty } from 'lodash/fp';
|
||||
import type { ILink } from '../../../../types';
|
||||
|
||||
// TODO: Fix any typings when we'll have better Strapi types
|
||||
|
||||
/**
|
||||
* Parse every links from an entity result (including nested components and dynamic zone levels)
|
||||
*/
|
||||
export const parseEntityLinks = (entity: any, populate: any, schema: any, strapi: any): any[] => {
|
||||
if (!entity) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (Array.isArray(entity)) {
|
||||
return entity
|
||||
.map((item) => parseEntityLinks(item, populate, schema, strapi))
|
||||
.reduce(concat, [])
|
||||
.flat();
|
||||
}
|
||||
|
||||
const { attributes } = schema;
|
||||
const links = [];
|
||||
|
||||
for (const key of Object.keys(populate)) {
|
||||
const attribute = attributes[key];
|
||||
const value = entity[key];
|
||||
const subPopulate = populate[key];
|
||||
|
||||
// Ignore nil values (relations, components or dynamic zones not set)
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Components
|
||||
// Recurse to find relations
|
||||
if (attribute.type === 'component') {
|
||||
const componentSchema = strapi.components[attribute.component];
|
||||
const componentLinks = parseEntityLinks(value, subPopulate.populate, componentSchema, strapi);
|
||||
|
||||
links.push(...componentLinks);
|
||||
}
|
||||
|
||||
// Dynamic Zones
|
||||
// We need to extract links from each items in the DZ's components
|
||||
if (attribute.type === 'dynamiczone') {
|
||||
const dzLinks = value
|
||||
.map(({ __component, ...item }: any) =>
|
||||
parseEntityLinks(item, subPopulate.populate, strapi.components[__component], strapi)
|
||||
)
|
||||
.reduce((acc: any, rlinks: any) => acc.concat(...rlinks), []);
|
||||
|
||||
links.push(...dzLinks);
|
||||
}
|
||||
|
||||
// Relations
|
||||
// If it's a regular relation, extract the links but do not recurse further
|
||||
if (attribute.type === 'relation') {
|
||||
const relationLinks = parseRelationLinks({ entity, fieldName: key, value, schema });
|
||||
|
||||
links.push(...relationLinks);
|
||||
}
|
||||
}
|
||||
|
||||
return links;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse links contained in a relational attribute
|
||||
*/
|
||||
export const parseRelationLinks = ({ entity, schema, fieldName, value }: any): ILink[] => {
|
||||
const attribute = schema.attributes[fieldName];
|
||||
|
||||
const { relation, target }: { relation: RelationsType; target: string } = attribute;
|
||||
|
||||
// Handle ToMany relations
|
||||
if (Array.isArray(value)) {
|
||||
return (
|
||||
value
|
||||
// Get links from value
|
||||
.map((item) => parseRelationLinks({ entity, schema, fieldName, value: item }))
|
||||
// Flatten the results, to make sure we're dealing with the right data structure
|
||||
.flat()
|
||||
// Update the pos with the relation index in the collection
|
||||
.map((link, i) => set<ILink>('left.pos', i, link))
|
||||
);
|
||||
}
|
||||
|
||||
const isMorphRelation = relation.startsWith('morph');
|
||||
const isCircularRelation = !isMorphRelation && target === schema.uid;
|
||||
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const kind: ILink['kind'] = isMorphRelation
|
||||
? // Polymorphic relations
|
||||
'relation.morph'
|
||||
: isCircularRelation
|
||||
? // Self referencing relations
|
||||
'relation.circular'
|
||||
: // Regular relations
|
||||
'relation.basic';
|
||||
|
||||
const link = linkBuilder(kind, relation)
|
||||
.left(schema.uid, entity.id, fieldName)
|
||||
.right(target, value.id, attribute.inversedBy).value;
|
||||
|
||||
return link ? [link] : [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a deep populate query for a given schema
|
||||
* It will populate first level for relations and media as well as
|
||||
* first-level relations for nested components and dynamic zones' components
|
||||
*/
|
||||
export const getDeepPopulateQuery = (schema: any, strapi: any) => {
|
||||
const populate: { [key: string]: any } = {};
|
||||
|
||||
for (const [key, attribute] of Object.entries<any>(schema.attributes)) {
|
||||
const setPopulateKey = (value: any) => {
|
||||
populate[key] = value;
|
||||
};
|
||||
|
||||
// Owning side of a relation
|
||||
if (attribute.type === 'relation' && !attribute.mappedBy) {
|
||||
setPopulateKey({ fields: ['id'] });
|
||||
}
|
||||
|
||||
// Media
|
||||
if (attribute.type === 'media') {
|
||||
setPopulateKey({ fields: ['id'] });
|
||||
}
|
||||
|
||||
// Dynamic zone (nested structure)
|
||||
if (attribute.type === 'dynamiczone') {
|
||||
const subPopulate: { [key: string]: any } = {};
|
||||
|
||||
for (const component of attribute.components) {
|
||||
const componentSchema = strapi.components[component];
|
||||
const componentPopulate = getDeepPopulateQuery(componentSchema, strapi);
|
||||
|
||||
// FIXME: Same problem as when trying to populate dynamic zones,
|
||||
// we don't have a way to discriminate components queries (which
|
||||
// can cause issue when components share same fields name)
|
||||
Object.assign(subPopulate, componentPopulate);
|
||||
}
|
||||
|
||||
if (!isEmpty(subPopulate)) {
|
||||
setPopulateKey({ fields: ['id'], populate: subPopulate });
|
||||
}
|
||||
}
|
||||
|
||||
// Component (nested structure)
|
||||
if (attribute.type === 'component') {
|
||||
const componentSchema = strapi.components[attribute.component];
|
||||
const componentPopulate = getDeepPopulateQuery(componentSchema, strapi);
|
||||
|
||||
if (!isEmpty(componentPopulate)) {
|
||||
setPopulateKey({ fields: ['id'], populate: componentPopulate });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return populate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Domain util to create a link
|
||||
* TODO: Move that to the domain layer when we'll update it
|
||||
*/
|
||||
export const linkBuilder = <T extends ILink = ILink>(kind: T['kind'], relation: RelationsType) => {
|
||||
const link: Partial<T> = {};
|
||||
|
||||
link.kind = kind;
|
||||
link.relation = relation;
|
||||
|
||||
return {
|
||||
left(type: string, ref: string | number, field: string, pos?: number) {
|
||||
link.left = { type, ref, field, pos };
|
||||
return this;
|
||||
},
|
||||
|
||||
right(type: string, ref: string | number, field?: string) {
|
||||
link.right = { type, ref, field };
|
||||
return this;
|
||||
},
|
||||
|
||||
get value() {
|
||||
return link.left && link.right ? (link as ILink) : null;
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -1 +1,2 @@
|
||||
export * as entity from './entity';
|
||||
export * as link from './link';
|
||||
|
||||
280
packages/core/data-transfer/lib/providers/shared/strapi/link.ts
Normal file
280
packages/core/data-transfer/lib/providers/shared/strapi/link.ts
Normal file
@ -0,0 +1,280 @@
|
||||
import { clone, isNil } from 'lodash/fp';
|
||||
import { ILink } from '../../../../types';
|
||||
|
||||
// TODO: Remove any types when we'll have types for DB metadata
|
||||
|
||||
export const createLinkQuery = (strapi: Strapi.Strapi) => {
|
||||
const query = () => {
|
||||
const { connection } = strapi.db;
|
||||
|
||||
async function* generateAllForAttribute(uid: string, fieldName: string): AsyncGenerator<ILink> {
|
||||
const metadata = strapi.db.metadata.get(uid);
|
||||
|
||||
if (!metadata) {
|
||||
throw new Error(`No metadata found for ${uid}`);
|
||||
}
|
||||
|
||||
const attributes = filterValidRelationalAttributes(metadata.attributes);
|
||||
|
||||
if (!(fieldName in attributes)) {
|
||||
throw new Error(`${fieldName} is not a valid relational attribute name`);
|
||||
}
|
||||
|
||||
const attribute = attributes[fieldName];
|
||||
|
||||
const kind = getLinkKind(attribute, uid);
|
||||
const { relation, target } = attribute;
|
||||
|
||||
// The relation is stored in the same table
|
||||
// TODO: handle manyToOne joinColumn
|
||||
if (attribute.joinColumn) {
|
||||
const joinColumnName: string = attribute.joinColumn.name;
|
||||
|
||||
const qb = connection.queryBuilder().select('id', joinColumnName).from(metadata.tableName);
|
||||
|
||||
// TODO: stream the query to improve performances
|
||||
const entries = await qb;
|
||||
|
||||
for (const entry of entries) {
|
||||
const ref = entry[joinColumnName];
|
||||
|
||||
if (ref !== null) {
|
||||
yield {
|
||||
kind,
|
||||
relation,
|
||||
left: { type: uid, ref: entry.id, field: fieldName },
|
||||
right: { type: target, ref },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The relation uses a join table
|
||||
if (attribute.joinTable) {
|
||||
const {
|
||||
name,
|
||||
joinColumn,
|
||||
inverseJoinColumn,
|
||||
orderColumnName,
|
||||
morphColumn,
|
||||
inverseOrderColumnName,
|
||||
} = attribute.joinTable;
|
||||
|
||||
const qb = connection.queryBuilder().from(name);
|
||||
|
||||
type Columns = {
|
||||
left: { ref: string | null; order?: string };
|
||||
right: { ref: string | null; order?: string; type?: string; field?: string };
|
||||
};
|
||||
|
||||
const columns: Columns = {
|
||||
left: { ref: null },
|
||||
right: { ref: null },
|
||||
};
|
||||
|
||||
const left: Partial<ILink['left']> = { type: uid, field: fieldName };
|
||||
const right: Partial<ILink['right']> = {};
|
||||
|
||||
if (kind === 'relation.basic' || kind === 'relation.circular') {
|
||||
right.type = attribute.target;
|
||||
right.field = attribute.inversedBy;
|
||||
|
||||
columns.left.ref = joinColumn.name;
|
||||
columns.right.ref = inverseJoinColumn.name;
|
||||
|
||||
if (orderColumnName) {
|
||||
columns.left.order = orderColumnName as string;
|
||||
}
|
||||
|
||||
if (inverseOrderColumnName) {
|
||||
columns.right.order = inverseOrderColumnName as string;
|
||||
}
|
||||
}
|
||||
|
||||
if (kind === 'relation.morph') {
|
||||
columns.left.ref = joinColumn.name;
|
||||
|
||||
columns.right.ref = morphColumn.idColumn.name;
|
||||
columns.right.type = morphColumn.typeColumn.name;
|
||||
columns.right.field = 'field';
|
||||
columns.right.order = 'order';
|
||||
}
|
||||
|
||||
const validColumns = [
|
||||
// Left
|
||||
columns.left.ref,
|
||||
columns.left.order,
|
||||
// Right
|
||||
columns.right.ref,
|
||||
columns.right.type,
|
||||
columns.right.field,
|
||||
columns.right.order,
|
||||
].filter((column: string | null | undefined) => !isNil(column));
|
||||
|
||||
qb.select(validColumns);
|
||||
|
||||
// TODO: stream the query to improve performances
|
||||
const entries = await qb;
|
||||
|
||||
for (const entry of entries) {
|
||||
if (columns.left.ref) {
|
||||
left.ref = entry[columns.left.ref];
|
||||
}
|
||||
|
||||
if (columns.right.ref) {
|
||||
right.ref = entry[columns.right.ref];
|
||||
}
|
||||
|
||||
if (columns.left.order) {
|
||||
left.pos = entry[columns.left.order as string];
|
||||
}
|
||||
|
||||
if (columns.right.order) {
|
||||
right.pos = entry[columns.right.order as string];
|
||||
}
|
||||
|
||||
if (columns.right.type) {
|
||||
right.type = entry[columns.right.type as string];
|
||||
}
|
||||
|
||||
if (columns.right.field) {
|
||||
right.field = entry[columns.right.field as string];
|
||||
}
|
||||
|
||||
const link: ILink = {
|
||||
kind,
|
||||
relation,
|
||||
left: clone(left as ILink['left']),
|
||||
right: clone(right as ILink['right']),
|
||||
};
|
||||
|
||||
yield link;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function* generateAll(uid: string): AsyncGenerator<ILink> {
|
||||
const metadata = strapi.db.metadata.get(uid);
|
||||
|
||||
if (!metadata) {
|
||||
throw new Error(`No metadata found for ${uid}`);
|
||||
}
|
||||
|
||||
const attributes = filterValidRelationalAttributes(metadata.attributes);
|
||||
|
||||
for (const fieldName of Object.keys(attributes)) {
|
||||
for await (const link of generateAllForAttribute(uid, fieldName)) {
|
||||
yield link;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const insert = async (link: ILink) => {
|
||||
const { kind, left, right } = link;
|
||||
|
||||
const metadata = strapi.db.metadata.get(left.type);
|
||||
const attribute = metadata.attributes[left.field];
|
||||
|
||||
const payload = {};
|
||||
|
||||
if (attribute.joinColumn) {
|
||||
const joinColumnName = attribute.joinColumn.name;
|
||||
|
||||
await connection(metadata.tableName)
|
||||
.where('id', left.ref)
|
||||
.update({ [joinColumnName]: right.ref });
|
||||
}
|
||||
|
||||
if (attribute.joinTable) {
|
||||
const {
|
||||
name,
|
||||
joinColumn,
|
||||
inverseJoinColumn,
|
||||
orderColumnName,
|
||||
inverseOrderColumnName,
|
||||
morphColumn,
|
||||
} = attribute.joinTable;
|
||||
|
||||
if (joinColumn) {
|
||||
Object.assign(payload, { [joinColumn.name]: left.ref });
|
||||
}
|
||||
|
||||
const assignInverseColumn = () => {
|
||||
if (inverseJoinColumn) {
|
||||
Object.assign(payload, {
|
||||
[inverseJoinColumn.name]: right.ref,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const assignOrderColumns = () => {
|
||||
if (orderColumnName) {
|
||||
Object.assign(payload, { [orderColumnName]: left.pos ?? null });
|
||||
}
|
||||
|
||||
if (inverseOrderColumnName) {
|
||||
Object.assign(payload, { [inverseOrderColumnName]: right.pos ?? null });
|
||||
}
|
||||
};
|
||||
|
||||
const assignMorphColumns = () => {
|
||||
const { idColumn, typeColumn } = morphColumn ?? {};
|
||||
|
||||
if (idColumn) {
|
||||
Object.assign(payload, { [idColumn.name]: right.ref });
|
||||
}
|
||||
|
||||
if (typeColumn) {
|
||||
Object.assign(payload, { [typeColumn.name]: right.type });
|
||||
}
|
||||
|
||||
Object.assign(payload, { order: right.pos ?? null, field: right.field ?? null });
|
||||
};
|
||||
|
||||
if (kind === 'relation.basic' || kind === 'relation.circular') {
|
||||
assignInverseColumn();
|
||||
}
|
||||
|
||||
if (kind === 'relation.morph') {
|
||||
assignMorphColumns();
|
||||
}
|
||||
|
||||
assignOrderColumns();
|
||||
|
||||
await connection.insert(payload).into(name);
|
||||
}
|
||||
};
|
||||
|
||||
return { generateAll, generateAllForAttribute, insert };
|
||||
};
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
const filterValidRelationalAttributes = (attributes: Record<string, any>) => {
|
||||
const isOwner = (attribute: any) => {
|
||||
return attribute.owner || (!attribute.mappedBy && !attribute.morphBy);
|
||||
};
|
||||
|
||||
const isComponentLike = (attribute: any) => {
|
||||
return attribute.component || attribute.components;
|
||||
};
|
||||
|
||||
return Object.entries(attributes)
|
||||
.filter(([, attribute]) => {
|
||||
return attribute.type === 'relation' && isOwner(attribute) && !isComponentLike(attribute);
|
||||
})
|
||||
.reduce<Record<string, any>>((acc, [key, attribute]) => ({ ...acc, [key]: attribute }), {});
|
||||
};
|
||||
|
||||
const getLinkKind = (attribute: any, uid: string): ILink['kind'] => {
|
||||
if (attribute.relation.startsWith('morph')) {
|
||||
return 'relation.morph';
|
||||
}
|
||||
|
||||
if (attribute.target === uid) {
|
||||
return 'relation.circular';
|
||||
}
|
||||
|
||||
return 'relation.basic';
|
||||
};
|
||||
@ -70,7 +70,7 @@ interface IDefaultLink {
|
||||
/**
|
||||
* Reference ID of the entity
|
||||
*/
|
||||
ref: number | string;
|
||||
ref: number;
|
||||
/**
|
||||
* Field used to hold the link in the entity
|
||||
*/
|
||||
@ -94,11 +94,15 @@ interface IDefaultLink {
|
||||
/**
|
||||
* Reference ID of the entity
|
||||
*/
|
||||
ref: number | string;
|
||||
ref: number;
|
||||
/**
|
||||
* Field used to hold the link in the entity
|
||||
*/
|
||||
field?: string;
|
||||
/**
|
||||
* If the link is part of a collection, keep its position here
|
||||
*/
|
||||
pos?: number;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
3
packages/core/database/lib/index.d.ts
vendored
3
packages/core/database/lib/index.d.ts
vendored
@ -1,3 +1,4 @@
|
||||
import { Knex } from 'knex';
|
||||
import { LifecycleProvider } from './lifecycles';
|
||||
import { MigrationProvider } from './migrations';
|
||||
import { SchemaProvider } from './schema';
|
||||
@ -158,6 +159,8 @@ export interface Database {
|
||||
migrations: MigrationProvider;
|
||||
entityManager: EntityManager;
|
||||
queryBuilder: any;
|
||||
metadata: any;
|
||||
connection: Knex;
|
||||
|
||||
query<T extends keyof AllTypes>(uid: T): QueryFromContentType<T>;
|
||||
}
|
||||
|
||||
@ -55,6 +55,16 @@ module.exports = async (opts) => {
|
||||
strategy: 'restore', // for an export to file, strategy will always be 'restore'
|
||||
versionMatching: 'ignore', // for an export to file, versionMatching will always be skipped
|
||||
transforms: {
|
||||
links: [
|
||||
{
|
||||
filter(link) {
|
||||
return (
|
||||
!DEFAULT_IGNORED_CONTENT_TYPES.includes(link.left.type) &&
|
||||
!DEFAULT_IGNORED_CONTENT_TYPES.includes(link.right.type)
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
entities: [
|
||||
{
|
||||
filter(entity) {
|
||||
|
||||
@ -57,6 +57,16 @@ module.exports = async (opts) => {
|
||||
versionMatching: opts.schemaComparison,
|
||||
exclude: opts.exclude,
|
||||
rules: {
|
||||
links: [
|
||||
{
|
||||
filter(link) {
|
||||
return (
|
||||
!DEFAULT_IGNORED_CONTENT_TYPES.includes(link.left.type) &&
|
||||
!DEFAULT_IGNORED_CONTENT_TYPES.includes(link.right.type)
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
entities: [
|
||||
{
|
||||
filter: (entity) => !DEFAULT_IGNORED_CONTENT_TYPES.includes(entity.type),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user