Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/datasource-customizer/src/collection-customizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,20 @@ export default class CollectionCustomizer<
});
}

/**
* Disable filtering on a specific field for the end-user.
* Operators stay available to the customizations, so the field can still be used
* as the key of a custom relation.
* @param name the name of the field with filtering to be disabled
* @example
* .disableFieldFiltering('authorId');
*/
disableFieldFiltering(name: TColumnName<S, N>): this {
return this.pushCustomization(async () => {
this.stack.schema.getCollection(this.name).disableFieldFiltering(name);
});
}

/**
* Enable sorting on a specific field using emulation.
* As for all the emulation method, the field sorting will be done in-memory.
Expand Down
32 changes: 28 additions & 4 deletions packages/datasource-customizer/src/decorators/schema/collection.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,45 @@
import type { CollectionSchema } from '@forestadmin/datasource-toolkit';
import type { CollectionSchema, ColumnSchema } from '@forestadmin/datasource-toolkit';

import { CollectionDecorator } from '@forestadmin/datasource-toolkit';
import {
CollectionDecorator,
FieldValidator,
ValidationError,
} from '@forestadmin/datasource-toolkit';

/**
* This decorator allows to override parts of the collections schema.
* It can be used to toggle off collection level capabilities for performance
* (for now, list-view counts and the search bar)
* (for now, list-view counts and the search bar), or to hide filter operators
* that decorators below still rely on (e.g. 'In' on a custom relation key).
*/
export default class SchemaCollectionDecorator extends CollectionDecorator {
private schemaOverride: Partial<CollectionSchema> = {};
private unfilterableFields = new Set<string>();

overrideSchema(value: Partial<CollectionSchema>): void {
Object.assign(this.schemaOverride, value);
this.markSchemaAsDirty();
}

disableFieldFiltering(name: string): void {
FieldValidator.validate(this, name);

if ((this.childCollection.schema.fields[name] as ColumnSchema).isPrimaryKey) {
throw new ValidationError(`Cannot disable filtering on primary key '${this.name}.${name}'`);
}

this.unfilterableFields.add(name);
this.markSchemaAsDirty();
}

protected override refineSchema(subSchema: CollectionSchema): CollectionSchema {
return { ...subSchema, ...this.schemaOverride };
const fields = { ...subSchema.fields };

// Empty Set rather than undefined: FilterFactory calls filterOperators.has() unguarded
for (const name of this.unfilterableFields) {
fields[name] = { ...(fields[name] as ColumnSchema), filterOperators: new Set() };
}

return { ...subSchema, fields, ...this.schemaOverride };
}
}
47 changes: 47 additions & 0 deletions packages/datasource-customizer/test/collection-customizer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,53 @@ describe('Builder > Collection', () => {
});
});

describe('disableFieldFiltering', () => {
it('should remove the operators of the field from the schema', async () => {
const { dsc, customizer } = await setup();

const self = customizer.disableFieldFiltering('firstName');
await dsc.getDataSource(logger);

expect(self.schema.fields.firstName).toEqual(
expect.objectContaining({ filterOperators: new Set() }),
);
expect(self).toEqual(customizer);
});

it('should still allow the field to be the key of a custom one to one', async () => {
const { dsc, customizer, bookCustomizer } = await setup();

customizer.emulateFieldOperator('firstName', 'In').disableFieldFiltering('firstName');
bookCustomizer
.emulateFieldOperator('title', 'In')
.addOneToOneRelation('myAuthor', 'authors', {
originKey: 'firstName',
originKeyTarget: 'title',
});
await dsc.getDataSource(logger);

expect(bookCustomizer.schema.fields.myAuthor).toEqual({
type: 'OneToOne',
foreignCollection: 'authors',
originKey: 'firstName',
originKeyTarget: 'title',
});
expect(customizer.schema.fields.firstName).toEqual(
expect.objectContaining({ filterOperators: new Set() }),
);
});

it('should throw on a primary key', async () => {
const { dsc, customizer } = await setup();

customizer.disableFieldFiltering('authorId');

await expect(dsc.getDataSource(logger)).rejects.toThrow(
"Cannot disable filtering on primary key 'authors.authorId'",
);
});
});

describe('disableSearch', () => {
it('should edit the schema', async () => {
const { dsc, customizer } = await setup();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,83 @@ describe('SchemaCollectionDecorator', () => {
expect(collection.schema.countable).toBe(true);
expect(decorator.schema.countable).toBe(false);
});

describe('disableFieldFiltering', () => {
const buildDecorator = () => {
const collection = factories.collection.build({
name: 'books',
schema: factories.collectionSchema.build({
fields: {
id: factories.columnSchema.uuidPrimaryKey().build({
filterOperators: new Set(['Equal', 'In']),
}),
authorId: factories.columnSchema.build({
filterOperators: new Set(['Equal', 'In', 'Present']),
}),
title: factories.columnSchema.build({ filterOperators: new Set(['Equal']) }),
author: factories.manyToOneSchema.build({
foreignCollection: 'authors',
foreignKey: 'authorId',
}),
},
}),
});

return { collection, decorator: new SchemaCollectionDecorator(collection, null) };
};

it('should expose an empty operator set on the field', () => {
const { decorator } = buildDecorator();

decorator.disableFieldFiltering('authorId');

expect(decorator.schema.fields.authorId).toEqual(
expect.objectContaining({ type: 'Column', filterOperators: new Set() }),
);
});

it('should keep the operators of the child collection', () => {
const { collection, decorator } = buildDecorator();

decorator.disableFieldFiltering('authorId');

expect(collection.schema.fields.authorId).toEqual(
expect.objectContaining({ filterOperators: new Set(['Equal', 'In', 'Present']) }),
);
});

it('should leave the other fields untouched', () => {
const { decorator } = buildDecorator();

decorator.disableFieldFiltering('authorId');

expect(decorator.schema.fields.title).toEqual(
expect.objectContaining({ filterOperators: new Set(['Equal']) }),
);
});

it('should throw on a primary key', () => {
const { decorator } = buildDecorator();

expect(() => decorator.disableFieldFiltering('id')).toThrow(
"Cannot disable filtering on primary key 'books.id'",
);
});

it('should throw on a relation', () => {
const { decorator } = buildDecorator();

expect(() => decorator.disableFieldFiltering('author')).toThrow(
"Unexpected field type: 'books.author' (found 'ManyToOne' expected 'Column')",
);
});

it('should throw on an unknown field', () => {
const { decorator } = buildDecorator();

expect(() => decorator.disableFieldFiltering('unknown')).toThrow(
"The 'books.unknown' field was not found",
);
});
});
});
Loading