mirror of
https://github.com/strapi/strapi.git
synced 2025-11-12 08:08:05 +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}"`);
|
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 = (
|
export const createLocalStrapiDestinationProvider = (
|
||||||
|
|||||||
@ -130,3 +130,4 @@ const useResults = (
|
|||||||
|
|
||||||
export * from './entities';
|
export * from './entities';
|
||||||
export * from './configuration';
|
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 { Readable } from 'stream';
|
||||||
|
|
||||||
import { createLinksStream } from '../links';
|
import { createLinksStream } from '../links';
|
||||||
import { getDeepPopulateQuery } from '../links/utils';
|
|
||||||
import { collect, getStrapiFactory } from '../../../__tests__/test-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', () => {
|
describe('Create Links Stream', () => {
|
||||||
test('should return an empty stream if no content types ', async () => {
|
test('should return an empty stream if no content types ', async () => {
|
||||||
const strapi = getStrapiFactory({
|
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 { Readable } from 'stream';
|
||||||
import { set } from 'lodash/fp';
|
import { set } from 'lodash/fp';
|
||||||
|
|
||||||
|
import type { IConfiguration } from '../../../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a readable stream that export the Strapi app configuration
|
* Create a readable stream that export the Strapi app configuration
|
||||||
*/
|
*/
|
||||||
@ -23,7 +25,7 @@ export const createConfigurationStream = (strapi: Strapi.Strapi): Readable => {
|
|||||||
|
|
||||||
// Readable configuration stream
|
// Readable configuration stream
|
||||||
return Readable.from(
|
return Readable.from(
|
||||||
(async function* () {
|
(async function* configurationGenerator(): AsyncGenerator<IConfiguration> {
|
||||||
for (const stream of streams) {
|
for (const stream of streams) {
|
||||||
for await (const item of stream) {
|
for await (const item of stream) {
|
||||||
yield item;
|
yield item;
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type { ContentTypeSchema } from '@strapi/strapi';
|
|||||||
|
|
||||||
import { isObject, isArray, isEmpty, size } from 'lodash/fp';
|
import { isObject, isArray, isEmpty, size } from 'lodash/fp';
|
||||||
import { Readable, PassThrough } from 'stream';
|
import { Readable, PassThrough } from 'stream';
|
||||||
|
import { IEntity } from '../../../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate and consume content-types streams in order to stream each entity individually
|
* 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(
|
return Readable.from(
|
||||||
(async function* () {
|
(async function* entitiesGenerator(): AsyncGenerator<{
|
||||||
|
entity: IEntity;
|
||||||
|
contentType: ContentTypeSchema;
|
||||||
|
}> {
|
||||||
for await (const { stream, contentType } of contentTypeStreamGenerator()) {
|
for await (const { stream, contentType } of contentTypeStreamGenerator()) {
|
||||||
for await (const entity of stream) {
|
for await (const entity of stream) {
|
||||||
yield { entity, contentType };
|
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 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
|
* Reference ID of the entity
|
||||||
*/
|
*/
|
||||||
ref: number | string;
|
ref: number;
|
||||||
/**
|
/**
|
||||||
* Field used to hold the link in the entity
|
* Field used to hold the link in the entity
|
||||||
*/
|
*/
|
||||||
@ -94,11 +94,15 @@ interface IDefaultLink {
|
|||||||
/**
|
/**
|
||||||
* Reference ID of the entity
|
* Reference ID of the entity
|
||||||
*/
|
*/
|
||||||
ref: number | string;
|
ref: number;
|
||||||
/**
|
/**
|
||||||
* Field used to hold the link in the entity
|
* Field used to hold the link in the entity
|
||||||
*/
|
*/
|
||||||
field?: string;
|
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 { LifecycleProvider } from './lifecycles';
|
||||||
import { MigrationProvider } from './migrations';
|
import { MigrationProvider } from './migrations';
|
||||||
import { SchemaProvider } from './schema';
|
import { SchemaProvider } from './schema';
|
||||||
@ -158,6 +159,8 @@ export interface Database {
|
|||||||
migrations: MigrationProvider;
|
migrations: MigrationProvider;
|
||||||
entityManager: EntityManager;
|
entityManager: EntityManager;
|
||||||
queryBuilder: any;
|
queryBuilder: any;
|
||||||
|
metadata: any;
|
||||||
|
connection: Knex;
|
||||||
|
|
||||||
query<T extends keyof AllTypes>(uid: T): QueryFromContentType<T>;
|
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'
|
strategy: 'restore', // for an export to file, strategy will always be 'restore'
|
||||||
versionMatching: 'ignore', // for an export to file, versionMatching will always be skipped
|
versionMatching: 'ignore', // for an export to file, versionMatching will always be skipped
|
||||||
transforms: {
|
transforms: {
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
filter(link) {
|
||||||
|
return (
|
||||||
|
!DEFAULT_IGNORED_CONTENT_TYPES.includes(link.left.type) &&
|
||||||
|
!DEFAULT_IGNORED_CONTENT_TYPES.includes(link.right.type)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
entities: [
|
entities: [
|
||||||
{
|
{
|
||||||
filter(entity) {
|
filter(entity) {
|
||||||
|
|||||||
@ -57,6 +57,16 @@ module.exports = async (opts) => {
|
|||||||
versionMatching: opts.schemaComparison,
|
versionMatching: opts.schemaComparison,
|
||||||
exclude: opts.exclude,
|
exclude: opts.exclude,
|
||||||
rules: {
|
rules: {
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
filter(link) {
|
||||||
|
return (
|
||||||
|
!DEFAULT_IGNORED_CONTENT_TYPES.includes(link.left.type) &&
|
||||||
|
!DEFAULT_IGNORED_CONTENT_TYPES.includes(link.right.type)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
entities: [
|
entities: [
|
||||||
{
|
{
|
||||||
filter: (entity) => !DEFAULT_IGNORED_CONTENT_TYPES.includes(entity.type),
|
filter: (entity) => !DEFAULT_IGNORED_CONTENT_TYPES.includes(entity.type),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user