Entity service tests

This commit is contained in:
Alexandre Bodin 2023-09-05 22:10:20 +02:00
parent c4c570fe3b
commit cc4b360d47
10 changed files with 171 additions and 114 deletions

View File

@ -53,7 +53,7 @@ import convertCustomFieldType from './utils/convert-custom-field-type';
// TODO: move somewhere else // TODO: move somewhere else
import * as draftAndPublishSync from './migrations/draft-publish'; import * as draftAndPublishSync from './migrations/draft-publish';
import type { Common, Shared } from './types'; import type { Common, Schema, Shared } from './types';
/** /**
* Resolve the working directories based on the instance options. * Resolve the working directories based on the instance options.
@ -635,7 +635,11 @@ class Strapi {
} }
} }
getModel(uid: string) { getModel(uid: Common.UID.ContentType): Schema.ContentType;
getModel(uid: Common.UID.Component): Schema.Component;
getModel(
uid: Common.UID.Component | Common.UID.ContentType
): Schema.Component | Schema.ContentType {
return ( return (
this.contentTypes[uid as Common.UID.ContentType] || this.contentTypes[uid as Common.UID.ContentType] ||
this.components[uid as Common.UID.Component] this.components[uid as Common.UID.Component]

View File

@ -1,12 +1,10 @@
'use strict'; import createEntityService from '..';
import entityValidator from '../../entity-validator';
const createEntityService = require('..');
const entityValidator = require('../../entity-validator');
describe('Entity service triggers webhooks', () => { describe('Entity service triggers webhooks', () => {
let instance; let instance: any;
const eventHub = { emit: jest.fn() }; const eventHub = { emit: jest.fn() };
let entity = { attr: 'value' }; let entity: unknown = { attr: 'value' };
beforeAll(() => { beforeAll(() => {
const model = { const model = {
@ -25,11 +23,11 @@ describe('Entity service triggers webhooks', () => {
}, },
}, },
db: { db: {
transaction: (cb) => cb(), transaction: (cb: any) => cb(),
query: () => ({ query: () => ({
count: () => 0, count: () => 0,
create: ({ data }) => data, create: ({ data }: any) => data,
update: ({ data }) => data, update: ({ data }: any) => data,
findOne: () => entity, findOne: () => entity,
findMany: () => [entity, entity], findMany: () => [entity, entity],
delete: () => ({}), delete: () => ({}),
@ -38,16 +36,16 @@ describe('Entity service triggers webhooks', () => {
}, },
eventHub, eventHub,
entityValidator, entityValidator,
}); } as any);
global.strapi = { global.strapi = {
getModel: () => model, getModel: () => model,
}; } as any;
}); });
test('Emit event: Create', async () => { test('Emit event: Create', async () => {
// Create entity // Create entity
await instance.create('test-model', { data: entity }); await instance.create('api::test.test-model', { data: entity });
// Expect entry.create event to be emitted // Expect entry.create event to be emitted
expect(eventHub.emit).toHaveBeenCalledWith('entry.create', { expect(eventHub.emit).toHaveBeenCalledWith('entry.create', {
@ -61,7 +59,7 @@ describe('Entity service triggers webhooks', () => {
test('Emit event: Update', async () => { test('Emit event: Update', async () => {
// Update entity // Update entity
await instance.update('test-model', 'entity-id', { data: entity }); await instance.update('api::test.test-model', 'entity-id', { data: entity });
// Expect entry.update event to be emitted // Expect entry.update event to be emitted
expect(eventHub.emit).toHaveBeenCalledWith('entry.update', { expect(eventHub.emit).toHaveBeenCalledWith('entry.update', {
@ -75,7 +73,7 @@ describe('Entity service triggers webhooks', () => {
test('Emit event: Delete', async () => { test('Emit event: Delete', async () => {
// Delete entity // Delete entity
await instance.delete('test-model', 'entity-id', {}); await instance.delete('api::test.test-model', 'entity-id', {});
// Expect entry.create event to be emitted // Expect entry.create event to be emitted
expect(eventHub.emit).toHaveBeenCalledWith('entry.delete', { expect(eventHub.emit).toHaveBeenCalledWith('entry.delete', {
@ -89,7 +87,7 @@ describe('Entity service triggers webhooks', () => {
test('Emit event: Delete Many', async () => { test('Emit event: Delete Many', async () => {
// Delete entity // Delete entity
await instance.deleteMany('test-model', {}); await instance.deleteMany('api::test.test-model', {});
// Expect entry.create event to be emitted // Expect entry.create event to be emitted
expect(eventHub.emit).toHaveBeenCalledWith('entry.delete', { expect(eventHub.emit).toHaveBeenCalledWith('entry.delete', {
@ -106,7 +104,7 @@ describe('Entity service triggers webhooks', () => {
test('Do not emit event when no deleted entity', async () => { test('Do not emit event when no deleted entity', async () => {
entity = null; entity = null;
// Delete non existent entity // Delete non existent entity
await instance.delete('test-model', 'entity-id', {}); await instance.delete('api::test.test-model', 'entity-id', {});
// Expect entry.create event to be emitted // Expect entry.create event to be emitted
expect(eventHub.emit).toHaveBeenCalledTimes(0); expect(eventHub.emit).toHaveBeenCalledTimes(0);

View File

@ -1,15 +1,18 @@
'use strict'; import { EventEmitter } from 'events';
import { errors } from '@strapi/utils';
import createEntityService from '..';
import entityValidator from '../../entity-validator';
import createEventHub from '../../event-hub';
import type { Schema, Utils } from '../../../types';
import uploadFiles from '../../utils/upload-files';
jest.mock('bcryptjs', () => ({ hashSync: () => 'secret-password' })); jest.mock('bcryptjs', () => ({ hashSync: () => 'secret-password' }));
const { EventEmitter } = require('events');
const { ValidationError } = require('@strapi/utils').errors;
const createEntityService = require('..');
const entityValidator = require('../../entity-validator');
jest.mock('../../utils/upload-files', () => jest.fn(() => Promise.resolve())); jest.mock('../../utils/upload-files', () => jest.fn(() => Promise.resolve()));
describe('Entity service', () => { describe('Entity service', () => {
const eventHub = createEventHub();
global.strapi = { global.strapi = {
getModel: jest.fn(() => ({})), getModel: jest.fn(() => ({})),
config: { config: {
@ -22,27 +25,28 @@ describe('Entity service', () => {
allowedEvents: new Map([['ENTRY_CREATE', 'entry.create']]), allowedEvents: new Map([['ENTRY_CREATE', 'entry.create']]),
addAllowedEvent: jest.fn(), addAllowedEvent: jest.fn(),
}, },
}; } as any;
describe('Decorator', () => { describe('Decorator', () => {
test.each(['create', 'update', 'findMany', 'findOne', 'delete', 'count', 'findPage'])( test.each(['create', 'update', 'findMany', 'findOne', 'delete', 'count', 'findPage'] as const)(
'Can decorate', 'Can decorate',
async (method) => { async (method) => {
const instance = createEntityService({ const instance = createEntityService({
strapi: global.strapi, strapi: global.strapi,
db: {}, db: {} as any,
eventHub: new EventEmitter(), eventHub,
entityValidator,
}); });
const methodFn = jest.fn(); const methodFn = jest.fn();
const decorator = () => ({
[method]: methodFn,
});
instance.decorate(decorator); instance.decorate((old) => ({
...old,
[method]: methodFn,
}));
const args = [{}, {}]; const args = [{}, {}];
await instance[method](...args); await (instance[method] as Utils.Function.Any)(...args);
expect(methodFn).toHaveBeenCalled(); expect(methodFn).toHaveBeenCalled();
} }
); );
@ -61,28 +65,30 @@ describe('Entity service', () => {
const fakeDB = { const fakeDB = {
query: jest.fn(() => fakeQuery), query: jest.fn(() => fakeQuery),
transaction: (cb) => cb(), transaction: (cb: Utils.Function.Any) => cb(),
}; };
const fakeStrapi = { const fakeStrapi = {
...global.strapi, ...global.strapi,
query: fakeQuery,
getModel: jest.fn(() => { getModel: jest.fn(() => {
return { kind: 'singleType' }; return { kind: 'singleType' };
}), }),
}; };
const instance = createEntityService({ const instance = createEntityService({
strapi: fakeStrapi, strapi: fakeStrapi as any,
db: fakeDB, db: fakeDB as any,
eventHub: new EventEmitter(), eventHub,
entityValidator,
}); });
const result = await instance.findMany('test-model'); const result = await instance.findMany('api::test.test-model');
expect(fakeStrapi.getModel).toHaveBeenCalledTimes(1); expect(fakeStrapi.getModel).toHaveBeenCalledTimes(1);
expect(fakeStrapi.getModel).toHaveBeenCalledWith('test-model'); expect(fakeStrapi.getModel).toHaveBeenCalledWith('api::test.test-model');
expect(fakeDB.query).toHaveBeenCalledWith('test-model'); expect(fakeDB.query).toHaveBeenCalledWith('api::test.test-model');
expect(fakeQuery.findOne).toHaveBeenCalledWith({}); expect(fakeQuery.findOne).toHaveBeenCalledWith({});
expect(result).toEqual(data); expect(result).toEqual(data);
}); });
@ -97,11 +103,14 @@ describe('Entity service', () => {
})), })),
findOne: jest.fn(), findOne: jest.fn(),
}; };
const fakeModels = {}; const fakeModels: Record<string, Schema.ContentType | Schema.Component> = {};
beforeAll(() => { beforeAll(() => {
global.strapi.getModel.mockImplementation((modelName) => fakeModels[modelName]); jest
global.strapi.query.mockImplementation(() => fakeQuery); .mocked(global.strapi.getModel)
.mockImplementation((modelName: string) => fakeModels[modelName] as any);
jest.mocked(global.strapi.query).mockImplementation(() => fakeQuery as any);
}); });
beforeEach(() => { beforeEach(() => {
@ -109,16 +118,16 @@ describe('Entity service', () => {
}); });
afterAll(() => { afterAll(() => {
global.strapi.getModel.mockImplementation(() => ({})); jest.mocked(global.strapi.getModel).mockImplementation(() => ({} as any));
}); });
describe('assign default values', () => { describe('assign default values', () => {
let instance; let instance: any;
const entityUID = 'api::entity.entity'; const entityUID = 'api::entity.entity';
const relationUID = 'api::relation.relation'; const relationUID = 'api::relation.relation';
beforeAll(() => { beforeAll(() => {
const fakeEntities = { const fakeEntities: Record<string, Record<string, unknown>> = {
[relationUID]: { [relationUID]: {
1: { 1: {
id: 1, id: 1,
@ -138,9 +147,16 @@ describe('Entity service', () => {
}; };
fakeModels[entityUID] = { fakeModels[entityUID] = {
modelType: 'contentType',
uid: entityUID, uid: entityUID,
kind: 'contentType', kind: 'collectionType',
modelName: 'test-model', modelName: 'test-model',
globalId: 'test-model',
info: {
singularName: 'entity',
pluralName: 'entities',
displayName: 'ENTITY',
},
options: {}, options: {},
attributes: { attributes: {
attrStringDefaultRequired: { attrStringDefaultRequired: {
@ -173,9 +189,17 @@ describe('Entity service', () => {
}, },
}, },
}; };
fakeModels[relationUID] = { fakeModels[relationUID] = {
uid: relationUID, uid: relationUID,
kind: 'contentType', modelType: 'contentType',
globalId: 'relation',
info: {
displayName: 'RELATION',
singularName: 'relation',
pluralName: 'relations',
},
kind: 'collectionType',
modelName: 'relation', modelName: 'relation',
attributes: { attributes: {
Name: { Name: {
@ -185,32 +209,43 @@ describe('Entity service', () => {
}, },
}, },
}; };
const fakeQuery = (uid) => ({ const fakeQuery = (uid: string) =>
({
create: jest.fn(({ data }) => data), create: jest.fn(({ data }) => data),
count: jest.fn(({ where }) => { count: jest.fn(({ where }) => {
return where.id.$in.filter((id) => Boolean(fakeEntities[uid][id])).length; return where.id.$in.filter((id: string) => Boolean(fakeEntities[uid][id])).length;
}), }),
}); } as any);
const fakeDB = { const fakeDB = {
transaction: (cb) => cb(), transaction: (cb: Utils.Function.Any) => cb(),
query: jest.fn((uid) => fakeQuery(uid)), query: jest.fn((uid) => fakeQuery(uid)),
}; } as any;
global.strapi.db = fakeDB; global.strapi = {
...global.strapi,
db: fakeDB,
query: jest.fn((uid) => fakeQuery(uid)),
} as any;
instance = createEntityService({ instance = createEntityService({
strapi: global.strapi, strapi: global.strapi,
db: fakeDB, db: fakeDB,
eventHub: new EventEmitter(), eventHub,
entityValidator, entityValidator,
}); });
}); });
afterAll(() => { afterAll(() => {
global.strapi.db = { global.strapi = {
...global.strapi,
db: {
query: jest.fn(() => fakeQuery), query: jest.fn(() => fakeQuery),
}; },
query: jest.fn(() => fakeQuery),
} as any;
}); });
test('should create record with all default attributes', async () => { test('should create record with all default attributes', async () => {
const data = {}; const data = {};
@ -315,7 +350,7 @@ describe('Entity service', () => {
const res = instance.create(entityUID, { data }); const res = instance.create(entityUID, { data });
await expect(res).rejects.toThrowError( await expect(res).rejects.toThrowError(
new ValidationError( new errors.ValidationError(
`1 relation(s) of type api::relation.relation associated with this entity do not exist` `1 relation(s) of type api::relation.relation associated with this entity do not exist`
) )
); );
@ -323,19 +358,24 @@ describe('Entity service', () => {
}); });
describe('with files', () => { describe('with files', () => {
let instance; let instance: any;
beforeAll(() => { beforeAll(() => {
fakeModels['test-model'] = { fakeModels['api::test.test-model'] = {
uid: 'test-model', uid: 'api::test.test-model',
kind: 'collectionType', kind: 'collectionType',
collectionName: 'test-model', collectionName: 'test-model',
info: {
displayName: 'test-model',
singularName: 'test-model',
pluralName: 'test-models',
},
options: {}, options: {},
attributes: { attributes: {
name: { name: {
type: 'string', type: 'string',
}, },
activity: { activity: {
displayName: 'activity',
type: 'component', type: 'component',
repeatable: true, repeatable: true,
component: 'basic.activity', component: 'basic.activity',
@ -343,12 +383,11 @@ describe('Entity service', () => {
}, },
modelType: 'contentType', modelType: 'contentType',
modelName: 'test-model', modelName: 'test-model',
globalId: 'test-model',
}; };
fakeModels['basic.activity'] = { fakeModels['basic.activity'] = {
collectionName: 'components_basic_activities', collectionName: 'components_basic_activities',
info: {
displayName: 'activity',
},
options: {}, options: {},
attributes: { attributes: {
docs: { docs: {
@ -375,6 +414,7 @@ describe('Entity service', () => {
getModel: jest.fn((modelName) => fakeModels[modelName]), getModel: jest.fn((modelName) => fakeModels[modelName]),
query: jest.fn(() => fakeQuery), query: jest.fn(() => fakeQuery),
db: { db: {
...fakeDB,
dialect: { dialect: {
client: 'sqlite', client: 'sqlite',
}, },
@ -384,17 +424,17 @@ describe('Entity service', () => {
global.strapi = { global.strapi = {
...global.strapi, ...global.strapi,
...fakeStrapi, ...fakeStrapi,
}; } as any;
instance = createEntityService({ instance = createEntityService({
strapi: global.strapi, strapi: global.strapi,
db: fakeDB, db: fakeDB,
eventHub: new EventEmitter(), eventHub,
entityValidator, entityValidator,
} as any);
}); });
});
test('should create record with attached files', async () => { test.only('should create record with attached files', async () => {
const uploadFiles = require('../../utils/upload-files');
const data = { const data = {
name: 'demoEvent', name: 'demoEvent',
activity: [{ name: 'Powering the Aviation of the Future' }], activity: [{ name: 'Powering the Aviation of the Future' }],
@ -411,13 +451,13 @@ describe('Entity service', () => {
fakeQuery.findOne.mockResolvedValue({ id: 1, ...data }); fakeQuery.findOne.mockResolvedValue({ id: 1, ...data });
await instance.create('test-model', { data, files }); await instance.create('api::test.test-model', { data, files });
expect(global.strapi.getModel).toBeCalled(); expect(global.strapi.getModel).toBeCalled();
expect(uploadFiles).toBeCalled(); expect(uploadFiles).toBeCalled();
expect(uploadFiles).toBeCalledTimes(1); expect(uploadFiles).toBeCalledTimes(1);
expect(uploadFiles).toBeCalledWith( expect(uploadFiles).toBeCalledWith(
'test-model', 'api::test.test-model',
{ {
id: 1, id: 1,
name: 'demoEvent', name: 'demoEvent',
@ -439,12 +479,12 @@ describe('Entity service', () => {
describe('Update', () => { describe('Update', () => {
describe('assign default values', () => { describe('assign default values', () => {
let instance; let instance: any;
const entityUID = 'api::entity.entity'; const entityUID = 'api::entity.entity';
const relationUID = 'api::relation.relation'; const relationUID = 'api::relation.relation';
const fakeEntities = { const fakeEntities: Record<string, Record<string, any>> = {
[entityUID]: { [entityUID]: {
0: { 0: {
id: 0, id: 0,
@ -471,10 +511,12 @@ describe('Entity service', () => {
}, },
}, },
}; };
const fakeModels = { const fakeModels: Record<string, Schema.ContentType> = {
[entityUID]: { [entityUID]: {
kind: 'collectionType', kind: 'collectionType',
modelName: 'entity', modelName: 'entity',
globalId: 'entity',
modelType: 'contentType',
collectionName: 'entity', collectionName: 'entity',
uid: entityUID, uid: entityUID,
options: {}, options: {},
@ -496,8 +538,16 @@ describe('Entity service', () => {
}, },
}, },
[relationUID]: { [relationUID]: {
kind: 'contentType', kind: 'collectionType',
globalId: 'entity',
modelType: 'contentType',
modelName: 'relation', modelName: 'relation',
uid: relationUID,
info: {
singularName: 'relation',
pluralName: 'relations',
displayName: 'RELATION',
},
attributes: { attributes: {
Name: { Name: {
type: 'string', type: 'string',
@ -509,11 +559,11 @@ describe('Entity service', () => {
}; };
beforeAll(() => { beforeAll(() => {
const fakeQuery = (key) => ({ const fakeQuery = (key: string) => ({
findOne: jest.fn(({ where }) => fakeEntities[key][where.id]), findOne: jest.fn(({ where }) => fakeEntities[key][where.id]),
count: jest.fn(({ where }) => { count: jest.fn(({ where }) => {
let ret = 0; let ret = 0;
where.id.$in.forEach((id) => { where.id.$in.forEach((id: string) => {
const entity = fakeEntities[key][id]; const entity = fakeEntities[key][id];
if (!entity) return; if (!entity) return;
ret += 1; ret += 1;
@ -534,18 +584,19 @@ describe('Entity service', () => {
global.strapi = { global.strapi = {
...global.strapi, ...global.strapi,
getModel: jest.fn((uid) => { getModel: jest.fn((uid: string) => {
return fakeModels[uid]; return fakeModels[uid];
}), }),
query: jest.fn((key) => fakeQuery(key)),
db: fakeDB, db: fakeDB,
}; } as any;
instance = createEntityService({ instance = createEntityService({
strapi: global.strapi, strapi: global.strapi,
db: fakeDB, db: fakeDB,
eventHub: new EventEmitter(), eventHub: new EventEmitter(),
entityValidator, entityValidator,
}); } as any);
}); });
test(`should fail if the entity doesn't exist`, async () => { test(`should fail if the entity doesn't exist`, async () => {
@ -587,7 +638,7 @@ describe('Entity service', () => {
const res = instance.update(entityUID, 0, { data }); const res = instance.update(entityUID, 0, { data });
await expect(res).rejects.toThrowError( await expect(res).rejects.toThrowError(
new ValidationError( new errors.ValidationError(
`1 relation(s) of type api::relation.relation associated with this entity do not exist` `1 relation(s) of type api::relation.relation associated with this entity do not exist`
) )
); );

View File

@ -34,6 +34,14 @@ export * from './types';
const { transformParamsToQuery } = convertQueryParams; const { transformParamsToQuery } = convertQueryParams;
type Decoratable<T> = T & {
decorate(
decorator: (old: Types.EntityService) => Types.EntityService & {
[key: string]: unknown;
}
): void;
};
type Context = { type Context = {
contentType: Schema.ContentType; contentType: Schema.ContentType;
}; };
@ -89,11 +97,11 @@ const createDefaultImplementation = ({
*/ */
uploadFiles, uploadFiles,
async wrapParams(options: any) { async wrapParams(options: any = {}) {
return options; return options;
}, },
async wrapResult(result: any) { async wrapResult(result: any = {}) {
return result; return result;
}, },
@ -237,10 +245,6 @@ const createDefaultImplementation = ({
}); });
const { data, files } = wrappedParams; const { data, files } = wrappedParams;
if (!data) {
throw new Error('cannot update');
}
const model = strapi.getModel(uid); const model = strapi.getModel(uid);
const entityToUpdate = await db.query(uid).findOne({ where: { id: entityId } }); const entityToUpdate = await db.query(uid).findOne({ where: { id: entityId } });
@ -455,7 +459,7 @@ export default (ctx: {
db: Database; db: Database;
eventHub: EventHub; eventHub: EventHub;
entityValidator: EntityValidator; entityValidator: EntityValidator;
}): Types.EntityService => { }): Decoratable<Types.EntityService> => {
Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => { Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
ctx.strapi.webhookStore?.addAllowedEvent(key, value); ctx.strapi.webhookStore?.addAllowedEvent(key, value);
}); });
@ -507,5 +511,5 @@ export default (ctx: {
return newService; return newService;
}); });
return service as unknown as Types.EntityService; return service as unknown as Decoratable<Types.EntityService>;
}; };

