diff --git a/packages/_example/src/connections/sequelize-mssql.ts b/packages/_example/src/connections/sequelize-mssql.ts index 78e23d4d67..a4d582e24e 100644 --- a/packages/_example/src/connections/sequelize-mssql.ts +++ b/packages/_example/src/connections/sequelize-mssql.ts @@ -19,6 +19,9 @@ const dvd = sequelizeMsSql.define( rentalPrice: { type: DataTypes.FLOAT, allowNull: false, + validate: { + max: '2' as unknown as number, + }, }, storeId: { type: DataTypes.INTEGER, diff --git a/packages/agent/src/routes/access/chart.ts b/packages/agent/src/routes/access/chart.ts index 53109e96a3..632a18be7a 100644 --- a/packages/agent/src/routes/access/chart.ts +++ b/packages/agent/src/routes/access/chart.ts @@ -9,6 +9,7 @@ import { Filter, FilterFactory, RelationSchema, + SchemaUtils, ValidationError, } from '@forestadmin/datasource-toolkit'; import { @@ -187,8 +188,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..a833c4dbbd 100644 --- a/packages/agent/src/routes/modification/create.ts +++ b/packages/agent/src/routes/modification/create.ts @@ -43,7 +43,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 +87,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 +121,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 +136,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..dcd140a051 100644 --- a/packages/agent/src/routes/relation-route.ts +++ b/packages/agent/src/routes/relation-route.ts @@ -1,4 +1,9 @@ -import { Collection, DataSource, RelationSchema } from '@forestadmin/datasource-toolkit'; +import { + Collection, + DataSource, + RelationSchema, + SchemaUtils, +} from '@forestadmin/datasource-toolkit'; import CollectionRoute from './collection-route'; import { ForestAdminHttpDriverServices } from '../services'; @@ -8,7 +13,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..692eb39a98 100644 --- a/packages/agent/src/utils/forest-schema/generator-actions.ts +++ b/packages/agent/src/utils/forest-schema/generator-actions.ts @@ -118,7 +118,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..902a37499c 100644 --- a/packages/agent/src/utils/forest-schema/generator-fields.ts +++ b/packages/agent/src/utils/forest-schema/generator-fields.ts @@ -28,7 +28,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 +58,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 +112,37 @@ export default class SchemaGeneratorFields { if (relation.type === 'OneToMany') { targetName = relation.originKeyTarget; - targetField = collection.schema.fields[targetName] as ColumnSchema; + targetField = SchemaUtils.getColumn( + foreignCollection.schema, + targetName, + foreignCollection.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 +174,12 @@ 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(collection.schema, relation.originKey, collection.name); return { ...baseSchema, @@ -177,7 +201,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 +221,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..705a0119bf 100644 --- a/packages/agent/src/utils/id.ts +++ b/packages/agent/src/utils/id.ts @@ -49,7 +49,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..904020c380 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}]`]}`; }); diff --git a/packages/datasource-customizer/src/collection-customizer.ts b/packages/datasource-customizer/src/collection-customizer.ts index 9f1aac7b90..1b658f337a 100644 --- a/packages/datasource-customizer/src/collection-customizer.ts +++ b/packages/datasource-customizer/src/collection-customizer.ts @@ -4,6 +4,7 @@ import { ColumnSchema, Logger, Operator, + SchemaUtils, allowedOperatorsForColumnType, } from '@forestadmin/datasource-toolkit'; @@ -458,7 +459,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..511364fb86 100644 --- a/packages/datasource-customizer/src/decorators/binary/collection.ts +++ b/packages/datasource-customizer/src/decorators/binary/collection.ts @@ -41,7 +41,7 @@ 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, this.childCollection.name); if (type !== 'datauri' && type !== 'hex') { throw new Error('Invalid binary mode'); @@ -150,7 +150,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 +181,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..e62494af66 100644 --- a/packages/datasource-customizer/src/decorators/computed/collection.ts +++ b/packages/datasource-customizer/src/decorators/computed/collection.ts @@ -11,6 +11,7 @@ import { Projection, RecordData, RelationSchema, + SchemaUtils, } from '@forestadmin/datasource-toolkit'; import computeFromRecords from './helpers/compute-fields'; @@ -28,7 +29,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..8d6c3d8c9a 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, RelationSchema, 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..511d382459 100644 --- a/packages/datasource-customizer/src/decorators/operators-emulate/collection.ts +++ b/packages/datasource-customizer/src/decorators/operators-emulate/collection.ts @@ -40,7 +40,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.getColumn( + this.childCollection.schema, + pk, + this.childCollection.name, + ); const operators = schema.filterOperators; if (!operators?.has('Equal') || !operators?.has('In')) { @@ -52,7 +56,11 @@ export default class OperatorsEmulateCollectionDecorator extends CollectionDecor }); // Check that targeted field is valid - const field = this.childCollection.schema.fields[name] as ColumnSchema; + const field = SchemaUtils.getColumn( + this.childCollection.schema, + name, + this.childCollection.name, + ); FieldValidator.validate(this, name); if (!field) throw new Error('Cannot replace operator for relation'); @@ -102,7 +110,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..c40ac424d6 100644 --- a/packages/datasource-customizer/src/decorators/override/collection.ts +++ b/packages/datasource-customizer/src/decorators/override/collection.ts @@ -78,7 +78,7 @@ export default class OverrideCollectionDecorator extends CollectionDecorator { records.forEach(result => { let hasPrimaryKey = false; Object.keys(result).forEach(key => { - const field = this.schema.fields[key]; + const field = SchemaUtils.getField(this.schema, key, this.name); if (!field || field.type !== 'Column') { delete result[key]; diff --git a/packages/datasource-customizer/src/decorators/publication/collection.ts b/packages/datasource-customizer/src/decorators/publication/collection.ts index f1823c0302..98d1e0fcb4 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.checkMissingField(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..85b250b05e 100644 --- a/packages/datasource-customizer/src/decorators/rename-field/collection.ts +++ b/packages/datasource-customizer/src/decorators/rename-field/collection.ts @@ -8,11 +8,11 @@ import { FieldSchema, FieldValidator, Filter, - MissingFieldError, PaginatedFilter, Projection, RecordData, RelationSchema, + SchemaUtils, } from '@forestadmin/datasource-toolkit'; /** @@ -30,9 +30,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.checkMissingField(this.schema, currentName, this.name); let initialName = currentName; @@ -152,7 +150,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 +164,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 +192,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..b3f328af04 100644 --- a/packages/datasource-customizer/src/decorators/sort-emulate/collection.ts +++ b/packages/datasource-customizer/src/decorators/sort-emulate/collection.ts @@ -13,6 +13,7 @@ import { RecordData, RecordUtils, RelationSchema, + SchemaUtils, Sort, } from '@forestadmin/datasource-toolkit'; @@ -133,7 +134,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 +159,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..61c1c195e2 100644 --- a/packages/datasource-customizer/src/decorators/write/write-replace/collection.ts +++ b/packages/datasource-customizer/src/decorators/write/write-replace/collection.ts @@ -8,6 +8,7 @@ import { Filter, RecordData, RecordValidator, + SchemaUtils, } from '@forestadmin/datasource-toolkit'; import WriteCustomizationContext from './context'; @@ -32,7 +33,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 +88,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') { 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..ddb6d03fc6 100644 --- a/packages/datasource-customizer/test/collection-customizer.test.ts +++ b/packages/datasource-customizer/test/collection-customizer.test.ts @@ -1,11 +1,6 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { - ColumnSchema, - ConditionTreeLeaf, - MissingFieldError, - Sort, -} from '@forestadmin/datasource-toolkit'; +import { ColumnSchema, ConditionTreeLeaf, Sort } from '@forestadmin/datasource-toolkit'; import * as factories from '@forestadmin/datasource-toolkit/dist/test/__factories__'; import { diff --git a/packages/datasource-customizer/test/decorators/computed/collection.test.ts b/packages/datasource-customizer/test/decorators/computed/collection.test.ts index 8da78471d9..c4502e793b 100644 --- a/packages/datasource-customizer/test/decorators/computed/collection.test.ts +++ b/packages/datasource-customizer/test/decorators/computed/collection.test.ts @@ -3,7 +3,6 @@ import { Collection, DataSource, DataSourceDecorator, - MissingFieldError, PaginatedFilter, Projection, } from '@forestadmin/datasource-toolkit'; 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..1f8093b111 100644 --- a/packages/datasource-customizer/test/decorators/operators-emulate/collection.test.ts +++ b/packages/datasource-customizer/test/decorators/operators-emulate/collection.test.ts @@ -4,7 +4,6 @@ import { ConditionTreeLeaf, DataSource, DataSourceDecorator, - MissingFieldError, PaginatedFilter, Projection, RecordData, diff --git a/packages/datasource-customizer/test/decorators/relation/collection.test.ts b/packages/datasource-customizer/test/decorators/relation/collection.test.ts index b1527eb64a..3a1d4c9391 100644 --- a/packages/datasource-customizer/test/decorators/relation/collection.test.ts +++ b/packages/datasource-customizer/test/decorators/relation/collection.test.ts @@ -7,7 +7,6 @@ import { DataSourceDecorator, Filter, ManyToManySchema, - MissingFieldError, PaginatedFilter, Projection, Sort, 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..f2b0f4abb0 100644 --- a/packages/datasource-customizer/test/decorators/rename-field/collection.test.ts +++ b/packages/datasource-customizer/test/decorators/rename-field/collection.test.ts @@ -6,7 +6,6 @@ import { DataSource, DataSourceDecorator, Filter, - MissingFieldError, PaginatedFilter, Projection, Sort, 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..2fc03668bc 100644 --- a/packages/datasource-customizer/test/decorators/sort-emulate/collection.test.ts +++ b/packages/datasource-customizer/test/decorators/sort-emulate/collection.test.ts @@ -3,7 +3,6 @@ import { ColumnSchema, DataSource, DataSourceDecorator, - MissingFieldError, Page, PaginatedFilter, Projection, diff --git a/packages/datasource-customizer/test/decorators/validation/collection.test.ts b/packages/datasource-customizer/test/decorators/validation/collection.test.ts index 343a86710b..5fb62a5be9 100644 --- a/packages/datasource-customizer/test/decorators/validation/collection.test.ts +++ b/packages/datasource-customizer/test/decorators/validation/collection.test.ts @@ -1,9 +1,4 @@ -import { - Collection, - DataSource, - DataSourceDecorator, - MissingFieldError, -} from '@forestadmin/datasource-toolkit'; +import { Collection, DataSource, DataSourceDecorator } from '@forestadmin/datasource-toolkit'; import * as factories from '@forestadmin/datasource-toolkit/dist/test/__factories__'; import ValidationDecorator from '../../../src/decorators/validation/collection'; 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..3adc941350 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.checkAlreadyDefinedField(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..d512ed290d 100644 --- a/packages/datasource-toolkit/src/errors.ts +++ b/packages/datasource-toolkit/src/errors.ts @@ -76,9 +76,3 @@ export class IntrospectionFormatError extends BusinessError { export class MissingSchemaElementError extends ValidationError {} export class MissingCollectionError extends MissingSchemaElementError {} - -export class MissingFieldError extends MissingSchemaElementError { - constructor(field: string, collection?: string) { - super(`Field "${field}" not found${collection ? ` in collection "${collection}"` : ''}.`); - } -} 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..3d8ba59f79 100644 --- a/packages/datasource-toolkit/src/interfaces/query/condition-tree/factory.ts +++ b/packages/datasource-toolkit/src/interfaces/query/condition-tree/factory.ts @@ -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..c67f099498 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 { @@ -30,7 +31,7 @@ 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', + f => SchemaUtils.getColumn(collection.schema, f, collection.name).type === 'Column', ), ); } diff --git a/packages/datasource-toolkit/src/interfaces/query/projection/index.ts b/packages/datasource-toolkit/src/interfaces/query/projection/index.ts index 3043d5df9d..c5a10c81e1 100644 --- a/packages/datasource-toolkit/src/interfaces/query/projection/index.ts +++ b/packages/datasource-toolkit/src/interfaces/query/projection/index.ts @@ -54,7 +54,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..97a3b88c1b 100644 --- a/packages/datasource-toolkit/src/interfaces/query/sort/factory.ts +++ b/packages/datasource-toolkit/src/interfaces/query/sort/factory.ts @@ -8,7 +8,7 @@ export default class SortFactory { 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..0531372fe9 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.checkMissingField(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..78b18848e5 100644 --- a/packages/datasource-toolkit/src/utils/schema.ts +++ b/packages/datasource-toolkit/src/utils/schema.ts @@ -1,18 +1,90 @@ -import { CollectionSchema, ManyToManySchema, OneToManySchema } from '../interfaces/schema'; +import { ValidationError } from '../errors'; +import { + CollectionSchema, + ColumnSchema, + FieldSchema, + ManyToManySchema, + OneToManySchema, + RelationSchema, +} from '../interfaces/schema'; export default class SchemaUtils { + static checkAlreadyDefinedField( + schema: CollectionSchema, + fieldName: string, + collectionName?: string, + ): void { + if (schema.fields[fieldName]) { + const path = collectionName ? `${collectionName}.${fieldName}` : fieldName; + throw new ValidationError(`Field '${path}' already defined in schema`); + } + } + + static checkMissingField( + schema: CollectionSchema, + fieldName: string, + collectionName?: string, + ): void { + SchemaUtils.getField(schema, fieldName, collectionName); + } + + static getField( + schema: CollectionSchema, + fieldName: string, + collectionName?: string, + ): FieldSchema { + if (!schema.fields[fieldName]) { + const path = collectionName ? `${collectionName}.${fieldName}` : fieldName; + throw new ValidationError( + `Field '${path}' not found in ${Object.keys(schema.fields)} schema`, + ); + } + + return schema.fields[fieldName]; + } + + static getColumn( + schema: CollectionSchema, + fieldName: string, + collectionName?: string, + ): ColumnSchema { + const fields = Object.values(schema.fields).filter(field => field.type === 'Column'); + + if (!fields[fieldName]) { + const path = collectionName ? `${collectionName}.${fieldName}` : fieldName; + throw new ValidationError(`Column '${path}' not found in ${Object.keys(fields)} schema`); + } + + return schema.fields[fieldName] as ColumnSchema; + } + + static getRelation( + schema: CollectionSchema, + relationName: string, + collectionName?: string, + ): RelationSchema { + const relations = Object.values(schema.fields).filter(field => field.type !== 'Column'); + + if (!relations[relationName]) { + const path = collectionName ? `${collectionName}.${relationName}` : relationName; + throw new ValidationError(`Relation '${path}' not found in ${Object.keys(relations)} schema`); + } + + 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.getColumn(schema, fieldName); - return field.type === 'Column' && field.isPrimaryKey; + return field.isPrimaryKey; } static isForeignKey(schema: CollectionSchema, name: string): boolean { - const field = schema.fields[name]; + const field = this.getColumn(schema, name); return ( field.type === 'Column' && @@ -26,9 +98,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( diff --git a/packages/datasource-toolkit/src/validation/field.ts b/packages/datasource-toolkit/src/validation/field.ts index d80b1f88fd..31091de43a 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 { 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( @@ -27,11 +24,7 @@ export default class FieldValidator { } } else { const prefix = field.substring(0, dotIndex); - const schema = collection.schema.fields[prefix]; - - if (!schema) { - throw new ValidationError(`Relation not found: '${collection.name}.${prefix}'`); - } + const schema = SchemaUtils.getRelation(collection.schema, prefix, collection.name); if (schema.type !== 'ManyToOne' && schema.type !== 'OneToOne') { throw new ValidationError( 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/validation/field/field.test.ts b/packages/datasource-toolkit/test/validation/field/field.test.ts index ebecb4c1e8..cae74d719f 100644 --- a/packages/datasource-toolkit/test/validation/field/field.test.ts +++ b/packages/datasource-toolkit/test/validation/field/field.test.ts @@ -1,4 +1,3 @@ -import { MissingFieldError } from '../../../src/errors'; import FieldValidator from '../../../src/validation/field'; import * as factories from '../../__factories__'; diff --git a/packages/datasource-toolkit/test/validation/field/validate.test.ts b/packages/datasource-toolkit/test/validation/field/validate.test.ts index a4b9175a57..d682d49745 100644 --- a/packages/datasource-toolkit/test/validation/field/validate.test.ts +++ b/packages/datasource-toolkit/test/validation/field/validate.test.ts @@ -1,4 +1,3 @@ -import { MissingFieldError } from '../../../src'; import FieldValidator from '../../../src/validation/field'; import * as factories from '../../__factories__'; 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..664314fd94 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 { ColumnSchema, 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..be09655b1a 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 { ColumnSchema, 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..98acd27694 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 { ColumnSchema, 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..946c211d0d 100644 --- a/packages/plugin-flattener/src/flatten-relation/helpers.ts +++ b/packages/plugin-flattener/src/flatten-relation/helpers.ts @@ -2,7 +2,8 @@ import type { CollectionCustomizer, DataSourceCustomizer, } from '@forestadmin/datasource-customizer'; -import type { CollectionSchema, RelationSchema } from '@forestadmin/datasource-toolkit'; + +import { CollectionSchema, RelationSchema } from '@forestadmin/datasource-toolkit'; export function getColumns(schema: CollectionSchema): string[] { return Object.keys(schema.fields).filter(name => schema.fields[name].type === 'Column'); diff --git a/packages/plugin-flattener/test/flatten-relation.test.ts b/packages/plugin-flattener/test/flatten-relation.test.ts index 774ec81415..688e7607b9 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 * as factories from '@forestadmin/datasource-toolkit/dist/test/__factories__'; -import { MissingFieldError } from '@forestadmin/datasource-toolkit/src'; import flattenRelation from '../src/flatten-relation';