diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index 4e9ad17..c8a0691 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -111,6 +111,17 @@ type PieceRecord = { updatedAt: Date; }; +type ModelTypeRecord = { + id: string; + name: string; + code: string; + category: 'COMPONENT' | 'PIECE'; + description: Nullable; + notes: Nullable; + createdAt: Date; + updatedAt: Date; +}; + type CustomFieldRecord = { id: string; name: string; @@ -152,6 +163,7 @@ class InMemoryPrismaService { private sites: SiteRecord[] = []; private typeComposants: TypeComposantRecord[] = []; private typePieces: TypePieceRecord[] = []; + private modelTypes: ModelTypeRecord[] = []; private modelTypeCodeCounter = 0; private typeMachines: TypeMachineRecord[] = []; private typeMachineComponentRequirements: TypeMachineComponentRequirementRecord[] = @@ -178,6 +190,7 @@ class InMemoryPrismaService { this.sites = []; this.typeComposants = []; this.typePieces = []; + this.modelTypes = []; this.modelTypeCodeCounter = 0; this.typeMachines = []; this.typeMachineComponentRequirements = []; @@ -190,6 +203,131 @@ class InMemoryPrismaService { this.profiles = []; } + private readonly modelTypeDelegate = { + create: async ({ data, include }: any) => { + const now = new Date(); + const category: 'COMPONENT' | 'PIECE' = data.category ?? 'COMPONENT'; + const record: ModelTypeRecord = { + id: generateId('model_type'), + name: data.name, + code: data.code ?? this.generateModelTypeCode(data.name), + category, + description: data.description ?? null, + notes: data.notes ?? null, + createdAt: now, + updatedAt: now, + }; + this.modelTypes.push(record); + + if (data.customFields?.create) { + for (const field of data.customFields.create) { + this.createCustomFieldForModel(record.id, 'COMPONENT', field); + } + } + + if (data.pieceCustomFields?.create) { + for (const field of data.pieceCustomFields.create) { + this.createCustomFieldForModel(record.id, 'PIECE', field); + } + } + + this.syncModelType(record); + const resolvedInclude = typeof include === 'object' ? include : {}; + return this.buildModelType(record, resolvedInclude); + }, + + findMany: async ({ where, include, orderBy }: any = {}) => { + let records = [...this.modelTypes]; + if (where) { + records = records.filter((item) => + this.matchesModelTypeWhere(item, where), + ); + } + if (orderBy?.name) { + records.sort((a, b) => + orderBy.name === 'desc' + ? b.name.localeCompare(a.name) + : a.name.localeCompare(b.name), + ); + } + const resolvedInclude = typeof include === 'object' ? include : {}; + return records.map((record) => + this.buildModelType(record, resolvedInclude), + ); + }, + + findFirst: async ({ where, include }: any = {}) => { + const record = this.modelTypes.find((item) => + this.matchesModelTypeWhere(item, where), + ); + const resolvedInclude = typeof include === 'object' ? include : {}; + return record ? this.buildModelType(record, resolvedInclude) : null; + }, + + findUnique: async ({ where, include }: any) => { + const record = this.modelTypes.find((item) => { + if (!where) return false; + if (where.id) { + return item.id === where.id; + } + if (where.code) { + return item.code === where.code; + } + return this.matchesModelTypeWhere(item, where); + }); + const resolvedInclude = typeof include === 'object' ? include : {}; + return record ? this.buildModelType(record, resolvedInclude) : null; + }, + + update: async ({ where, data, include }: any) => { + const record = this.modelTypes.find((item) => + this.matchesModelTypeWhere(item, where), + ); + if (!record) { + throw new Error('ModelType not found'); + } + + if (data.name !== undefined) { + record.name = this.applyUpdateValue(data.name); + } + if (data.description !== undefined) { + record.description = this.applyUpdateValue(data.description); + } + if (data.notes !== undefined) { + record.notes = this.applyUpdateValue(data.notes); + } + if (data.code !== undefined) { + record.code = this.applyUpdateValue(data.code); + } + + record.updatedAt = new Date(); + this.syncModelType(record); + const resolvedInclude = typeof include === 'object' ? include : {}; + return this.buildModelType(record, resolvedInclude); + }, + + delete: async ({ where }: any) => { + const index = this.modelTypes.findIndex((item) => + this.matchesModelTypeWhere(item, where), + ); + if (index === -1) { + throw new Error('ModelType not found'); + } + const [record] = this.modelTypes.splice(index, 1); + this.removeModelType(record); + this.customFields = this.customFields.filter( + (field) => + field.typeComposantId !== record.id && + field.typePieceId !== record.id, + ); + return { ...record }; + }, + }; + + get modelType() { + return this.modelTypeDelegate; + } + site = { create: async ({ data }: any) => { const now = new Date(); @@ -604,6 +742,26 @@ class InMemoryPrismaService { this.customFields.push(record); return { ...record }; }, + createMany: async ({ data }: any) => { + const now = new Date(); + const records = data.map((item: any) => { + const record: CustomFieldRecord = { + id: generateId('cf'), + name: item.name, + type: item.type, + required: item.required ?? false, + options: item.options ?? [], + typeMachineId: item.typeMachineId ?? null, + typeComposantId: item.typeComposantId ?? null, + typePieceId: item.typePieceId ?? null, + createdAt: now, + updatedAt: now, + }; + this.customFields.push(record); + return record; + }); + return { count: records.length }; + }, findMany: async ({ where }: any) => { return this.customFields .filter((field) => { @@ -766,6 +924,183 @@ class InMemoryPrismaService { return result; } + private matchesModelTypeWhere(record: ModelTypeRecord, where?: any) { + if (!where) { + return true; + } + if (where.AND && Array.isArray(where.AND)) { + return where.AND.every((clause: any) => + this.matchesModelTypeWhere(record, clause), + ); + } + if (where.OR && Array.isArray(where.OR)) { + return where.OR.some((clause: any) => + this.matchesModelTypeWhere(record, clause), + ); + } + if (where.NOT && Array.isArray(where.NOT)) { + return where.NOT.every( + (clause: any) => !this.matchesModelTypeWhere(record, clause), + ); + } + return Object.entries(where).every(([key, value]) => { + if (value === undefined) { + return true; + } + if ( + key === 'id' || + key === 'code' || + key === 'category' || + key === 'name' + ) { + return (record as any)[key] === value; + } + return true; + }); + } + + private createCustomFieldForModel( + modelTypeId: string, + category: 'COMPONENT' | 'PIECE', + field: any, + ) { + const now = new Date(); + const record: CustomFieldRecord = { + id: generateId('cf'), + name: field.name, + type: field.type, + required: field.required ?? false, + options: field.options ?? [], + typeMachineId: null, + typeComposantId: category === 'COMPONENT' ? modelTypeId : null, + typePieceId: category === 'PIECE' ? modelTypeId : null, + createdAt: now, + updatedAt: now, + }; + this.customFields.push(record); + return record; + } + + private syncModelType(record: ModelTypeRecord) { + const base = { + id: record.id, + name: record.name, + description: record.description ?? null, + createdAt: record.createdAt, + updatedAt: record.updatedAt, + }; + + if (record.category === 'COMPONENT') { + const existing = this.typeComposants.find( + (item) => item.id === record.id, + ); + if (existing) { + existing.name = base.name; + existing.description = base.description; + existing.updatedAt = record.updatedAt; + } else { + this.typeComposants.push({ ...base }); + } + } else { + const existing = this.typePieces.find((item) => item.id === record.id); + if (existing) { + existing.name = base.name; + existing.description = base.description; + existing.updatedAt = record.updatedAt; + } else { + this.typePieces.push({ ...base }); + } + } + } + + private removeModelType(record: ModelTypeRecord) { + if (record.category === 'COMPONENT') { + this.typeComposants = this.typeComposants.filter( + (item) => item.id !== record.id, + ); + } else { + this.typePieces = this.typePieces.filter((item) => item.id !== record.id); + } + } + + private generateModelTypeCode(name: string) { + const base = + (name || 'type') + .toLowerCase() + .normalize('NFD') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') || 'type'; + let candidate = base; + let suffix = 1; + while (this.modelTypes.some((item) => item.code === candidate)) { + candidate = `${base}-${suffix++}`; + } + return candidate; + } + + private applyUpdateValue(value: any): T { + if (value && typeof value === 'object' && 'set' in value) { + return value.set as T; + } + return value as T; + } + + private buildModelType(record: ModelTypeRecord, include: any) { + const base: any = { ...record }; + + const customFieldsInclude = include?.customFields; + if (customFieldsInclude) { + const select = + typeof customFieldsInclude === 'object' + ? customFieldsInclude.select + : undefined; + base.customFields = this.customFields + .filter((field) => field.typeComposantId === record.id) + .map((field) => this.applySelect(field, select)); + } + + const pieceCustomFieldsInclude = include?.pieceCustomFields; + if (pieceCustomFieldsInclude) { + const select = + typeof pieceCustomFieldsInclude === 'object' + ? pieceCustomFieldsInclude.select + : undefined; + base.pieceCustomFields = this.customFields + .filter((field) => field.typePieceId === record.id) + .map((field) => this.applySelect(field, select)); + } + + if (include?.composants) { + base.composants = this.composants + .filter((item) => item.typeComposantId === record.id) + .map((item) => ({ ...item })); + } + + if (include?.models) { + base.models = []; + } + + if (include?.pieceModels) { + base.pieceModels = []; + } + + if (include?.pieceRequirements) { + base.pieceRequirements = []; + } + + if (include?.pieces) { + base.pieces = this.pieces + .filter((item) => item.typePieceId === record.id) + .map((item) => ({ ...item })); + } + + if (include?.componentRequirements) { + base.componentRequirements = []; + } + + return base; + } + private buildSite(site: SiteRecord, include: any) { const base: any = { ...site }; if (include?.machines) {