diff --git a/packages/agent/src/routes/access/chart.ts b/packages/agent/src/routes/access/chart.ts index 53109e96a3..fa48bd8f74 100644 --- a/packages/agent/src/routes/access/chart.ts +++ b/packages/agent/src/routes/access/chart.ts @@ -8,7 +8,7 @@ import { DateOperation, Filter, FilterFactory, - RelationSchema, + SchemaUtils, ValidationError, } from '@forestadmin/datasource-toolkit'; import { @@ -187,8 +187,11 @@ export default class ChartRoute extends CollectionRoute { context: Context, ): Promise> { const body = context.request.body; - const field = this.collection.schema.fields[body.relationshipFieldName] as RelationSchema; - + const field = SchemaUtils.getRelation( + this.collection.schema, + body.relationshipFieldName, + this.collection.name, + ); let collection: string; let filter: Filter; let aggregation: Aggregation; diff --git a/packages/agent/src/routes/modification/create.ts b/packages/agent/src/routes/modification/create.ts index a2e821e8c2..4d3bba09bd 100644 --- a/packages/agent/src/routes/modification/create.ts +++ b/packages/agent/src/routes/modification/create.ts @@ -7,7 +7,6 @@ import { ManyToOneSchema, RecordData, RecordValidator, - RelationSchema, SchemaUtils, } from '@forestadmin/datasource-toolkit'; import Router from '@koa/router'; @@ -43,7 +42,7 @@ export default class CreateRoute extends CollectionRoute { const relations: Record = {}; const promises = Object.entries(record).map(async ([field, value]) => { - const schema = this.collection.schema.fields[field]; + const schema = SchemaUtils.getField(this.collection.schema, field, this.collection.name); if (schema?.type === 'OneToOne' || schema?.type === 'ManyToOne') { relations[field] = this.getRelationRecord(field, value as CompositeId); @@ -87,7 +86,7 @@ export default class CreateRoute extends CollectionRoute { const caller = QueryStringParser.parseCaller(context); const promises = Object.entries(relations).map(async ([field, linked]) => { - const relation = this.collection.schema.fields[field]; + const relation = SchemaUtils.getRelation(this.collection.schema, field, this.collection.name); if (linked === null || relation.type !== 'OneToOne') return; // Permissions @@ -121,7 +120,7 @@ export default class CreateRoute extends CollectionRoute { private getRelationRecord(field: string, id: CompositeId): RecordData { if (id === null) return null; - const schema = this.collection.schema.fields[field] as RelationSchema; + const schema = SchemaUtils.getRelation(this.collection.schema, field, this.collection.name); const foreignCollection = this.dataSource.getCollection(schema.foreignCollection); const pkName = SchemaUtils.getPrimaryKeys(foreignCollection.schema); @@ -136,7 +135,11 @@ export default class CreateRoute extends CollectionRoute { if (id === null) return null; const caller = QueryStringParser.parseCaller(context); - const schema = this.collection.schema.fields[field] as ManyToOneSchema; + const schema = SchemaUtils.getRelation( + this.collection.schema, + field, + this.collection.name, + ) as ManyToOneSchema; const foreignCollection = this.dataSource.getCollection(schema.foreignCollection); return CollectionUtils.getValue(foreignCollection, caller, id, schema.foreignKeyTarget); diff --git a/packages/agent/src/routes/modification/update-relation.ts b/packages/agent/src/routes/modification/update-relation.ts index 61799b5bc6..4f8205fccd 100644 --- a/packages/agent/src/routes/modification/update-relation.ts +++ b/packages/agent/src/routes/modification/update-relation.ts @@ -9,6 +9,7 @@ import { Filter, ManyToOneSchema, OneToOneSchema, + SchemaUtils, } from '@forestadmin/datasource-toolkit'; import Router from '@koa/router'; import { Context } from 'koa'; @@ -29,7 +30,11 @@ export default class UpdateRelation extends RelationRoute { public async handleUpdateRelationRoute(context: Context): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any const body = context.request.body as any; - const relation = this.collection.schema.fields[this.relationName]; + const relation = SchemaUtils.getRelation( + this.collection.schema, + this.relationName, + this.collection.name, + ); const caller = QueryStringParser.parseCaller(context); const parentId = IdUtils.unpackId(this.collection.schema, context.params.parentId); diff --git a/packages/agent/src/routes/relation-route.ts b/packages/agent/src/routes/relation-route.ts index 41d858206a..f34a20319e 100644 --- a/packages/agent/src/routes/relation-route.ts +++ b/packages/agent/src/routes/relation-route.ts @@ -1,4 +1,4 @@ -import { Collection, DataSource, RelationSchema } from '@forestadmin/datasource-toolkit'; +import { Collection, DataSource, SchemaUtils } from '@forestadmin/datasource-toolkit'; import CollectionRoute from './collection-route'; import { ForestAdminHttpDriverServices } from '../services'; @@ -8,7 +8,11 @@ export default abstract class RelationRoute extends CollectionRoute { protected readonly relationName: string; protected get foreignCollection(): Collection { - const schema = this.collection.schema.fields[this.relationName] as RelationSchema; + const schema = SchemaUtils.getRelation( + this.collection.schema, + this.relationName, + this.collection.name, + ); return this.collection.dataSource.getCollection(schema.foreignCollection); } diff --git a/packages/agent/src/utils/forest-schema/generator-actions.ts b/packages/agent/src/utils/forest-schema/generator-actions.ts index c29f17886d..a6af5255d4 100644 --- a/packages/agent/src/utils/forest-schema/generator-actions.ts +++ b/packages/agent/src/utils/forest-schema/generator-actions.ts @@ -3,7 +3,6 @@ import { ActionFormElement, ActionLayoutElement, Collection, - ColumnSchema, DataSource, LayoutElementInput, PrimitiveTypes, @@ -118,7 +117,7 @@ export default class SchemaGeneratorActions { if (ActionFields.isCollectionField(field)) { const collection = dataSource.getCollection(field.collectionName); const [pk] = SchemaUtils.getPrimaryKeys(collection.schema); - const pkSchema = collection.schema.fields[pk] as ColumnSchema; + const pkSchema = SchemaUtils.getColumn(collection.schema, pk, collection.name); output.type = pkSchema.columnType; output.reference = `${collection.name}.${pk}`; diff --git a/packages/agent/src/utils/forest-schema/generator-fields.ts b/packages/agent/src/utils/forest-schema/generator-fields.ts index 7f7cdc5e32..cd73040c7a 100644 --- a/packages/agent/src/utils/forest-schema/generator-fields.ts +++ b/packages/agent/src/utils/forest-schema/generator-fields.ts @@ -9,7 +9,6 @@ import { OneToManySchema, OneToOneSchema, PrimitiveTypes, - RelationSchema, SchemaUtils, ValidationError, } from '@forestadmin/datasource-toolkit'; @@ -28,7 +27,7 @@ export default class SchemaGeneratorFields { }; static buildSchema(collection: Collection, name: string): ForestServerField { - const { type } = collection.schema.fields[name]; + const { type } = SchemaUtils.getField(collection.schema, name, collection.name); let schema: ForestServerField; @@ -58,7 +57,7 @@ export default class SchemaGeneratorFields { } private static buildColumnSchema(collection: Collection, name: string): ForestServerField { - const column = collection.schema.fields[name] as ColumnSchema; + const column = SchemaUtils.getColumn(collection.schema, name, collection.name); ColumnSchemaValidator.validate(column, name); const isForeignKey = SchemaUtils.isForeignKey(collection.schema, name); @@ -112,17 +111,33 @@ export default class SchemaGeneratorFields { if (relation.type === 'OneToMany') { targetName = relation.originKeyTarget; - targetField = collection.schema.fields[targetName] as ColumnSchema; + targetField = SchemaUtils.getColumn(collection.schema, targetName, collection.name); - const originKey = foreignCollection.schema.fields[relation.originKey] as ColumnSchema; + const originKey = SchemaUtils.getColumn( + foreignCollection.schema, + relation.originKey, + foreignCollection.name, + ); isReadOnly = originKey.isReadOnly; } else { targetName = relation.foreignKeyTarget; - targetField = foreignCollection.schema.fields[targetName] as ColumnSchema; + targetField = SchemaUtils.getColumn( + foreignCollection.schema, + targetName, + foreignCollection.name, + ); - const throughSchema = collection.dataSource.getCollection(relation.throughCollection).schema; - const foreignKey = throughSchema.fields[relation.foreignKey] as ColumnSchema; - const originKey = throughSchema.fields[relation.originKey] as ColumnSchema; + const throughCollection = collection.dataSource.getCollection(relation.throughCollection); + const foreignKey = SchemaUtils.getColumn( + throughCollection.schema, + relation.foreignKey, + throughCollection.name, + ); + const originKey = SchemaUtils.getColumn( + throughCollection.schema, + relation.originKey, + throughCollection.name, + ); isReadOnly = originKey.isReadOnly || foreignKey.isReadOnly; } @@ -154,8 +169,16 @@ export default class SchemaGeneratorFields { foreignCollection: Collection, baseSchema: ForestServerField, ): ForestServerField { - const targetField = collection.schema.fields[relation.originKeyTarget] as ColumnSchema; - const keyField = foreignCollection.schema.fields[relation.originKey] as ColumnSchema; + const targetField = SchemaUtils.getColumn( + collection.schema, + relation.originKeyTarget, + collection.name, + ); + const keyField = SchemaUtils.getColumn( + foreignCollection.schema, + relation.originKey, + foreignCollection.name, + ); return { ...baseSchema, @@ -177,7 +200,7 @@ export default class SchemaGeneratorFields { foreignCollection: Collection, baseSchema: ForestServerField, ): ForestServerField { - const keyField = collection.schema.fields[relation.foreignKey] as ColumnSchema; + const keyField = SchemaUtils.getColumn(collection.schema, relation.foreignKey, collection.name); return { ...baseSchema, @@ -197,7 +220,7 @@ export default class SchemaGeneratorFields { } private static buildRelationSchema(collection: Collection, name: string): ForestServerField { - const relation = collection.schema.fields[name] as RelationSchema; + const relation = SchemaUtils.getRelation(collection.schema, name, collection.name); const foreignCollection = collection.dataSource.getCollection(relation.foreignCollection); const relationSchema = { diff --git a/packages/agent/src/utils/id.ts b/packages/agent/src/utils/id.ts index a74416eb0e..615bd52fb9 100644 --- a/packages/agent/src/utils/id.ts +++ b/packages/agent/src/utils/id.ts @@ -1,6 +1,5 @@ import { CollectionSchema, - ColumnSchema, CompositeId, FieldValidator, RecordData, @@ -49,7 +48,7 @@ export default class IdUtils { } return pkNames.map((pkName, index) => { - const schemaField = schema.fields[pkName] as ColumnSchema; + const schemaField = SchemaUtils.getColumn(schema, pkName); const value = pkValues[index]; const castedValue = schemaField.columnType === 'Number' ? Number(value) : value; diff --git a/packages/agent/src/utils/query-string.ts b/packages/agent/src/utils/query-string.ts index ff9d8f4a81..08b911db58 100644 --- a/packages/agent/src/utils/query-string.ts +++ b/packages/agent/src/utils/query-string.ts @@ -8,6 +8,7 @@ import { Projection, ProjectionFactory, ProjectionValidator, + SchemaUtils, Sort, SortFactory, SortValidator, @@ -54,11 +55,9 @@ export default class QueryStringParser { const { schema } = collection; const rootFields = fields.toString().split(','); const explicitRequest = rootFields.map(field => { - if (!schema.fields[field]) { - throw new ValidationError(`field not found '${collection.name}.${field}'`); - } + const columnOrRelation = SchemaUtils.getField(schema, field, collection.name); - return schema.fields[field].type === 'Column' + return columnOrRelation.type === 'Column' ? field : `${field}:${context.request.query[`fields[${field}]`]}`; }); @@ -67,7 +66,7 @@ export default class QueryStringParser { return new Projection(...explicitRequest); } catch (e) { - throw new ValidationError(`Invalid projection: ${e.message}}`); + throw new ValidationError(`Invalid projection: ${e.message}`); } } diff --git a/packages/agent/test/routes/access/chart.test.ts b/packages/agent/test/routes/access/chart.test.ts index e9eb63f370..baab419001 100644 --- a/packages/agent/test/routes/access/chart.test.ts +++ b/packages/agent/test/routes/access/chart.test.ts @@ -1,4 +1,8 @@ -import { ConditionTreeFactory, ValidationError } from '@forestadmin/datasource-toolkit'; +import { + ConditionTreeFactory, + MissingRelationError, + ValidationError, +} from '@forestadmin/datasource-toolkit'; import { createMockContext } from '@shopify/jest-koa-mocks'; import Chart from '../../../src/routes/access/chart'; @@ -71,6 +75,8 @@ describe('ChartRoute', () => { originKey: 'authorId', originKeyTarget: 'id', }), + manyToOneRelation: factories.manyToOneSchema.build(), + oneToOneRelation: factories.oneToOneSchema.build(), }, }), }), @@ -892,7 +898,7 @@ describe('ChartRoute', () => { }); }); - describe('when relation field is invalid', () => { + describe('when relation field is missing', () => { test('it should throw an error', async () => { jest.spyOn(dataSource.getCollection('persons'), 'aggregate').mockResolvedValueOnce([ { value: 1234, group: { id: 2 } }, @@ -913,13 +919,36 @@ describe('ChartRoute', () => { ...defaultContext, }); - await expect(chart.handleChart(context)).rejects.toThrow( - new ValidationError( - 'Failed to generate leaderboard chart: parameters do not match pre-requisites', - ), - ); + await expect(chart.handleChart(context)).rejects.toThrow(MissingRelationError); }); }); + + describe('when the given relation is not allowed', () => { + test.each(['manyToOneRelation', 'oneToOneRelation'])( + 'should throw an error when the relation is %s', + async relationType => { + const chart = new Chart(services, options, dataSource, 'persons'); + + const chartRequest = { + type: 'Leaderboard', + aggregator: 'Sum', + aggregateFieldName: 'id', + sourceCollectionName: 'persons', + labelFieldName: 'id', + relationshipFieldName: relationType, + limit: 2, + }; + const context = createMockContext({ + requestBody: chartRequest, + ...defaultContext, + }); + + await expect(chart.handleChart(context)).rejects.toThrow( + 'Failed to generate leaderboard chart: parameters do not match pre-requisites', + ); + }, + ); + }); }); }); }); diff --git a/packages/agent/test/utils/query-string.test.ts b/packages/agent/test/utils/query-string.test.ts index 2e9f8d8dc8..f2661c894d 100644 --- a/packages/agent/test/utils/query-string.test.ts +++ b/packages/agent/test/utils/query-string.test.ts @@ -157,7 +157,7 @@ describe('QueryStringParser', () => { const fn = () => QueryStringParser.parseProjection(collectionSimple, context); expect(fn).toThrow( - "Invalid projection: field not found 'books.field-that-do-not-exist'", + "Invalid projection: The 'books.field-that-do-not-exist' field was not found. Available fields are: [id,name]. Please check if the field name is correct.", ); }); }); diff --git a/packages/datasource-customizer/src/collection-customizer.ts b/packages/datasource-customizer/src/collection-customizer.ts index 9f1aac7b90..42860bbb37 100644 --- a/packages/datasource-customizer/src/collection-customizer.ts +++ b/packages/datasource-customizer/src/collection-customizer.ts @@ -1,9 +1,9 @@ import { CollectionSchema, CollectionUtils, - ColumnSchema, Logger, Operator, + SchemaUtils, allowedOperatorsForColumnType, } from '@forestadmin/datasource-toolkit'; @@ -458,7 +458,7 @@ export default class CollectionCustomizer< emulateFieldFiltering(name: TColumnName): this { return this.pushCustomization(async () => { const collection = this.stack.lateOpEmulate.getCollection(this.name); - const field = collection.schema.fields[name] as ColumnSchema; + const field = SchemaUtils.getColumn(collection.schema, name, collection.name); if (typeof field.columnType === 'string') { const operators = allowedOperatorsForColumnType[field.columnType]; diff --git a/packages/datasource-customizer/src/decorators/binary/collection.ts b/packages/datasource-customizer/src/decorators/binary/collection.ts index 0ac6b74965..37491b03a9 100644 --- a/packages/datasource-customizer/src/decorators/binary/collection.ts +++ b/packages/datasource-customizer/src/decorators/binary/collection.ts @@ -41,7 +41,11 @@ export default class BinaryCollectionDecorator extends CollectionDecorator { private useHexConversion: Map = new Map(); setBinaryMode(name: string, type: BinaryMode): void { - const field = this.childCollection.schema.fields[name]; + const field = SchemaUtils.getField( + this.childCollection.schema, + name, + this.childCollection.name, + ); if (type !== 'datauri' && type !== 'hex') { throw new Error('Invalid binary mode'); @@ -150,7 +154,11 @@ export default class BinaryCollectionDecorator extends CollectionDecorator { private async convertConditionTreeLeaf(leaf: ConditionTreeLeaf): Promise { const [prefix, suffix] = leaf.field.split(/:(.*)/); - const schema = this.childCollection.schema.fields[prefix]; + const schema = SchemaUtils.getField( + this.childCollection.schema, + prefix, + this.childCollection.name, + ); if (schema.type !== 'Column') { const conditionTree = await this.dataSource @@ -177,7 +185,11 @@ export default class BinaryCollectionDecorator extends CollectionDecorator { private async convertValue(toBackend: boolean, path: string, value: unknown): Promise { const [prefix, suffix] = path.split(/:(.*)/); - const schema = this.childCollection.schema.fields[prefix]; + const schema = SchemaUtils.getField( + this.childCollection.schema, + prefix, + this.childCollection.name, + ); if (schema.type !== 'Column') { const foreignCollection = this.dataSource.getCollection(schema.foreignCollection); diff --git a/packages/datasource-customizer/src/decorators/computed/collection.ts b/packages/datasource-customizer/src/decorators/computed/collection.ts index c6805499da..0d77c3a9a1 100644 --- a/packages/datasource-customizer/src/decorators/computed/collection.ts +++ b/packages/datasource-customizer/src/decorators/computed/collection.ts @@ -10,7 +10,7 @@ import { PaginatedFilter, Projection, RecordData, - RelationSchema, + SchemaUtils, } from '@forestadmin/datasource-toolkit'; import computeFromRecords from './helpers/compute-fields'; @@ -28,7 +28,11 @@ export default class ComputedCollection extends CollectionDecorator { const index = path.indexOf(':'); if (index === -1) return this.computeds[path]; - const { foreignCollection } = this.schema.fields[path.substring(0, index)] as RelationSchema; + const { foreignCollection } = SchemaUtils.getRelation( + this.schema, + path.substring(0, index), + this.name, + ); const association = this.dataSource.getCollection(foreignCollection); return association.getComputed(path.substring(index + 1)); diff --git a/packages/datasource-customizer/src/decorators/computed/helpers/rewrite-projection.ts b/packages/datasource-customizer/src/decorators/computed/helpers/rewrite-projection.ts index feedb23d57..a2a1e9e7b5 100644 --- a/packages/datasource-customizer/src/decorators/computed/helpers/rewrite-projection.ts +++ b/packages/datasource-customizer/src/decorators/computed/helpers/rewrite-projection.ts @@ -1,4 +1,4 @@ -import { Projection, RelationSchema } from '@forestadmin/datasource-toolkit'; +import { Projection, SchemaUtils } from '@forestadmin/datasource-toolkit'; import ComputedCollection from '../collection'; @@ -6,7 +6,7 @@ export default function rewriteField(collection: ComputedCollection, path: strin // Projection is targeting a field on another collection => recurse. if (path.includes(':')) { const [prefix] = path.split(':'); - const schema = collection.schema.fields[prefix] as RelationSchema; + const schema = SchemaUtils.getRelation(collection.schema, prefix, collection.name); const association = collection.dataSource.getCollection(schema.foreignCollection); return new Projection(path) diff --git a/packages/datasource-customizer/src/decorators/operators-emulate/collection.ts b/packages/datasource-customizer/src/decorators/operators-emulate/collection.ts index 966c1a3538..12c11cb7b3 100644 --- a/packages/datasource-customizer/src/decorators/operators-emulate/collection.ts +++ b/packages/datasource-customizer/src/decorators/operators-emulate/collection.ts @@ -12,7 +12,6 @@ import { FieldValidator, Operator, PaginatedFilter, - RelationSchema, SchemaUtils, } from '@forestadmin/datasource-toolkit'; @@ -40,7 +39,11 @@ export default class OperatorsEmulateCollectionDecorator extends CollectionDecor // Check that the collection can actually support our rewriting const pks = SchemaUtils.getPrimaryKeys(this.childCollection.schema); pks.forEach(pk => { - const schema = this.childCollection.schema.fields[pk] as ColumnSchema; + const schema = SchemaUtils.getField( + this.childCollection.schema, + pk, + this.childCollection.name, + ) as ColumnSchema; const operators = schema.filterOperators; if (!operators?.has('Equal') || !operators?.has('In')) { @@ -52,9 +55,8 @@ export default class OperatorsEmulateCollectionDecorator extends CollectionDecor }); // Check that targeted field is valid - const field = this.childCollection.schema.fields[name] as ColumnSchema; + SchemaUtils.throwIfMissingField(this.childCollection.schema, name, this.childCollection.name); FieldValidator.validate(this, name); - if (!field) throw new Error('Cannot replace operator for relation'); // Mark the field operator as replaced. if (!this.fields.has(name)) this.fields.set(name, new Map()); @@ -102,7 +104,7 @@ export default class OperatorsEmulateCollectionDecorator extends CollectionDecor // ConditionTree is targeting a field on another collection => recurse. if (leaf.field.includes(':')) { const [prefix] = leaf.field.split(':'); - const schema = this.schema.fields[prefix] as RelationSchema; + const schema = SchemaUtils.getRelation(this.schema, prefix, this.name); const association = this.dataSource.getCollection(schema.foreignCollection); const associationLeaf = await leaf .unnest() diff --git a/packages/datasource-customizer/src/decorators/override/collection.ts b/packages/datasource-customizer/src/decorators/override/collection.ts index 3ded867005..452215c539 100644 --- a/packages/datasource-customizer/src/decorators/override/collection.ts +++ b/packages/datasource-customizer/src/decorators/override/collection.ts @@ -78,6 +78,7 @@ export default class OverrideCollectionDecorator extends CollectionDecorator { records.forEach(result => { let hasPrimaryKey = false; Object.keys(result).forEach(key => { + // don't use SchemaUtils.getField util because if the field does not exist, we remove it. const field = this.schema.fields[key]; if (!field || field.type !== 'Column') { diff --git a/packages/datasource-customizer/src/decorators/publication/collection.ts b/packages/datasource-customizer/src/decorators/publication/collection.ts index f1823c0302..2ae7e870e0 100644 --- a/packages/datasource-customizer/src/decorators/publication/collection.ts +++ b/packages/datasource-customizer/src/decorators/publication/collection.ts @@ -3,7 +3,6 @@ import { CollectionDecorator, CollectionSchema, FieldSchema, - MissingFieldError, RecordData, SchemaUtils, } from '@forestadmin/datasource-toolkit'; @@ -17,11 +16,7 @@ export default class PublicationCollectionDecorator extends CollectionDecorator /** Show/hide fields from the schema */ changeFieldVisibility(name: string, visible: boolean): void { - const field = this.childCollection.schema.fields[name]; - - if (!field) { - throw new MissingFieldError(name, this.childCollection.name); - } + SchemaUtils.throwIfMissingField(this.childCollection.schema, name, this.childCollection.name); if (SchemaUtils.isPrimaryKey(this.childCollection.schema, name)) { throw new Error(`Cannot hide primary key`); @@ -63,7 +58,11 @@ export default class PublicationCollectionDecorator extends CollectionDecorator if (this.blacklist.has(name)) return false; // Implicitly hidden - const field = this.childCollection.schema.fields[name]; + const field = SchemaUtils.getField( + this.childCollection.schema, + name, + this.childCollection.name, + ); if (field.type === 'ManyToOne') { return ( diff --git a/packages/datasource-customizer/src/decorators/relation/collection.ts b/packages/datasource-customizer/src/decorators/relation/collection.ts index edd81e96c2..29a6684a85 100644 --- a/packages/datasource-customizer/src/decorators/relation/collection.ts +++ b/packages/datasource-customizer/src/decorators/relation/collection.ts @@ -11,7 +11,6 @@ import { ConditionTreeLeaf, DataSourceDecorator, Filter, - MissingFieldError, PaginatedFilter, Projection, RecordData, @@ -160,8 +159,8 @@ export default class RelationCollectionDecorator extends CollectionDecorator { RelationCollectionDecorator.checkColumn(owner, keyName); RelationCollectionDecorator.checkColumn(targetOwner, targetName); - const key = owner.schema.fields[keyName] as ColumnSchema; - const target = targetOwner.schema.fields[targetName] as ColumnSchema; + const key = SchemaUtils.getColumn(owner.schema, keyName, owner.name); + const target = SchemaUtils.getColumn(targetOwner.schema, targetName, targetOwner.name); if (key.columnType !== target.columnType) { throw new Error( @@ -172,11 +171,7 @@ export default class RelationCollectionDecorator extends CollectionDecorator { } private static checkColumn(owner: Collection, name: string): void { - const column = owner.schema.fields[name]; - - if (!column || column.type !== 'Column') { - throw new MissingFieldError(name, owner.name); - } + const column = SchemaUtils.getColumn(owner.schema, name, owner.name); if (!column.filterOperators?.has('In')) { throw new Error(`Column does not support the In operator: '${owner.name}.${name}'`); @@ -185,7 +180,7 @@ export default class RelationCollectionDecorator extends CollectionDecorator { private rewriteField(field: string): string[] { const prefix = field.split(':').shift(); - const schema = this.schema.fields[prefix]; + const schema = SchemaUtils.getField(this.schema, prefix, this.name); if (schema.type === 'Column') return [field]; const relation = this.dataSource.getCollection(schema.foreignCollection); @@ -210,7 +205,7 @@ export default class RelationCollectionDecorator extends CollectionDecorator { private async rewriteLeaf(caller: Caller, leaf: ConditionTreeLeaf): Promise { const prefix = leaf.field.split(':').shift(); - const schema = this.schema.fields[prefix]; + const schema = SchemaUtils.getField(this.schema, prefix, this.name); if (schema.type === 'Column') return leaf; const relation = this.dataSource.getCollection(schema.foreignCollection); @@ -303,7 +298,7 @@ export default class RelationCollectionDecorator extends CollectionDecorator { name: string, projection: Projection, ): Promise { - const schema = this.schema.fields[name] as RelationSchema; + const schema = SchemaUtils.getRelation(this.schema, name, this.name); const association = this.dataSource.getCollection(schema.foreignCollection); if (!this.relations[name]) { diff --git a/packages/datasource-customizer/src/decorators/rename-field/collection.ts b/packages/datasource-customizer/src/decorators/rename-field/collection.ts index 056bd00c30..13fd4cea78 100644 --- a/packages/datasource-customizer/src/decorators/rename-field/collection.ts +++ b/packages/datasource-customizer/src/decorators/rename-field/collection.ts @@ -8,11 +8,10 @@ import { FieldSchema, FieldValidator, Filter, - MissingFieldError, PaginatedFilter, Projection, RecordData, - RelationSchema, + SchemaUtils, } from '@forestadmin/datasource-toolkit'; /** @@ -30,9 +29,7 @@ export default class RenameFieldCollectionDecorator extends CollectionDecorator /** Rename a field from the collection */ renameField(currentName: string, newName: string): void { - if (!this.schema.fields[currentName]) { - throw new MissingFieldError(currentName); - } + SchemaUtils.throwIfMissingField(this.schema, currentName, this.name); let initialName = currentName; @@ -152,7 +149,7 @@ export default class RenameFieldCollectionDecorator extends CollectionDecorator const dotIndex = childPath.indexOf(':'); const childField = childPath.substring(0, dotIndex); const thisField = this.fromChildCollection[childField] ?? childField; - const schema = this.schema.fields[thisField] as RelationSchema; + const schema = SchemaUtils.getRelation(this.schema, thisField, this.name); const relation = this.dataSource.getCollection(schema.foreignCollection); return `${thisField}:${relation.pathFromChildCollection(childPath.substring(dotIndex + 1))}`; @@ -166,7 +163,7 @@ export default class RenameFieldCollectionDecorator extends CollectionDecorator if (thisPath.includes(':')) { const dotIndex = thisPath.indexOf(':'); const thisField = thisPath.substring(0, dotIndex); - const schema = this.schema.fields[thisField] as RelationSchema; + const schema = SchemaUtils.getRelation(this.schema, thisField, this.name); const relation = this.dataSource.getCollection(schema.foreignCollection); const childField = this.toChildCollection[thisField] ?? thisField; @@ -194,7 +191,7 @@ export default class RenameFieldCollectionDecorator extends CollectionDecorator for (const [childField, value] of Object.entries(childRecord)) { const thisField = this.fromChildCollection[childField] ?? childField; - const fieldSchema = schema.fields[thisField]; + const fieldSchema = SchemaUtils.getField(schema, thisField, this.name); // Perform the mapping, recurse for relations. if (fieldSchema.type === 'Column') { diff --git a/packages/datasource-customizer/src/decorators/sort-emulate/collection.ts b/packages/datasource-customizer/src/decorators/sort-emulate/collection.ts index bdc46a8fbf..f4817f22d9 100644 --- a/packages/datasource-customizer/src/decorators/sort-emulate/collection.ts +++ b/packages/datasource-customizer/src/decorators/sort-emulate/collection.ts @@ -12,7 +12,7 @@ import { Projection, RecordData, RecordUtils, - RelationSchema, + SchemaUtils, Sort, } from '@forestadmin/datasource-toolkit'; @@ -133,7 +133,7 @@ export default class SortEmulate extends CollectionDecorator { // Order by is targeting a field on another collection => recurse. if (clause.field.includes(':')) { const [prefix] = clause.field.split(':'); - const schema = this.schema.fields[prefix] as RelationSchema; + const schema = SchemaUtils.getRelation(this.schema, prefix, this.name); const association = this.dataSource.getCollection(schema.foreignCollection); return new Sort(clause) @@ -158,7 +158,11 @@ export default class SortEmulate extends CollectionDecorator { const index = path.indexOf(':'); if (index === -1) return this.sorts.has(path); - const { foreignCollection } = this.schema.fields[path.substring(0, index)] as RelationSchema; + const { foreignCollection } = SchemaUtils.getRelation( + this.schema, + path.substring(0, index), + this.name, + ); const association = this.dataSource.getCollection(foreignCollection); return association.isEmulated(path.substring(index + 1)); diff --git a/packages/datasource-customizer/src/decorators/validation/collection.ts b/packages/datasource-customizer/src/decorators/validation/collection.ts index 24d9b1c992..f733d6765f 100644 --- a/packages/datasource-customizer/src/decorators/validation/collection.ts +++ b/packages/datasource-customizer/src/decorators/validation/collection.ts @@ -9,6 +9,7 @@ import { FieldValidator, Filter, RecordData, + SchemaUtils, ValidationError, } from '@forestadmin/datasource-toolkit'; @@ -20,7 +21,7 @@ export default class ValidationDecorator extends CollectionDecorator { addValidation(name: string, validation: ValidationRule): void { FieldValidator.validate(this, name); - const field = this.childCollection.schema.fields[name] as ColumnSchema; + const field = SchemaUtils.getColumn(this.schema, name, this.name); if (field?.type !== 'Column') { throw new Error('Cannot add validators on a relation, use the foreign key instead'); @@ -49,7 +50,7 @@ export default class ValidationDecorator extends CollectionDecorator { const schema = { ...subSchema, fields: { ...subSchema.fields } }; for (const [name, rules] of Object.entries(this.validation)) { - const field = { ...schema.fields[name] } as ColumnSchema; + const field = { ...SchemaUtils.getColumn(schema, name) }; field.validation = [...(field.validation ?? []), ...rules]; schema.fields[name] = field; } diff --git a/packages/datasource-customizer/src/decorators/write/create-relations/collection.ts b/packages/datasource-customizer/src/decorators/write/create-relations/collection.ts index b49d492d91..7470a72ee6 100644 --- a/packages/datasource-customizer/src/decorators/write/create-relations/collection.ts +++ b/packages/datasource-customizer/src/decorators/write/create-relations/collection.ts @@ -6,6 +6,7 @@ import { ManyToOneSchema, OneToOneSchema, RecordData, + SchemaUtils, } from '@forestadmin/datasource-toolkit'; type RecordWithIndex = { subRecord: RecordData; index: number }; @@ -19,7 +20,7 @@ export default class CreateRelationsCollectionDecorator extends CollectionDecora // Step 2: Create the many-to-one relations, and put the foreign keys in the records await Promise.all( Object.entries(recordsByRelation) - .filter(([key]) => this.schema.fields[key].type === 'ManyToOne') + .filter(([key]) => SchemaUtils.getField(this.schema, key, this.name).type === 'ManyToOne') .map(([key, entries]) => this.createManyToOneRelation(caller, records, key, entries)), ); @@ -30,7 +31,7 @@ export default class CreateRelationsCollectionDecorator extends CollectionDecora // Note: the createOneToOneRelation method modifies the recordsWithPk array in place! await Promise.all( Object.entries(recordsByRelation) - .filter(([key]) => this.schema.fields[key].type === 'OneToOne') + .filter(([key]) => SchemaUtils.getField(this.schema, key, this.name).type === 'OneToOne') .map(([key, entries]) => this.createOneToOneRelation(caller, recordsWithPk, key, entries)), ); @@ -42,7 +43,9 @@ export default class CreateRelationsCollectionDecorator extends CollectionDecora for (const [index, record] of records.entries()) { for (const [key, subRecord] of Object.entries(record)) { - if (this.schema.fields[key].type !== 'Column') { + const field = SchemaUtils.getField(this.schema, key, this.name); + + if (field.type !== 'Column') { recordsByRelation[key] ??= []; recordsByRelation[key].push({ subRecord, index }); delete record[key]; @@ -59,7 +62,7 @@ export default class CreateRelationsCollectionDecorator extends CollectionDecora key: string, entries: RecordWithIndex[], ): Promise { - const schema = this.schema.fields[key] as ManyToOneSchema; + const schema = SchemaUtils.getRelation(this.schema, key, this.name) as ManyToOneSchema; const relation = this.dataSource.getCollection(schema.foreignCollection); const creations = entries.filter(({ index }) => !records[index][schema.foreignKey]); const updates = entries.filter(({ index }) => records[index][schema.foreignKey]); @@ -94,7 +97,7 @@ export default class CreateRelationsCollectionDecorator extends CollectionDecora key: string, entries: RecordWithIndex[], ): Promise { - const schema = this.schema.fields[key] as OneToOneSchema; + const schema = SchemaUtils.getRelation(this.schema, key, this.name) as OneToOneSchema; const relation = this.dataSource.getCollection(schema.foreignCollection); // Set origin key in the related record diff --git a/packages/datasource-customizer/src/decorators/write/update-relations/collection.ts b/packages/datasource-customizer/src/decorators/write/update-relations/collection.ts index 45ad85cc9b..6bc3455b80 100644 --- a/packages/datasource-customizer/src/decorators/write/update-relations/collection.ts +++ b/packages/datasource-customizer/src/decorators/write/update-relations/collection.ts @@ -7,21 +7,26 @@ import { OneToOneSchema, Projection, RecordData, + SchemaUtils, } from '@forestadmin/datasource-toolkit'; export default class UpdateRelationCollectionDecorator extends CollectionDecorator { override async update(caller: Caller, filter: Filter, patch: RecordData): Promise { // Step 1: Perform the normal update - if (Object.keys(patch).some(key => this.schema.fields[key].type === 'Column')) { + const patches = Object.keys(patch); + + if (patches.some(key => SchemaUtils.getField(this.schema, key, this.name).type === 'Column')) { const patchWithoutRelations = Object.keys(patch).reduce((memo, key) => { - return this.schema.fields[key].type === 'Column' ? { ...memo, [key]: patch[key] } : memo; + return SchemaUtils.getField(this.schema, key, this.name).type === 'Column' + ? { ...memo, [key]: patch[key] } + : memo; }, {}); await this.childCollection.update(caller, filter, patchWithoutRelations); } // Step 2: Perform additional updates for relations - if (Object.keys(patch).some(key => this.schema.fields[key].type !== 'Column')) { + if (patches.some(key => SchemaUtils.getField(this.schema, key, this.name).type !== 'Column')) { // Fetch the records that will be updated, to know which relations need to be created/updated const projection = this.buildProjection(patch); const records = await this.list(caller, filter, projection); @@ -29,7 +34,7 @@ export default class UpdateRelationCollectionDecorator extends CollectionDecorat // Perform the updates for each relation await Promise.all( Object.keys(patch) - .filter(key => this.schema.fields[key].type !== 'Column') + .filter(key => SchemaUtils.getField(this.schema, key, this.name).type !== 'Column') .map(key => this.createOrUpdateRelation(caller, records, key, patch[key])), ); } @@ -45,7 +50,7 @@ export default class UpdateRelationCollectionDecorator extends CollectionDecorat let projection = new Projection().withPks(this); for (const key of Object.keys(patch)) { - const schema = this.schema.fields[key]; + const schema = SchemaUtils.getField(this.schema, key, this.name); if (schema.type !== 'Column') { const relation = this.dataSource.getCollection(schema.foreignCollection); @@ -74,7 +79,9 @@ export default class UpdateRelationCollectionDecorator extends CollectionDecorat key: string, patch: RecordData, ): Promise { - const schema = this.schema.fields[key] as ManyToOneSchema | OneToOneSchema; + const schema = SchemaUtils.getRelation(this.schema, key, this.name) as + | ManyToOneSchema + | OneToOneSchema; const relation = this.dataSource.getCollection(schema.foreignCollection); const creates = records.filter(r => r[key] === null); const updates = records.filter(r => r[key] !== null); diff --git a/packages/datasource-customizer/src/decorators/write/write-replace/collection.ts b/packages/datasource-customizer/src/decorators/write/write-replace/collection.ts index 616706f0b8..c74618a6bf 100644 --- a/packages/datasource-customizer/src/decorators/write/write-replace/collection.ts +++ b/packages/datasource-customizer/src/decorators/write/write-replace/collection.ts @@ -2,12 +2,12 @@ import { Caller, CollectionDecorator, CollectionSchema, - ColumnSchema, DataSourceDecorator, FieldValidator, Filter, RecordData, RecordValidator, + SchemaUtils, } from '@forestadmin/datasource-toolkit'; import WriteCustomizationContext from './context'; @@ -32,7 +32,7 @@ export default class WriteReplacerCollectionDecorator extends CollectionDecorato for (const [fieldName, handler] of Object.entries(this.handlers)) { schema.fields[fieldName] = { - ...(schema.fields[fieldName] as ColumnSchema), + ...SchemaUtils.getColumn(schema, fieldName, this.name), isReadOnly: handler === null, }; } @@ -87,7 +87,7 @@ export default class WriteReplacerCollectionDecorator extends CollectionDecorato if (used.includes(key)) throw new Error(`Cycle detected: ${used.join(' -> ')}.`); const { record, action, caller } = context; - const schema = this.schema.fields[key]; + const schema = SchemaUtils.getField(this.schema, key, this.name); // Handle Column fields. if (schema?.type === 'Column') { @@ -114,8 +114,6 @@ export default class WriteReplacerCollectionDecorator extends CollectionDecorato return { [key]: await relation.rewritePatch(caller, action, record[key]) }; } - - throw new Error(`Unknown field: "${key}"`); } /** diff --git a/packages/datasource-customizer/src/plugins/import-field.ts b/packages/datasource-customizer/src/plugins/import-field.ts index 144af09c71..9977fe1216 100644 --- a/packages/datasource-customizer/src/plugins/import-field.ts +++ b/packages/datasource-customizer/src/plugins/import-field.ts @@ -1,4 +1,4 @@ -import { ColumnSchema, MissingFieldError, RecordUtils } from '@forestadmin/datasource-toolkit'; +import { ColumnSchema, RecordUtils, SchemaUtils } from '@forestadmin/datasource-toolkit'; import CollectionCustomizer from '../collection-customizer'; import DataSourceCustomizer from '../datasource-customizer'; @@ -17,11 +17,7 @@ export default async function importField< const { schema } = options.path.split(':').reduce<{ collection?: string; schema?: ColumnSchema }>( (memo, field) => { const collection = dataSourceCustomizer.getCollection(memo.collection as TCollectionName); - const fieldSchema = collection.schema.fields[field]; - - if (fieldSchema === undefined) { - throw new MissingFieldError(field, memo.collection); - } + const fieldSchema = SchemaUtils.getField(collection.schema, field, memo.collection); if (fieldSchema.type === 'Column') return { schema: fieldSchema }; diff --git a/packages/datasource-customizer/test/collection-customizer.test.ts b/packages/datasource-customizer/test/collection-customizer.test.ts index 35c8cb9bfa..50aa214bd0 100644 --- a/packages/datasource-customizer/test/collection-customizer.test.ts +++ b/packages/datasource-customizer/test/collection-customizer.test.ts @@ -327,9 +327,7 @@ describe('Builder > Collection', () => { const { dsc, customizer } = await setup(); customizer.importField('translatorName', { path: 'doesNotExistPath' }); - await expect(dsc.getDataSource(logger)).rejects.toThrow( - new MissingFieldError('doesNotExistPath', 'authors'), - ); + await expect(dsc.getDataSource(logger)).rejects.toThrow(MissingFieldError); }); }); }); diff --git a/packages/datasource-customizer/test/context/wrapper.test.ts b/packages/datasource-customizer/test/context/wrapper.test.ts index c050a47736..b519fd12c0 100644 --- a/packages/datasource-customizer/test/context/wrapper.test.ts +++ b/packages/datasource-customizer/test/context/wrapper.test.ts @@ -5,6 +5,7 @@ import { ConditionTreeLeaf, DataSource, Filter, + MissingFieldError, Page, PaginatedFilter, Projection, @@ -168,7 +169,7 @@ describe('RelaxedWrappers', () => { test('should validate patch operation before forwarding update', async () => { await expect(() => relaxed.update({ segment: 'some_segment' }, { nonexistingField: 'newValue' }), - ).toThrow('Unknown field "nonexistingField"'); + ).toThrow(MissingFieldError); expect(collection.update).not.toHaveBeenCalled(); }); diff --git a/packages/datasource-customizer/test/decorators/computed/collection.test.ts b/packages/datasource-customizer/test/decorators/computed/collection.test.ts index 8da78471d9..5cc8b3c9b4 100644 --- a/packages/datasource-customizer/test/decorators/computed/collection.test.ts +++ b/packages/datasource-customizer/test/decorators/computed/collection.test.ts @@ -103,7 +103,7 @@ describe('ComputedDecorator', () => { dependencies: ['__nonExisting__'], getValues: () => Promise.reject(), }); - }).toThrow(new MissingFieldError('__nonExisting__', 'books')); + }).toThrow(MissingFieldError); }); test('should throw if defining a field with invalid dependencies', () => { diff --git a/packages/datasource-customizer/test/decorators/operators-emulate/collection.test.ts b/packages/datasource-customizer/test/decorators/operators-emulate/collection.test.ts index 55e73caacc..92b10f4f2a 100644 --- a/packages/datasource-customizer/test/decorators/operators-emulate/collection.test.ts +++ b/packages/datasource-customizer/test/decorators/operators-emulate/collection.test.ts @@ -8,6 +8,7 @@ import { PaginatedFilter, Projection, RecordData, + RelationFieldAccessDeniedError, } from '@forestadmin/datasource-toolkit'; import * as factories from '@forestadmin/datasource-toolkit/dist/test/__factories__'; @@ -116,7 +117,7 @@ describe('OperatorsEmulateCollectionDecorator', () => { test('emulateFieldOperator() should throw if the field does not exists', () => { expect(() => newBooks.emulateFieldOperator('__dontExist', 'Equal')).toThrow( - new MissingFieldError('__dontExist', 'books'), + MissingFieldError, ); }); @@ -128,7 +129,7 @@ describe('OperatorsEmulateCollectionDecorator', () => { test('emulateFieldOperator() should throw if the field is in a relation', () => { expect(() => newBooks.emulateFieldOperator('author:firstName', 'Equal')).toThrow( - 'Cannot replace operator for relation', + RelationFieldAccessDeniedError, ); }); diff --git a/packages/datasource-customizer/test/decorators/override/collection.test.ts b/packages/datasource-customizer/test/decorators/override/collection.test.ts index c7312ec076..63d83abd4d 100644 --- a/packages/datasource-customizer/test/decorators/override/collection.test.ts +++ b/packages/datasource-customizer/test/decorators/override/collection.test.ts @@ -137,7 +137,7 @@ describe('OverrideCollectionDecorator', () => { describe('when the handler does not return a primary key in each record', () => { it('should throw an error', async () => { - const handler = jest.fn().mockResolvedValue([{ id: 'valid' }, { noId: 1 }]); + const handler = jest.fn().mockResolvedValue([{ id: 'valid' }, { description: 1 }]); const currentCaller = factories.caller.build(); const currentData = [factories.recordData.build()]; diff --git a/packages/datasource-customizer/test/decorators/publication/collection.test.ts b/packages/datasource-customizer/test/decorators/publication/collection.test.ts index 46e3d82067..5bb8b11856 100644 --- a/packages/datasource-customizer/test/decorators/publication/collection.test.ts +++ b/packages/datasource-customizer/test/decorators/publication/collection.test.ts @@ -85,9 +85,7 @@ describe('PublicationCollectionDecorator', () => { }); test('should throw when hiding a field which does not exists', () => { - expect(() => newPersons.changeFieldVisibility('unknown', false)).toThrow( - new MissingFieldError('unknown', 'persons'), - ); + expect(() => newPersons.changeFieldVisibility('unknown', false)).toThrow(MissingFieldError); }); test('should throw when hiding the primary key', () => { diff --git a/packages/datasource-customizer/test/decorators/relation/collection.test.ts b/packages/datasource-customizer/test/decorators/relation/collection.test.ts index b1527eb64a..c8d5b8697d 100644 --- a/packages/datasource-customizer/test/decorators/relation/collection.test.ts +++ b/packages/datasource-customizer/test/decorators/relation/collection.test.ts @@ -7,7 +7,7 @@ import { DataSourceDecorator, Filter, ManyToManySchema, - MissingFieldError, + MissingColumnError, PaginatedFilter, Projection, Sort, @@ -166,7 +166,7 @@ describe('RelationCollectionDecorator', () => { foreignCollection: 'passports', originKey: '__nonExisting__', }), - ).toThrow(new MissingFieldError('__nonExisting__', 'passports')); + ).toThrow(MissingColumnError); }); }); @@ -283,7 +283,7 @@ describe('RelationCollectionDecorator', () => { foreignCollection: 'persons', foreignKey: '__nonExisting__', }), - ).toThrow(new MissingFieldError('__nonExisting__', 'passports')); + ).toThrow(MissingColumnError); }); }); @@ -351,7 +351,7 @@ describe('RelationCollectionDecorator', () => { originKey: '__nonExisting__', throughCollection: 'passports', } as ManyToManySchema), - ).toThrow(new MissingFieldError('__nonExisting__', 'passports')); + ).toThrow(MissingColumnError); }); test('should throw with a non existent fk', () => { @@ -363,7 +363,7 @@ describe('RelationCollectionDecorator', () => { originKey: 'ownerId', throughCollection: 'passports', } as ManyToManySchema), - ).toThrow(new MissingFieldError('__nonExisting__', 'passports')); + ).toThrow(MissingColumnError); }); }); diff --git a/packages/datasource-customizer/test/decorators/rename-field/collection.test.ts b/packages/datasource-customizer/test/decorators/rename-field/collection.test.ts index ba1dffb342..890a297ec9 100644 --- a/packages/datasource-customizer/test/decorators/rename-field/collection.test.ts +++ b/packages/datasource-customizer/test/decorators/rename-field/collection.test.ts @@ -131,14 +131,12 @@ describe('RenameFieldCollectionDecorator', () => { }); test('should throw when renaming a field which does not exists', () => { - expect(() => newPersons.renameField('unknown', 'somethingnew')).toThrow( - new MissingFieldError('unknown'), - ); + expect(() => newPersons.renameField('unknown', 'somethingnew')).toThrow(MissingFieldError); }); test('should throw when renaming a field using an older name', () => { newPersons.renameField('id', 'key'); - expect(() => newPersons.renameField('id', 'primaryKey')).toThrow(new MissingFieldError('id')); + expect(() => newPersons.renameField('id', 'primaryKey')).toThrow(MissingFieldError); }); test('should throw when renaming with a name including space', () => { diff --git a/packages/datasource-customizer/test/decorators/segment/collection.test.ts b/packages/datasource-customizer/test/decorators/segment/collection.test.ts index e1a4772a27..e855627f23 100644 --- a/packages/datasource-customizer/test/decorators/segment/collection.test.ts +++ b/packages/datasource-customizer/test/decorators/segment/collection.test.ts @@ -1,4 +1,4 @@ -import { Collection, DataSource } from '@forestadmin/datasource-toolkit'; +import { Collection, DataSource, MissingFieldError } from '@forestadmin/datasource-toolkit'; import * as factories from '@forestadmin/datasource-toolkit/dist/test/__factories__'; import SegmentCollectionDecorator from '../../../src/decorators/segment/collection'; @@ -102,7 +102,7 @@ describe('SegmentCollectionDecorator', () => { factories.caller.build(), factories.filter.build({ segment: 'segmentName' }), ), - ).rejects.toThrow("Column not found 'books.do not exists'"); + ).rejects.toThrow(MissingFieldError); expect(conditionTreeGenerator).toHaveBeenCalled(); }); diff --git a/packages/datasource-customizer/test/decorators/sort-emulate/collection.test.ts b/packages/datasource-customizer/test/decorators/sort-emulate/collection.test.ts index d00fd50cc4..a3107eb517 100644 --- a/packages/datasource-customizer/test/decorators/sort-emulate/collection.test.ts +++ b/packages/datasource-customizer/test/decorators/sort-emulate/collection.test.ts @@ -94,9 +94,7 @@ describe('SortEmulationDecoratorCollection', () => { }); test('emulateFieldSorting() should throw if the field does not exists', () => { - expect(() => newBooks.emulateFieldSorting('__dontExist')).toThrow( - new MissingFieldError('__dontExist', 'books'), - ); + expect(() => newBooks.emulateFieldSorting('__dontExist')).toThrow(MissingFieldError); }); test('emulateFieldSorting() should throw if the field is a relation', () => { diff --git a/packages/datasource-customizer/test/decorators/validation/collection.test.ts b/packages/datasource-customizer/test/decorators/validation/collection.test.ts index 343a86710b..0ae9cd23af 100644 --- a/packages/datasource-customizer/test/decorators/validation/collection.test.ts +++ b/packages/datasource-customizer/test/decorators/validation/collection.test.ts @@ -3,6 +3,7 @@ import { DataSource, DataSourceDecorator, MissingFieldError, + RelationFieldAccessDeniedError, } from '@forestadmin/datasource-toolkit'; import * as factories from '@forestadmin/datasource-toolkit/dist/test/__factories__'; @@ -54,7 +55,7 @@ describe('SortEmulationDecoratorCollection', () => { test('addValidation() should throw if the field does not exists', () => { expect(() => newBooks.addValidation('__dontExist', { operator: 'Present' })).toThrow( - new MissingFieldError('__dontExist', 'books'), + MissingFieldError, ); }); @@ -72,7 +73,7 @@ describe('SortEmulationDecoratorCollection', () => { test('addValidation() should throw if the field is in a relation', () => { expect(() => newBooks.addValidation('author:firstName', { operator: 'Present' })).toThrow( - 'Cannot add validators on a relation, use the foreign key instead', + RelationFieldAccessDeniedError, ); }); diff --git a/packages/datasource-customizer/test/decorators/write/write-replace/collection_basics.test.ts b/packages/datasource-customizer/test/decorators/write/write-replace/collection_basics.test.ts index 829113cefd..16a6305864 100644 --- a/packages/datasource-customizer/test/decorators/write/write-replace/collection_basics.test.ts +++ b/packages/datasource-customizer/test/decorators/write/write-replace/collection_basics.test.ts @@ -24,7 +24,7 @@ describe('WriteDecorator > When their are no relations', () => { const decorator = new WriteDecorator(collection, dataSource); expect(() => decorator.replaceFieldWriting('inexistant', () => ({}))).toThrow( - new MissingFieldError('inexistant', 'books'), + MissingFieldError, ); }); diff --git a/packages/datasource-customizer/test/decorators/write/write-replace/collection_simple-creations.test.ts b/packages/datasource-customizer/test/decorators/write/write-replace/collection_simple-creations.test.ts index da0595abd5..3389717a19 100644 --- a/packages/datasource-customizer/test/decorators/write/write-replace/collection_simple-creations.test.ts +++ b/packages/datasource-customizer/test/decorators/write/write-replace/collection_simple-creations.test.ts @@ -1,4 +1,4 @@ -import { Collection } from '@forestadmin/datasource-toolkit'; +import { Collection, MissingFieldError } from '@forestadmin/datasource-toolkit'; import * as factories from '@forestadmin/datasource-toolkit/dist/test/__factories__'; import WriteDataSourceDecorator from '../../../../src/decorators/write/datasource'; @@ -160,17 +160,13 @@ describe('WriteDecorator > Create with no relations', () => { test('when the handler returns non existent fields', async () => { decorator.replaceFieldWriting('age', () => ({ author: 'Asimov' })); - await expect(decorator.create(caller, [{ age: '10' }])).rejects.toThrow( - 'Unknown field: "author"', - ); + await expect(decorator.create(caller, [{ age: '10' }])).rejects.toThrow(MissingFieldError); }); test('when the handler returns non existent relations', async () => { decorator.replaceFieldWriting('age', () => ({ author: { lastname: 'Asimov' } })); - await expect(decorator.create(caller, [{ age: '10' }])).rejects.toThrow( - 'Unknown field: "author"', - ); + await expect(decorator.create(caller, [{ age: '10' }])).rejects.toThrow(MissingFieldError); }); test('if the customer attemps to update the patch in the handler', async () => { diff --git a/packages/datasource-customizer/test/decorators/write/write-replace/collection_simple-updates.test.ts b/packages/datasource-customizer/test/decorators/write/write-replace/collection_simple-updates.test.ts index db49dcc9f4..fa0b3b05aa 100644 --- a/packages/datasource-customizer/test/decorators/write/write-replace/collection_simple-updates.test.ts +++ b/packages/datasource-customizer/test/decorators/write/write-replace/collection_simple-updates.test.ts @@ -1,4 +1,4 @@ -import { Collection } from '@forestadmin/datasource-toolkit'; +import { Collection, MissingFieldError } from '@forestadmin/datasource-toolkit'; import * as factories from '@forestadmin/datasource-toolkit/dist/test/__factories__'; import WriteReplacerCollectionDecorator from '../../../../src/decorators/write/write-replace/collection'; @@ -155,7 +155,7 @@ describe('WriteDecorator > Simple structure', () => { decorator.replaceFieldWriting('age', () => ({ author: 'Asimov' })); await expect(decorator.update(caller, filter, { age: '10' })).rejects.toThrow( - 'Unknown field: "author"', + MissingFieldError, ); }); @@ -163,7 +163,7 @@ describe('WriteDecorator > Simple structure', () => { decorator.replaceFieldWriting('age', () => ({ author: { lastname: 'Asimov' } })); await expect(decorator.update(caller, filter, { age: '10' })).rejects.toThrow( - 'Unknown field: "author"', + MissingFieldError, ); }); diff --git a/packages/datasource-customizer/test/plugins/import-field.test.ts b/packages/datasource-customizer/test/plugins/import-field.test.ts index b3f1d52055..4a56fff759 100644 --- a/packages/datasource-customizer/test/plugins/import-field.test.ts +++ b/packages/datasource-customizer/test/plugins/import-field.test.ts @@ -53,6 +53,6 @@ describe('importField', () => { path: 'INVALID', name: 'NOPE', }), - ).rejects.toThrow(new MissingFieldError('INVALID', 'collection1')); + ).rejects.toThrow(MissingFieldError); }); }); diff --git a/packages/datasource-replica/src/decorators/sync/collection.ts b/packages/datasource-replica/src/decorators/sync/collection.ts index 8009c71cba..c02e77e25f 100644 --- a/packages/datasource-replica/src/decorators/sync/collection.ts +++ b/packages/datasource-replica/src/decorators/sync/collection.ts @@ -1,17 +1,18 @@ import type TriggerSyncDataSourceDecorator from './data-source'; import type { TFilter, TPaginatedFilter } from '@forestadmin/datasource-customizer'; -import type { + +import { AggregateResult, Aggregation, Caller, + CollectionDecorator, Filter, PaginatedFilter, Projection, RecordData, + SchemaUtils, } from '@forestadmin/datasource-toolkit'; -import { CollectionDecorator } from '@forestadmin/datasource-toolkit'; - export default class SyncCollectionDecorator extends CollectionDecorator { override dataSource: TriggerSyncDataSourceDecorator; @@ -137,7 +138,11 @@ export default class SyncCollectionDecorator extends CollectionDecorator { private getCollectionFromPath(path: string): string { const [prefix, suffix] = path.split(/:(.*)/); - const schema = this.childCollection.schema.fields[prefix]; + const schema = SchemaUtils.getField( + this.childCollection.schema, + prefix, + this.childCollection.name, + ); // FIXME need to handle many to many relationships here // the through table is not included, and it should be diff --git a/packages/datasource-toolkit/src/base-collection.ts b/packages/datasource-toolkit/src/base-collection.ts index f7a96b5145..cb66e73bf6 100644 --- a/packages/datasource-toolkit/src/base-collection.ts +++ b/packages/datasource-toolkit/src/base-collection.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ +import { SchemaUtils } from './index'; import { ActionField, ActionResult } from './interfaces/action'; import { Caller } from './interfaces/caller'; import { Chart } from './interfaces/chart'; @@ -47,9 +48,7 @@ export default abstract class BaseCollection implements Collection { } protected addField(name: string, schema: FieldSchema): void { - const fieldSchema = this.schema.fields[name]; - - if (fieldSchema !== undefined) throw new Error(`Field "${name}" already defined in collection`); + SchemaUtils.throwIfAlreadyDefinedField(this.schema, name, this.name); this.schema.fields[name] = schema; } diff --git a/packages/datasource-toolkit/src/errors.ts b/packages/datasource-toolkit/src/errors.ts index fb7fac42c9..1668e41ebd 100644 --- a/packages/datasource-toolkit/src/errors.ts +++ b/packages/datasource-toolkit/src/errors.ts @@ -77,8 +77,64 @@ export class MissingSchemaElementError extends ValidationError {} export class MissingCollectionError extends MissingSchemaElementError {} +function buildPath(fieldName: string, collectionName?: string): string { + return collectionName ? `${collectionName}.${fieldName}` : fieldName; +} + +type MissingFieldErrorOptions = { + fieldName: string; + availableFields: string[]; + collectionName?: string; +}; + +function buildMessageMissingElement(options: { + typeOfField: 'Field' | 'Column' | 'Relation'; + fieldName: string; + availableFields: string[]; + collectionName?: string; +}): string { + const { typeOfField, fieldName, availableFields, collectionName } = options; + const path = buildPath(fieldName, collectionName); + + return `The '${path}' ${typeOfField.toLowerCase()} was not found. Available ${typeOfField.toLowerCase()}s are: [${availableFields}]. Please check if the ${typeOfField.toLowerCase()} name is correct.`; +} + +export class RelationFieldAccessDeniedError extends ValidationError { + constructor(options: Pick) { + const { fieldName, collectionName } = options; + const path = buildPath(fieldName, collectionName); + + super( + `Access to the '${path}' field is denied. You are trying to access a field from a related entity, but this is not allowed in the current context. Please verify the field name and context of use.`, + ); + } +} + export class MissingFieldError extends MissingSchemaElementError { - constructor(field: string, collection?: string) { - super(`Field "${field}" not found${collection ? ` in collection "${collection}"` : ''}.`); + constructor(options: MissingFieldErrorOptions) { + super(buildMessageMissingElement({ typeOfField: 'Field', ...options })); + } +} + +export class MissingColumnError extends MissingSchemaElementError { + constructor(options: MissingFieldErrorOptions) { + super(buildMessageMissingElement({ typeOfField: 'Column', ...options })); + } +} + +export class MissingRelationError extends MissingSchemaElementError { + constructor(options: MissingFieldErrorOptions) { + super(buildMessageMissingElement({ typeOfField: 'Relation', ...options })); + } +} + +export class AlreadyDefinedFieldError extends ValidationError { + constructor(options: { fieldName: string; collectionName?: string }) { + const { fieldName, collectionName } = options; + const path = buildPath(fieldName, collectionName); + + super( + `The '${path}' field is already defined. Please check if the field name is correct and unique.`, + ); } } diff --git a/packages/datasource-toolkit/src/interfaces/query/condition-tree/factory.ts b/packages/datasource-toolkit/src/interfaces/query/condition-tree/factory.ts index 0006e14b0e..f9158011c3 100644 --- a/packages/datasource-toolkit/src/interfaces/query/condition-tree/factory.ts +++ b/packages/datasource-toolkit/src/interfaces/query/condition-tree/factory.ts @@ -5,7 +5,7 @@ import { Operator } from './nodes/operators'; import RecordUtils from '../../../utils/record'; import SchemaUtils from '../../../utils/schema'; import { CompositeId, RecordData } from '../../record'; -import { CollectionSchema, ColumnSchema } from '../../schema'; +import { CollectionSchema } from '../../schema'; export type GenericTreeBranch = { aggregator: Aggregator; conditions: Array }; export type GenericTreeLeaf = { field: string; operator: Operator; value?: unknown }; @@ -29,7 +29,7 @@ export default class ConditionTreeFactory { } for (const name of primaryKeyNames) { - const operators = (schema.fields[name] as ColumnSchema).filterOperators; + const operators = SchemaUtils.getColumn(schema, name).filterOperators; if (!operators?.has('Equal') || !operators?.has('In')) { throw new Error(`Field '${name}' must support operators: ['Equal', 'In']`); diff --git a/packages/datasource-toolkit/src/interfaces/query/filter/factory.ts b/packages/datasource-toolkit/src/interfaces/query/filter/factory.ts index d534d83383..3dc098d9dd 100644 --- a/packages/datasource-toolkit/src/interfaces/query/filter/factory.ts +++ b/packages/datasource-toolkit/src/interfaces/query/filter/factory.ts @@ -102,7 +102,11 @@ export default class FilterFactory { caller: Caller, baseForeignFilter: PaginatedFilter, ): Promise { - const relation = collection.schema.fields[relationName] as ManyToManySchema; + const relation = SchemaUtils.getRelation( + collection.schema, + relationName, + collection.name, + ) as ManyToManySchema; const originValue = await CollectionUtils.getValue( collection, caller, @@ -113,8 +117,12 @@ export default class FilterFactory { // Optimization for many to many when there is not search/segment (saves one query) if (foreignRelation && baseForeignFilter.isNestable) { - const foreignKeySchema = collection.dataSource.getCollection(relation.throughCollection) - .schema.fields[relation.foreignKey]; + const foreignCollection = collection.dataSource.getCollection(relation.throughCollection); + const foreignKeySchema = SchemaUtils.getField( + foreignCollection.schema, + relation.foreignKey, + foreignCollection.name, + ); const baseThroughFilter = baseForeignFilter.nest(foreignRelation); let conditionTree = ConditionTreeFactory.intersect( @@ -189,7 +197,11 @@ export default class FilterFactory { } else { // ManyToMany case (more complicated...) const through = collection.dataSource.getCollection(relation.throughCollection); - const foreignKeySchema = through.schema.fields[relation.foreignKey]; + const foreignKeySchema = SchemaUtils.getField( + through.schema, + relation.foreignKey, + through.name, + ); let throughTree: ConditionTree = new ConditionTreeLeaf( relation.originKey, 'Equal', diff --git a/packages/datasource-toolkit/src/interfaces/query/projection/factory.ts b/packages/datasource-toolkit/src/interfaces/query/projection/factory.ts index dc21a337de..52d9b103d2 100644 --- a/packages/datasource-toolkit/src/interfaces/query/projection/factory.ts +++ b/packages/datasource-toolkit/src/interfaces/query/projection/factory.ts @@ -1,4 +1,5 @@ import Projection from '.'; +import { SchemaUtils } from '../../../index'; import { Collection } from '../../collection'; export default class ProjectionFactory { @@ -28,10 +29,6 @@ export default class ProjectionFactory { } static columns(collection: Collection): Projection { - return new Projection( - ...Object.keys(collection.schema.fields).filter( - f => collection.schema.fields[f].type === 'Column', - ), - ); + return new Projection(...SchemaUtils.getColumnNames(collection.schema)); } } diff --git a/packages/datasource-toolkit/src/interfaces/query/projection/index.ts b/packages/datasource-toolkit/src/interfaces/query/projection/index.ts index 3043d5df9d..9ce8c7f3b5 100644 --- a/packages/datasource-toolkit/src/interfaces/query/projection/index.ts +++ b/packages/datasource-toolkit/src/interfaces/query/projection/index.ts @@ -1,7 +1,6 @@ import SchemaUtils from '../../../utils/schema'; import { Collection } from '../../collection'; import { RecordData } from '../../record'; -import { RelationSchema } from '../../schema'; export default class Projection extends Array { get columns(): string[] { @@ -54,7 +53,7 @@ export default class Projection extends Array { } for (const [relation, projection] of Object.entries(this.relations)) { - const schema = collection.schema.fields[relation] as RelationSchema; + const schema = SchemaUtils.getRelation(collection.schema, relation, collection.name); const association = collection.dataSource.getCollection(schema.foreignCollection); const projectionWithPk = projection.withPks(association).nest(relation); diff --git a/packages/datasource-toolkit/src/interfaces/query/sort/factory.ts b/packages/datasource-toolkit/src/interfaces/query/sort/factory.ts index 2463bbdbd8..5c47c227d4 100644 --- a/packages/datasource-toolkit/src/interfaces/query/sort/factory.ts +++ b/packages/datasource-toolkit/src/interfaces/query/sort/factory.ts @@ -1,14 +1,13 @@ import Sort from '.'; import SchemaUtils from '../../../utils/schema'; import { Collection } from '../../collection'; -import { ColumnSchema } from '../../schema'; export default class SortFactory { static byPrimaryKeys(collection: Collection): Sort { return new Sort( ...SchemaUtils.getPrimaryKeys(collection.schema) .map(pk => - (collection.schema.fields[pk] as ColumnSchema).isSortable + SchemaUtils.getColumn(collection.schema, pk, collection.name).isSortable ? { field: pk, ascending: true, diff --git a/packages/datasource-toolkit/src/utils/collection.ts b/packages/datasource-toolkit/src/utils/collection.ts index d9ce79a3d4..7d7cbbec5c 100644 --- a/packages/datasource-toolkit/src/utils/collection.ts +++ b/packages/datasource-toolkit/src/utils/collection.ts @@ -16,17 +16,13 @@ export default class CollectionUtils { const index = path.indexOf(':'); if (index === -1) { - if (!fields[path]) throw new Error(`Column not found '${collection.name}.${path}'`); + SchemaUtils.throwIfMissingField(collection.schema, path, collection.name); return fields[path]; } const associationName = path.substring(0, index); - const schema = fields[associationName] as RelationSchema; - - if (!schema) { - throw new Error(`Relation not found '${collection.name}.${associationName}'`); - } + const schema = SchemaUtils.getRelation(collection.schema, associationName, collection.name); if (schema.type !== 'ManyToOne' && schema.type !== 'OneToOne') { throw new Error( @@ -41,7 +37,7 @@ export default class CollectionUtils { } static getInverseRelation(collection: Collection, relationName: string): string { - const relation = collection.schema.fields[relationName] as RelationSchema; + const relation = SchemaUtils.getRelation(collection.schema, relationName, collection.name); const foreignCollection = collection.dataSource.getCollection(relation.foreignCollection); const inverse = Object.entries(foreignCollection.schema.fields).find( ([, field]: [string, RelationSchema]) => { @@ -73,7 +69,7 @@ export default class CollectionUtils { } static getThroughOrigin(collection: Collection, relationName: string): string { - const relation = collection.schema.fields[relationName]; + const relation = SchemaUtils.getRelation(collection.schema, relationName, collection.name); if (relation.type !== 'ManyToMany') throw new Error('Relation must be many to many'); const throughCollection = collection.dataSource.getCollection(relation.throughCollection); @@ -92,7 +88,7 @@ export default class CollectionUtils { } static getThroughTarget(collection: Collection, relationName: string): string { - const relation = collection.schema.fields[relationName]; + const relation = SchemaUtils.getRelation(collection.schema, relationName, collection.name); if (relation.type !== 'ManyToMany') throw new Error('Relation must be many to many'); const throughCollection = collection.dataSource.getCollection(relation.throughCollection); diff --git a/packages/datasource-toolkit/src/utils/schema.ts b/packages/datasource-toolkit/src/utils/schema.ts index b99dd56594..394ead518d 100644 --- a/packages/datasource-toolkit/src/utils/schema.ts +++ b/packages/datasource-toolkit/src/utils/schema.ts @@ -1,18 +1,108 @@ -import { CollectionSchema, ManyToManySchema, OneToManySchema } from '../interfaces/schema'; +import { + AlreadyDefinedFieldError, + MissingColumnError, + MissingFieldError, + MissingRelationError, + RelationFieldAccessDeniedError, +} from '../errors'; +import { + CollectionSchema, + ColumnSchema, + FieldSchema, + ManyToManySchema, + OneToManySchema, + RelationSchema, +} from '../interfaces/schema'; export default class SchemaUtils { + static throwIfAlreadyDefinedField( + schema: CollectionSchema, + fieldName: string, + collectionName?: string, + ): void { + SchemaUtils.throwIfAccessingFieldFromRelation(fieldName, collectionName); + + if (schema.fields[fieldName]) { + throw new AlreadyDefinedFieldError({ fieldName, collectionName }); + } + } + + static throwIfMissingField( + schema: CollectionSchema, + fieldName: string, + collectionName?: string, + ): void { + SchemaUtils.throwIfAccessingFieldFromRelation(fieldName, collectionName); + + if (!schema.fields[fieldName]) { + throw new MissingFieldError({ + fieldName, + availableFields: Object.keys(schema.fields), + collectionName, + }); + } + } + + static getField( + schema: CollectionSchema, + fieldName: string, + collectionName?: string, + ): FieldSchema { + SchemaUtils.throwIfMissingField(schema, fieldName, collectionName); + + return schema.fields[fieldName]; + } + + static getColumn( + schema: CollectionSchema, + fieldName: string, + collectionName?: string, + ): ColumnSchema { + SchemaUtils.throwIfAccessingFieldFromRelation(fieldName, collectionName); + + const columns = SchemaUtils.getColumnNames(schema); + + if (!columns.find(name => name === fieldName)) { + throw new MissingColumnError({ fieldName, availableFields: columns, collectionName }); + } + + return schema.fields[fieldName] as ColumnSchema; + } + + static getRelation( + schema: CollectionSchema, + relationName: string, + collectionName?: string, + ): RelationSchema { + SchemaUtils.throwIfAccessingFieldFromRelation(relationName, collectionName); + + const relations = Object.keys(schema.fields).filter( + name => schema.fields[name].type !== 'Column', + ); + + if (!relations.find(name => name === relationName)) { + throw new MissingRelationError({ + fieldName: relationName, + availableFields: relations, + collectionName, + }); + } + + return schema.fields[relationName] as RelationSchema; + } + static getPrimaryKeys(schema: CollectionSchema): string[] { return Object.keys(schema.fields).filter(name => this.isPrimaryKey(schema, name)); } static isPrimaryKey(schema: CollectionSchema, fieldName: string): boolean { - const field = schema.fields[fieldName]; + const field = this.getField(schema, fieldName); return field.type === 'Column' && field.isPrimaryKey; } static isForeignKey(schema: CollectionSchema, name: string): boolean { - const field = schema.fields[name]; + const field = this.getField(schema, name); return ( field.type === 'Column' && @@ -26,9 +116,7 @@ export default class SchemaUtils { schema: CollectionSchema, relationName: string, ): ManyToManySchema | OneToManySchema { - const relationFieldSchema = schema.fields[relationName]; - - if (!relationFieldSchema) throw new Error(`Relation '${relationName}' not found`); + const relationFieldSchema = this.getRelation(schema, relationName); if (relationFieldSchema.type !== 'OneToMany' && relationFieldSchema.type !== 'ManyToMany') { throw new Error( @@ -39,4 +127,19 @@ export default class SchemaUtils { return relationFieldSchema as ManyToManySchema | OneToManySchema; } + + static getColumnNames(schema: CollectionSchema): string[] { + return Object.keys(schema.fields).filter( + name => SchemaUtils.getField(schema, name).type === 'Column', + ); + } + + private static throwIfAccessingFieldFromRelation( + fieldName: string, + collectionName?: string, + ): void { + if (fieldName.includes(':')) { + throw new RelationFieldAccessDeniedError({ fieldName, collectionName }); + } + } } diff --git a/packages/datasource-toolkit/src/validation/field.ts b/packages/datasource-toolkit/src/validation/field.ts index d80b1f88fd..ccf31d34fa 100644 --- a/packages/datasource-toolkit/src/validation/field.ts +++ b/packages/datasource-toolkit/src/validation/field.ts @@ -1,6 +1,7 @@ import { MAP_ALLOWED_TYPES_FOR_COLUMN_TYPE } from './rules'; import TypeGetter from './type-getter'; -import { MissingFieldError, ValidationError } from '../errors'; +import { MissingRelationError, ValidationError } from '../errors'; +import { SchemaUtils } from '../index'; import { Collection } from '../interfaces/collection'; import { ColumnSchema, PrimitiveTypes } from '../interfaces/schema'; @@ -9,11 +10,7 @@ export default class FieldValidator { const dotIndex = field.indexOf(':'); if (dotIndex === -1) { - const schema = collection.schema.fields[field]; - - if (!schema) { - throw new MissingFieldError(field, collection.name); - } + const schema = SchemaUtils.getField(collection.schema, field, collection.name); if (schema.type !== 'Column') { throw new ValidationError( @@ -30,7 +27,15 @@ export default class FieldValidator { const schema = collection.schema.fields[prefix]; if (!schema) { - throw new ValidationError(`Relation not found: '${collection.name}.${prefix}'`); + throw new MissingRelationError({ + collectionName: collection.name, + fieldName: prefix, + availableFields: Object.keys(collection.schema.fields).filter( + name => + collection.schema.fields[name].type === 'ManyToOne' || + collection.schema.fields[name].type === 'OneToOne', + ), + }); } if (schema.type !== 'ManyToOne' && schema.type !== 'OneToOne') { diff --git a/packages/datasource-toolkit/src/validation/record.ts b/packages/datasource-toolkit/src/validation/record.ts index 4ae76eacb6..a331c1b55b 100644 --- a/packages/datasource-toolkit/src/validation/record.ts +++ b/packages/datasource-toolkit/src/validation/record.ts @@ -1,5 +1,6 @@ import FieldValidator from './field'; import { ValidationError } from '../errors'; +import { SchemaUtils } from '../index'; import { Collection } from '../interfaces/collection'; import { RecordData } from '../interfaces/record'; @@ -10,11 +11,9 @@ export default class RecordValidator { } for (const key of Object.keys(recordData)) { - const schema = collection.schema.fields[key]; + const schema = SchemaUtils.getField(collection.schema, key, collection.name); - if (!schema) { - throw new ValidationError(`Unknown field "${key}"`); - } else if (schema.type === 'Column') { + if (schema.type === 'Column') { FieldValidator.validate(collection, key, [recordData[key]]); } else if (schema.type === 'OneToOne' || schema.type === 'ManyToOne') { const subRecord = recordData[key] as RecordData; diff --git a/packages/datasource-toolkit/test/base-collection.test.ts b/packages/datasource-toolkit/test/base-collection.test.ts index 572a80e62d..ccc5e2d2ed 100644 --- a/packages/datasource-toolkit/test/base-collection.test.ts +++ b/packages/datasource-toolkit/test/base-collection.test.ts @@ -123,7 +123,7 @@ describe('BaseCollection', () => { it('should prevent instanciation when adding field with duplicated name', () => { expect(() => new DuplicatedFieldErrorCollection()).toThrow( - 'Field "__duplicated__" already defined in collection', + "The 'books.__duplicated__' field is already defined. Please check if the field name is correct and unique.", ); }); diff --git a/packages/datasource-toolkit/test/utils/collection.test.ts b/packages/datasource-toolkit/test/utils/collection.test.ts index acde66de44..6a1d165782 100644 --- a/packages/datasource-toolkit/test/utils/collection.test.ts +++ b/packages/datasource-toolkit/test/utils/collection.test.ts @@ -1,3 +1,4 @@ +import { MissingFieldError, MissingRelationError } from '../../src'; import Aggregation from '../../src/interfaces/query/aggregation'; import ConditionTreeFactory from '../../src/interfaces/query/condition-tree/factory'; import ConditionTreeLeaf from '../../src/interfaces/query/condition-tree/nodes/leaf'; @@ -155,7 +156,7 @@ describe('CollectionUtils', () => { expect(() => CollectionUtils.getFieldSchema(dataSource.getCollection('books'), 'unknown:id'), - ).toThrow("Relation not found 'books.unknown'"); + ).toThrow(MissingRelationError); }); test('should throw if the field is missing', () => { @@ -163,7 +164,7 @@ describe('CollectionUtils', () => { expect(() => CollectionUtils.getFieldSchema(dataSource.getCollection('books'), 'author:something'), - ).toThrow(`Column not found 'persons.something'`); + ).toThrow(MissingFieldError); }); }); }); diff --git a/packages/datasource-toolkit/test/utils/schema.test.ts b/packages/datasource-toolkit/test/utils/schema.test.ts index 2086054164..17a9df0c64 100644 --- a/packages/datasource-toolkit/test/utils/schema.test.ts +++ b/packages/datasource-toolkit/test/utils/schema.test.ts @@ -1,3 +1,10 @@ +import { + AlreadyDefinedFieldError, + MissingColumnError, + MissingFieldError, + MissingRelationError, + RelationFieldAccessDeniedError, +} from '../../src'; import SchemaUtils from '../../src/utils/schema'; import * as factories from '../__factories__'; @@ -126,8 +133,194 @@ describe('SchemaUtils', () => { const schema = factories.collectionSchema.build({}); expect(() => SchemaUtils.getToManyRelation(schema, 'anUnknownRelation')).toThrow( - `Relation 'anUnknownRelation' not found`, + MissingRelationError, ); }); }); + + describe('getColumnNames', () => { + it('should return all the field names without the relations', () => { + const schema = factories.collectionSchema.build({ + fields: { + relation: factories.manyToManySchema.build(), + field: factories.columnSchema.build({}), + }, + }); + + expect(SchemaUtils.getColumnNames(schema)).toEqual(['field']); + }); + }); + + describe('getRelation', () => { + describe('when accessing field from relation', () => { + it('should deny the access', () => { + expect(() => + SchemaUtils.getRelation(factories.collectionSchema.build(), 'relation:field'), + ).toThrow(RelationFieldAccessDeniedError); + }); + }); + + describe('when relation does not exist', () => { + it('should throw an error', () => { + expect(() => + SchemaUtils.getRelation(factories.collectionSchema.build(), 'relation'), + ).toThrow(MissingRelationError); + }); + }); + + describe('when relation exists', () => { + it('should return the relation', () => { + const schema = factories.collectionSchema.build({ + fields: { relation: factories.manyToManySchema.build() }, + }); + + expect(SchemaUtils.getRelation(schema, 'relation')).toEqual(schema.fields.relation); + }); + }); + + describe('when asking column', () => { + it('should throw an error', () => { + expect(() => SchemaUtils.getRelation(factories.collectionSchema.build(), 'field')).toThrow( + MissingRelationError, + ); + }); + }); + }); + + describe('getField', () => { + describe('when accessing field from relation', () => { + it('should deny the access', () => { + expect(() => + SchemaUtils.getField(factories.collectionSchema.build(), 'relation:field'), + ).toThrow(RelationFieldAccessDeniedError); + }); + }); + + describe('when field does not exist', () => { + it('should throw an error', () => { + expect(() => SchemaUtils.getField(factories.collectionSchema.build(), 'field')).toThrow( + MissingFieldError, + ); + }); + }); + + describe('when field exists', () => { + it('should return fields from column', () => { + const schema = factories.collectionSchema.build({ + fields: { + field: factories.columnSchema.build(), + }, + }); + + expect(SchemaUtils.getField(schema, 'field')).toEqual(schema.fields.field); + }); + + it('should return fields from relation', () => { + const schema = factories.collectionSchema.build({ + fields: { + relation: factories.manyToManySchema.build(), + }, + }); + + expect(SchemaUtils.getField(schema, 'relation')).toEqual(schema.fields.relation); + }); + }); + }); + + describe('getColumn', () => { + describe('when accessing field from relation', () => { + it('should deny the access', () => { + expect(() => + SchemaUtils.getColumn(factories.collectionSchema.build(), 'relation:field'), + ).toThrow(RelationFieldAccessDeniedError); + }); + }); + + describe('when column does not exist', () => { + it('should throw an error', () => { + expect(() => SchemaUtils.getColumn(factories.collectionSchema.build(), 'field')).toThrow( + MissingColumnError, + ); + }); + }); + + describe('when column exists', () => { + it('should return the column', () => { + const schema = factories.collectionSchema.build({ + fields: { + field: factories.columnSchema.build(), + relation: factories.manyToManySchema.build(), + }, + }); + + expect(SchemaUtils.getColumn(schema, 'field')).toEqual(schema.fields.field); + }); + }); + + describe('when asking relation', () => { + it('should throw an error', () => { + expect(() => SchemaUtils.getColumn(factories.collectionSchema.build(), 'relation')).toThrow( + MissingColumnError, + ); + }); + }); + }); + + describe('throwIfAlreadyDefinedField', () => { + describe('when the field is already defined', () => { + it('should deny the access', () => { + const schema = factories.collectionSchema.build({ + fields: { field: factories.columnSchema.build() }, + }); + + expect(() => SchemaUtils.throwIfAlreadyDefinedField(schema, 'field')).toThrow( + AlreadyDefinedFieldError, + ); + }); + }); + + describe('when the field is not defined', () => { + it('should not throw an error', () => { + const schema = factories.collectionSchema.build({ + fields: {}, + }); + + expect(() => SchemaUtils.throwIfAlreadyDefinedField(schema, 'field')).not.toThrow(); + }); + }); + }); + + describe('throwIfMissingField', () => { + describe('when the field is inside a relation', () => { + it('should deny the access', () => { + const schema = factories.collectionSchema.build({ + fields: { relation: factories.manyToManySchema.build() }, + }); + + expect(() => SchemaUtils.throwIfMissingField(schema, 'relation:field')).toThrow( + RelationFieldAccessDeniedError, + ); + }); + }); + + describe('when the field is missing', () => { + it('should throw an error', () => { + const schema = factories.collectionSchema.build({ + fields: {}, + }); + + expect(() => SchemaUtils.throwIfMissingField(schema, 'field')).toThrow(MissingFieldError); + }); + }); + + describe('when the field is defined', () => { + it('should not throw an error', () => { + const schema = factories.collectionSchema.build({ + fields: { field: factories.columnSchema.build() }, + }); + + expect(() => SchemaUtils.throwIfMissingField(schema, 'field')).not.toThrow(); + }); + }); + }); }); diff --git a/packages/datasource-toolkit/test/validation/condition-tree/index.test.ts b/packages/datasource-toolkit/test/validation/condition-tree/index.test.ts index bb2f3d9435..cbc2a5bc28 100644 --- a/packages/datasource-toolkit/test/validation/condition-tree/index.test.ts +++ b/packages/datasource-toolkit/test/validation/condition-tree/index.test.ts @@ -1,3 +1,4 @@ +import { MissingFieldError } from '../../../src'; import ConditionTree from '../../../src/interfaces/query/condition-tree/nodes/base'; import { Aggregator } from '../../../src/interfaces/query/condition-tree/nodes/branch'; import ConditionTreeValidator from '../../../src/validation/condition-tree'; @@ -58,7 +59,7 @@ describe('ConditionTreeValidation', () => { }); expect(() => ConditionTreeValidator.validate(conditionTree, collection)).toThrow( - "Column not found 'a collection.fieldDoesNotExistInSchema'", + MissingFieldError, ); }); @@ -133,7 +134,7 @@ describe('ConditionTreeValidation', () => { }); expect(() => ConditionTreeValidator.validate(conditionTree, collection)).toThrow( - "Column not found 'a collection.fieldDoesNotExistInSchema'", + MissingFieldError, ); }); }); diff --git a/packages/datasource-toolkit/test/validation/field/field.test.ts b/packages/datasource-toolkit/test/validation/field/field.test.ts index ebecb4c1e8..e7c16f467d 100644 --- a/packages/datasource-toolkit/test/validation/field/field.test.ts +++ b/packages/datasource-toolkit/test/validation/field/field.test.ts @@ -1,4 +1,4 @@ -import { MissingFieldError } from '../../../src/errors'; +import { MissingFieldError } from '../../../src'; import FieldValidator from '../../../src/validation/field'; import * as factories from '../../__factories__'; @@ -24,8 +24,6 @@ describe('FieldValidator', () => { }, }); - expect(() => FieldValidator.validate(collection, 'INVALID')).toThrow( - new MissingFieldError('INVALID', 'collection1'), - ); + expect(() => FieldValidator.validate(collection, 'INVALID')).toThrow(MissingFieldError); }); }); diff --git a/packages/datasource-toolkit/test/validation/field/validate.test.ts b/packages/datasource-toolkit/test/validation/field/validate.test.ts index a4b9175a57..79b0ca868a 100644 --- a/packages/datasource-toolkit/test/validation/field/validate.test.ts +++ b/packages/datasource-toolkit/test/validation/field/validate.test.ts @@ -1,4 +1,4 @@ -import { MissingFieldError } from '../../../src'; +import { MissingFieldError, MissingRelationError } from '../../../src'; import FieldValidator from '../../../src/validation/field'; import * as factories from '../../__factories__'; @@ -13,6 +13,7 @@ describe('FieldValidator', () => { foreignCollection: 'owner', originKey: 'id', }), + drivers: factories.manyToManySchema.build(), }, }), }); @@ -33,13 +34,13 @@ describe('FieldValidator', () => { test('should throw if the field does not exists', () => { expect(() => FieldValidator.validate(carsCollection, '__not_defined')).toThrow( - new MissingFieldError('__not_defined', 'cars'), + MissingFieldError, ); }); test('should throw if the relation does not exists', () => { expect(() => FieldValidator.validate(carsCollection, '__not_defined:id')).toThrow( - "Relation not found: 'cars.__not_defined'", + MissingRelationError, ); }); @@ -75,6 +76,12 @@ describe('FieldValidator', () => { "Unexpected field type: 'cars.id' (found 'Column' expected 'ManyToOne' or 'OneToOne')", ); }); + + test('should throw when the requested field is a Many to Many', () => { + expect(() => FieldValidator.validate(carsCollection, 'drivers:address')).toThrow( + "Unexpected field type: 'cars.drivers' (found 'ManyToMany' expected 'ManyToOne' or 'OneToOne')", + ); + }); }); describe('when validating a json field with an array value', () => { diff --git a/packages/datasource-toolkit/test/validation/record.test.ts b/packages/datasource-toolkit/test/validation/record.test.ts index 7548331c60..df3d2c535c 100644 --- a/packages/datasource-toolkit/test/validation/record.test.ts +++ b/packages/datasource-toolkit/test/validation/record.test.ts @@ -1,3 +1,4 @@ +import { MissingFieldError } from '../../src'; import RecordValidator from '../../src/validation/record'; import * as factories from '../__factories__'; @@ -16,7 +17,7 @@ describe('RecordValidator', () => { RecordValidator.validate(collection, { unknownField: 'this field is not defined in the collection', }), - ).toThrow('Unknown field "unknownField"'); + ).toThrow(MissingFieldError); }); }); @@ -110,7 +111,7 @@ describe('RecordValidator', () => { RecordValidator.validate(dataSourceBook.getCollection('book'), { relation: { fieldNotExist: 'a name' }, }), - ).toThrow('Unknown field "fieldNotExist'); + ).toThrow(MissingFieldError); }); test('should throw an error when the relation is an empty object', () => { diff --git a/packages/plugin-aws-s3/package.json b/packages/plugin-aws-s3/package.json index 461578ea3e..5dc2493036 100644 --- a/packages/plugin-aws-s3/package.json +++ b/packages/plugin-aws-s3/package.json @@ -13,7 +13,8 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.157.0", - "@aws-sdk/s3-request-presigner": "^3.157.0" + "@aws-sdk/s3-request-presigner": "^3.157.0", + "@forestadmin/datasource-toolkit": "1.40.0" }, "devDependencies": { "@forestadmin/datasource-customizer": "1.52.1", diff --git a/packages/plugin-aws-s3/src/field/make-field-filterable.ts b/packages/plugin-aws-s3/src/field/make-field-filterable.ts index 7c16f890dd..acb1b14467 100644 --- a/packages/plugin-aws-s3/src/field/make-field-filterable.ts +++ b/packages/plugin-aws-s3/src/field/make-field-filterable.ts @@ -1,12 +1,13 @@ import type { Configuration } from '../types'; import type { CollectionCustomizer } from '@forestadmin/datasource-customizer'; -import type { ColumnSchema } from '@forestadmin/datasource-toolkit'; + +import { SchemaUtils } from '@forestadmin/datasource-toolkit'; export default function makeFieldFilterable( collection: CollectionCustomizer, config: Configuration, ): void { - const schema = collection.schema.fields[config.sourcename] as ColumnSchema; + const schema = SchemaUtils.getColumn(collection.schema, config.sourcename, collection.name); for (const operator of schema.filterOperators) { collection.replaceFieldOperator(config.filename, operator, value => ({ diff --git a/packages/plugin-aws-s3/src/field/make-field-required.ts b/packages/plugin-aws-s3/src/field/make-field-required.ts index 28c87a056a..9bcbdb98ae 100644 --- a/packages/plugin-aws-s3/src/field/make-field-required.ts +++ b/packages/plugin-aws-s3/src/field/make-field-required.ts @@ -1,12 +1,13 @@ import type { Configuration } from '../types'; import type { CollectionCustomizer } from '@forestadmin/datasource-customizer'; -import type { ColumnSchema } from '@forestadmin/datasource-toolkit'; + +import { SchemaUtils } from '@forestadmin/datasource-toolkit'; export default function makeFieldRequired( collection: CollectionCustomizer, config: Configuration, ): void { - const schema = collection.schema.fields[config.sourcename] as ColumnSchema; + const schema = SchemaUtils.getColumn(collection.schema, config.sourcename, collection.name); if (schema.validation?.find(rule => rule.operator === 'Present')) { collection.addFieldValidation(config.filename, 'Present'); diff --git a/packages/plugin-aws-s3/src/field/make-field-writable.ts b/packages/plugin-aws-s3/src/field/make-field-writable.ts index 934a2176c9..c583c1188f 100644 --- a/packages/plugin-aws-s3/src/field/make-field-writable.ts +++ b/packages/plugin-aws-s3/src/field/make-field-writable.ts @@ -1,5 +1,6 @@ import type { CollectionCustomizer } from '@forestadmin/datasource-customizer'; -import type { ColumnSchema, RecordData } from '@forestadmin/datasource-toolkit'; + +import { RecordData, SchemaUtils } from '@forestadmin/datasource-toolkit'; import { Configuration } from '../types'; import { parseDataUri } from '../utils/data-uri'; @@ -24,7 +25,7 @@ export default function makeFieldWritable( collection: CollectionCustomizer, config: Configuration, ): void { - const schema = collection.schema.fields[config.sourcename] as ColumnSchema; + const schema = SchemaUtils.getColumn(collection.schema, config.sourcename, collection.name); if (schema.isReadOnly) return; collection.replaceFieldWriting(config.filename, async (value, context) => { diff --git a/packages/plugin-aws-s3/src/index.ts b/packages/plugin-aws-s3/src/index.ts index 8c7d902faf..d981080ef1 100644 --- a/packages/plugin-aws-s3/src/index.ts +++ b/packages/plugin-aws-s3/src/index.ts @@ -1,5 +1,7 @@ import type { TCollectionName, TSchema } from '@forestadmin/datasource-customizer'; +import { SchemaUtils } from '@forestadmin/datasource-toolkit'; + import createField from './field/create-field'; import makeFieldDeleteable from './field/make-field-deleteable'; import makeFieldFilterable from './field/make-field-filterable'; @@ -19,7 +21,7 @@ export async function createFileField< if (!collection) throw new Error('createFileField can only be used on collections.'); if (!options) throw new Error('Options must be provided.'); - const sourceSchema = collection.schema.fields[options.fieldname]; + const sourceSchema = SchemaUtils.getField(collection.schema, options.fieldname, collection.name); if (!sourceSchema || sourceSchema.type !== 'Column' || sourceSchema.columnType !== 'String') { const field = `${collection.name}.${options.fieldname}`; diff --git a/packages/plugin-flattener/src/flatten-column/index.ts b/packages/plugin-flattener/src/flatten-column/index.ts index a01476dd8d..597c1ac1ce 100644 --- a/packages/plugin-flattener/src/flatten-column/index.ts +++ b/packages/plugin-flattener/src/flatten-column/index.ts @@ -3,7 +3,7 @@ import type { DataSourceCustomizer, } from '@forestadmin/datasource-customizer'; -import { ColumnSchema, ColumnType } from '@forestadmin/datasource-toolkit'; +import { ColumnType, SchemaUtils } from '@forestadmin/datasource-toolkit'; import { makeCreateHook, makeField, makeUpdateHook, makeWriteHandler } from './customization'; import { includeStrToPath, listPaths } from './helpers'; @@ -20,7 +20,7 @@ export type FlattenColumnOptions = { function optionsToPaths(collection: CollectionCustomizer, options: FlattenColumnOptions): string[] { const { columnName, include, exclude, level, columnType } = options; const errorMessage = `'${collection.name}.${columnName}' cannot be flattened`; - const schema = collection.schema.fields[options.columnName]; + const schema = SchemaUtils.getColumn(collection.schema, columnName, collection.name); if (!schema) throw new Error(`${errorMessage} (not found).`); if (schema.type !== 'Column') throw new Error(`${errorMessage} (not a column).`); @@ -70,7 +70,7 @@ export default async function flattenColumn( if (!collection) throw new Error('This plugin can only be called when customizing collections.'); const paths = optionsToPaths(collection, options); - const schema = collection.schema.fields[options.columnName] as ColumnSchema; + const schema = SchemaUtils.getColumn(collection.schema, options.columnName, collection.name); // Add fields that reads the value from the deeply nested column for (const path of paths) { diff --git a/packages/plugin-flattener/src/flatten-json-column/index.ts b/packages/plugin-flattener/src/flatten-json-column/index.ts index 892fb1d408..37ec25ed03 100644 --- a/packages/plugin-flattener/src/flatten-json-column/index.ts +++ b/packages/plugin-flattener/src/flatten-json-column/index.ts @@ -3,7 +3,7 @@ import type { DataSourceCustomizer, } from '@forestadmin/datasource-customizer'; -import { ColumnSchema, ColumnType } from '@forestadmin/datasource-toolkit'; +import { ColumnType, SchemaUtils } from '@forestadmin/datasource-toolkit'; import flattenColumn from '../flatten-column'; @@ -71,7 +71,7 @@ export default function flattenJsonColumn( if (!collection) throw new Error('This plugin can only be called when customizing collections.'); const errorMessage = `'${collection.name}.${options.columnName} cannot be flattened`; - const schema = collection.schema.fields[options.columnName] as ColumnSchema; + const schema = SchemaUtils.getColumn(collection.schema, options.columnName, collection.name); if (schema?.columnType !== 'Json') { throw new Error( diff --git a/packages/plugin-flattener/src/flatten-relation/helpers.ts b/packages/plugin-flattener/src/flatten-relation/helpers.ts index fb24dfe1b8..7d265109b8 100644 --- a/packages/plugin-flattener/src/flatten-relation/helpers.ts +++ b/packages/plugin-flattener/src/flatten-relation/helpers.ts @@ -2,12 +2,10 @@ import type { CollectionCustomizer, DataSourceCustomizer, } from '@forestadmin/datasource-customizer'; -import type { CollectionSchema, RelationSchema } from '@forestadmin/datasource-toolkit'; -export function getColumns(schema: CollectionSchema): string[] { - return Object.keys(schema.fields).filter(name => schema.fields[name].type === 'Column'); -} +import { RelationSchema } from '@forestadmin/datasource-toolkit'; +// eslint-disable-next-line import/prefer-default-export export function getRelation( relationName: string, dataSource: DataSourceCustomizer, diff --git a/packages/plugin-flattener/src/flatten-relation/index.ts b/packages/plugin-flattener/src/flatten-relation/index.ts index f8a1e90b17..e8de65a6cd 100644 --- a/packages/plugin-flattener/src/flatten-relation/index.ts +++ b/packages/plugin-flattener/src/flatten-relation/index.ts @@ -3,7 +3,9 @@ import type { DataSourceCustomizer, } from '@forestadmin/datasource-customizer'; -import { getColumns, getRelation } from './helpers'; +import { SchemaUtils } from '@forestadmin/datasource-toolkit'; + +import { getRelation } from './helpers'; /** * Import all fields of a relation in the current collection. @@ -33,7 +35,9 @@ export default async function flattenRelation( const relation = getRelation(relationName, dataSource, collection); const foreignCollection = dataSource.getCollection(relation.foreignCollection); - const columns = new Set(include?.length > 0 ? include : getColumns(foreignCollection.schema)); + const columns = new Set( + include?.length > 0 ? include : SchemaUtils.getColumnNames(foreignCollection.schema), + ); exclude?.forEach(column => columns.delete(column)); for (const column of columns) { diff --git a/packages/plugin-flattener/test/flatten-column.test.ts b/packages/plugin-flattener/test/flatten-column.test.ts index 869d55cff6..0a36c8bdb8 100644 --- a/packages/plugin-flattener/test/flatten-column.test.ts +++ b/packages/plugin-flattener/test/flatten-column.test.ts @@ -1,5 +1,5 @@ import { DataSourceCustomizer } from '@forestadmin/datasource-customizer'; -import { DataSource, Projection } from '@forestadmin/datasource-toolkit'; +import { DataSource, MissingColumnError, Projection } from '@forestadmin/datasource-toolkit'; import * as factories from '@forestadmin/datasource-toolkit/dist/test/__factories__'; import flattenColumn from '../src/flatten-column'; @@ -76,7 +76,7 @@ describe('flattenColumn', () => { customizer .customizeCollection('book', book => book.use(flattenColumn, options)) .getDataSource(logger), - ).rejects.toThrow("'book.doctor who?' cannot be flattened (not found)."); + ).rejects.toThrow(MissingColumnError); }); it('should throw when target is a primitive', async () => { @@ -106,7 +106,7 @@ describe('flattenColumn', () => { customizer .customizeCollection('book', book => book.use(flattenColumn, options)) .getDataSource(logger), - ).rejects.toThrow("'book.myself' cannot be flattened (not a column)."); + ).rejects.toThrow(MissingColumnError); }); it('should throw level is invalid', async () => { diff --git a/packages/plugin-flattener/test/flatten-relation.test.ts b/packages/plugin-flattener/test/flatten-relation.test.ts index 774ec81415..2de851425e 100644 --- a/packages/plugin-flattener/test/flatten-relation.test.ts +++ b/packages/plugin-flattener/test/flatten-relation.test.ts @@ -1,7 +1,6 @@ import { DataSourceCustomizer } from '@forestadmin/datasource-customizer'; -import { ColumnSchema } from '@forestadmin/datasource-toolkit'; +import { ColumnSchema, MissingFieldError } from '@forestadmin/datasource-toolkit'; import * as factories from '@forestadmin/datasource-toolkit/dist/test/__factories__'; -import { MissingFieldError } from '@forestadmin/datasource-toolkit/src'; import flattenRelation from '../src/flatten-relation'; @@ -250,7 +249,7 @@ describe('flattenRelation', () => { book.use(flattenRelation, { relationName: 'owner', include: ['doesNotExist'] }), ) .getDataSource(logger), - ).rejects.toThrow(new MissingFieldError('doesNotExist', 'owner')); + ).rejects.toThrow(MissingFieldError); }); }); });