View File

@ -7,7 +7,7 @@ import { uniqBy, castArray, isNil, isArray, mergeWith } from 'lodash';
import { has, prop, isObject, isEmpty } from 'lodash/fp'; import { has, prop, isObject, isEmpty } from 'lodash/fp';
import strapiUtils from '@strapi/utils'; import strapiUtils from '@strapi/utils';
import validators from './validators'; import validators from './validators';
import { Common, Schema, Attribute, UID, Shared } from '../../types'; import { Common, Schema, Attribute, Shared } from '../../types';
import type * as Types from '../entity-service/types'; import type * as Types from '../entity-service/types';
type CreateOrUpdate = 'creation' | 'update'; type CreateOrUpdate = 'creation' | 'update';
@ -37,12 +37,12 @@ interface ValidatorContext {
interface AttributeValidatorMetas { interface AttributeValidatorMetas {
attr: Attribute.Any; attr: Attribute.Any;
updatedAttribute: { name: string; value: unknown }; updatedAttribute: { name: string; value: unknown };
model: Schema.ContentType; model: Schema.ContentType | Schema.Component;
entity?: Entity; entity?: Entity;
} }
interface ModelValidatorMetas { interface ModelValidatorMetas {
model: Schema.ContentType; model: Schema.ContentType | Schema.Component;
data: Record<string, unknown>; data: Record<string, unknown>;
entity?: Entity; entity?: Entity;
} }
@ -299,7 +299,7 @@ const createModelValidator =
const createValidateEntity = (createOrUpdate: CreateOrUpdate) => { const createValidateEntity = (createOrUpdate: CreateOrUpdate) => {
return async <TUID extends Common.UID.ContentType, TData extends Types.Params.Data.Input<TUID>>( return async <TUID extends Common.UID.ContentType, TData extends Types.Params.Data.Input<TUID>>(
model: Shared.ContentTypes[TUID], model: Shared.ContentTypes[TUID],
data: TData | Partial<TData>, data: TData | Partial<TData> | undefined,
options?: { isDraft?: boolean }, options?: { isDraft?: boolean },
entity?: Entity entity?: Entity
): Promise<TData> => { ): Promise<TData> => {
@ -312,11 +312,7 @@ const createValidateEntity = (createOrUpdate: CreateOrUpdate) => {
} }
const validator = createModelValidator(createOrUpdate)( const validator = createModelValidator(createOrUpdate)(
{ { model, data, entity },
model,
data,
entity,
},
{ isDraft: options?.isDraft ?? false } { isDraft: options?.isDraft ?? false }
) )
.test('relations-test', 'check that all relations exist', async function (data) { .test('relations-test', 'check that all relations exist', async function (data) {
@ -342,11 +338,11 @@ const createValidateEntity = (createOrUpdate: CreateOrUpdate) => {
/** /**
* Builds an object containing all the media and relations being associated with an entity * Builds an object containing all the media and relations being associated with an entity
*/ */
const buildRelationsStore = ({ const buildRelationsStore = <TUID extends Common.UID.ContentType | Common.UID.Component>({
uid, uid,
data, data,
}: { }: {
uid: Common.UID.ContentType | Common.UID.Component; uid: TUID;
data: Record<string, unknown> | null; data: Record<string, unknown> | null;
}): Record<string, ID[]> => { }): Record<string, ID[]> => {
if (!uid) { if (!uid) {
@ -357,7 +353,7 @@ const buildRelationsStore = ({
return {}; return {};
} }
const currentModel: Schema.ContentType = strapi.getModel(uid); const currentModel: Common.Schemas[TUID] = strapi.getModel(uid);
return Object.keys(currentModel.attributes).reduce((result, attributeName: string) => { return Object.keys(currentModel.attributes).reduce((result, attributeName: string) => {
const attribute = currentModel.attributes[attributeName]; const attribute = currentModel.attributes[attributeName];
@ -470,7 +466,7 @@ const checkRelationsExist = async (relationsStore: Record<string, ID[]> = {}) =>
for (const [key, value] of Object.entries(relationsStore)) { for (const [key, value] of Object.entries(relationsStore)) {
const evaluate = async () => { const evaluate = async () => {
const uniqueValues = uniqBy(value, `id`); const uniqueValues = uniqBy(value, `id`);
const count = await strapi.query(key as UID.ContentType).count({ const count = await strapi.query(key as Common.UID.Schema).count({
where: { where: {
id: { id: {
$in: uniqueValues.map((v) => v.id), $in: uniqueValues.map((v) => v.id),
@ -500,7 +496,7 @@ export interface EntityValidator {
) => Promise<Types.Params.Data.Input<TUID>>; ) => Promise<Types.Params.Data.Input<TUID>>;
validateEntityUpdate: <TUID extends Common.UID.ContentType>( validateEntityUpdate: <TUID extends Common.UID.ContentType>(
model: Shared.ContentTypes[TUID], model: Shared.ContentTypes[TUID],
data: Partial<Types.Params.Data.Input<TUID>>, data: Partial<Types.Params.Data.Input<TUID>> | undefined,
options?: { isDraft?: boolean }, options?: { isDraft?: boolean },
entity?: Entity entity?: Entity
) => Promise<Types.Params.Data.Input<TUID>>; ) => Promise<Types.Params.Data.Input<TUID>>;

View File

@ -9,7 +9,7 @@ export type Enumeration<TValues extends string[] = []> = Attribute.OfType<'enume
EnumerationProperties<TValues> & EnumerationProperties<TValues> &
// Options // Options
Attribute.ConfigurableOption & Attribute.ConfigurableOption &
Attribute.DefaultOption<TValues> & Attribute.DefaultOption<TValues[number]> &
Attribute.PrivateOption & Attribute.PrivateOption &
Attribute.RequiredOption & Attribute.RequiredOption &
Attribute.WritableOption & Attribute.WritableOption &

View File

@ -8,7 +8,7 @@ export interface MediaProperties<
TKind extends MediaKind | undefined = undefined, TKind extends MediaKind | undefined = undefined,
TMultiple extends Utils.Expression.BooleanValue = Utils.Expression.False TMultiple extends Utils.Expression.BooleanValue = Utils.Expression.False
> { > {
allowedTypes?: TKind; allowedTypes?: TKind | TKind[];
multiple?: TMultiple; multiple?: TMultiple;
} }

View File

@ -141,4 +141,8 @@ export interface SingleType extends ContentType {
*/ */
export interface Component extends Schema { export interface Component extends Schema {
modelType: 'component'; modelType: 'component';
uid: Common.UID.Component;
category: string;
} }

View File

@ -195,8 +195,8 @@ const isTypedAttribute = (attribute: Attribute, type: string) => {
*/ */
const getContentTypeRoutePrefix = (contentType: Model) => { const getContentTypeRoutePrefix = (contentType: Model) => {
return isSingleType(contentType) return isSingleType(contentType)
? _.kebabCase(contentType.info.singularName) ? _.kebabCase(contentType.info?.singularName)
: _.kebabCase(contentType.info.pluralName); : _.kebabCase(contentType.info?.pluralName);
}; };
export { export {

View File

@ -67,10 +67,10 @@ export type AnyAttribute =
export type Kind = 'singleType' | 'collectionType'; export type Kind = 'singleType' | 'collectionType';
export interface Model { export interface Model {
modelType: 'contentType'; modelType: 'contentType' | 'component';
uid: string; uid: string;
kind: Kind; kind?: Kind;
info: { info?: {
displayName: string; displayName: string;
singularName: string; singularName: string;
pluralName: string; pluralName: string;