Skip to content

Commit

Permalink
fix(customizer-stack): reduce customizer stack to avoid issues (#1139)
Browse files Browse the repository at this point in the history
  • Loading branch information
Thenkei authored Jul 9, 2024
1 parent fc37f6f commit b13965f
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 79 deletions.
1 change: 1 addition & 0 deletions packages/agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ export default class Agent<S extends TSchema = TSchema> extends FrameworkMounter
// It allows to rebuild the full customization stack with no code customizations
this.nocodeCustomizer = new DataSourceCustomizer<S>({
ignoreMissingSchemaElementErrors: this.options.ignoreMissingSchemaElementErrors || false,
strategy: 'NoCode',
});
this.nocodeCustomizer.addDataSource(this.customizer.getFactory());
this.nocodeCustomizer.use(this.customizationService.addCustomizations);
Expand Down
10 changes: 7 additions & 3 deletions packages/datasource-customizer/src/collection-customizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { BinaryMode } from './decorators/binary/types';
import { CollectionChartDefinition } from './decorators/chart/types';
import { ComputedDefinition } from './decorators/computed/types';
import mapDeprecated from './decorators/computed/utils/map-deprecated';
import DecoratorsStack from './decorators/decorators-stack';
import DecoratorsStackBase from './decorators/decorators-stack-base';
import { HookHandler, HookPosition, HookType, HooksContext } from './decorators/hook/types';
import { OperatorDefinition } from './decorators/operators-emulate/types';
import {
Expand Down Expand Up @@ -42,14 +42,18 @@ export default class CollectionCustomizer<
N extends TCollectionName<S> = TCollectionName<S>,
> {
private readonly dataSourceCustomizer: DataSourceCustomizer<S>;
private readonly stack: DecoratorsStack;
private readonly stack: DecoratorsStackBase;
readonly name: string;

get schema(): CollectionSchema {
return this.stack.validation.getCollection(this.name).schema;
}

constructor(dataSourceCustomizer: DataSourceCustomizer<S>, stack: DecoratorsStack, name: string) {
constructor(
dataSourceCustomizer: DataSourceCustomizer<S>,
stack: DecoratorsStackBase,
name: string,
) {
this.dataSourceCustomizer = dataSourceCustomizer;
this.name = name;
this.stack = stack;
Expand Down
8 changes: 7 additions & 1 deletion packages/datasource-customizer/src/datasource-customizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import CollectionCustomizer from './collection-customizer';
import { DataSourceChartDefinition } from './decorators/chart/types';
import CompositeDatasource from './decorators/composite-datasource';
import DecoratorsStack from './decorators/decorators-stack';
import DecoratorsStackNoCode from './decorators/decorators-stack-no-code';
import PublicationDataSourceDecorator from './decorators/publication/datasource';
import RenameCollectionDataSourceDecorator from './decorators/rename-collection/datasource';
import { TCollectionName, TSchema } from './templates';
Expand All @@ -18,6 +19,7 @@ import TypingGenerator from './typing-generator';

export type Options = {
ignoreMissingSchemaElementErrors?: boolean;
strategy?: 'Normal' | 'NoCode';
};

/**
Expand Down Expand Up @@ -52,7 +54,11 @@ export default class DataSourceCustomizer<S extends TSchema = TSchema> {

constructor(options?: Options) {
this.compositeDataSource = new CompositeDatasource<Collection>();
this.stack = new DecoratorsStack(this.compositeDataSource, options);

this.stack = new {
NoCode: DecoratorsStackNoCode,
Normal: DecoratorsStack,
}[options?.strategy ?? 'Normal'](this.compositeDataSource, options);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {
DataSource,
DataSourceDecorator,
Logger,
MissingSchemaElementError,
} from '@forestadmin/datasource-toolkit';

import ActionCollectionDecorator from './actions/collection';
import BinaryCollectionDecorator from './binary/collection';
import ChartDataSourceDecorator from './chart/datasource';
import ComputedCollectionDecorator from './computed/collection';
import HookCollectionDecorator from './hook/collection';
import OperatorsEmulateCollectionDecorator from './operators-emulate/collection';
import OverrideCollectionDecorator from './override/collection';
import PublicationDataSourceDecorator from './publication/datasource';
import RelationCollectionDecorator from './relation/collection';
import RenameFieldCollectionDecorator from './rename-field/collection';
import SchemaCollectionDecorator from './schema/collection';
import SearchCollectionDecorator from './search/collection';
import SegmentCollectionDecorator from './segment/collection';
import SortEmulateCollectionDecorator from './sort-emulate/collection';
import ValidationCollectionDecorator from './validation/collection';
import WriteDataSourceDecorator from './write/datasource';

export type Options = {
ignoreMissingSchemaElementErrors?: boolean;
};

export default abstract class DecoratorsStackBase {
protected customizations: Array<(logger: Logger) => Promise<void>> = [];
private options: Required<Options>;

public dataSource: DataSource;

action: DataSourceDecorator<ActionCollectionDecorator>;
binary: DataSourceDecorator<BinaryCollectionDecorator>;
chart: ChartDataSourceDecorator;
earlyComputed: DataSourceDecorator<ComputedCollectionDecorator>;
earlyOpEmulate: DataSourceDecorator<OperatorsEmulateCollectionDecorator>;
hook: DataSourceDecorator<HookCollectionDecorator>;
lateComputed: DataSourceDecorator<ComputedCollectionDecorator>;
lateOpEmulate: DataSourceDecorator<OperatorsEmulateCollectionDecorator>;
publication: PublicationDataSourceDecorator;
relation: DataSourceDecorator<RelationCollectionDecorator>;
renameField: DataSourceDecorator<RenameFieldCollectionDecorator>;
schema: DataSourceDecorator<SchemaCollectionDecorator>;
search: DataSourceDecorator<SearchCollectionDecorator>;
segment: DataSourceDecorator<SegmentCollectionDecorator>;
sortEmulate: DataSourceDecorator<SortEmulateCollectionDecorator>;
validation: DataSourceDecorator<ValidationCollectionDecorator>;
write: WriteDataSourceDecorator;
override: DataSourceDecorator<OverrideCollectionDecorator>;

finalizeStackSetup(dataSource: DataSource, options?: Options) {
this.dataSource = dataSource;

this.options = {
ignoreMissingSchemaElementErrors: false,
...(options || {}),
};
}

queueCustomization(customization: (logger: Logger) => Promise<void>): void {
this.customizations.push(customization);
}

/**
* Apply all customizations
* Plugins may queue new customizations, or call other plugins which will queue customizations.
*
* This method will be called recursively and clears the queue at each recursion to ensure
* that all customizations are applied in the right order.
*/
async applyQueuedCustomizations(logger: Logger): Promise<void> {
const queuedCustomizations = this.customizations.slice();
this.customizations.length = 0;

while (queuedCustomizations.length) {
const firstInQueue = queuedCustomizations.shift();

try {
await firstInQueue(logger); // eslint-disable-line no-await-in-loop
} catch (e) {
if (
this.options.ignoreMissingSchemaElementErrors &&
e instanceof MissingSchemaElementError
) {
logger('Warn', e.message, e);
} else {
throw e;
}
}

await this.applyQueuedCustomizations(logger); // eslint-disable-line no-await-in-loop
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { DataSource, DataSourceDecorator } from '@forestadmin/datasource-toolkit';

import ActionCollectionDecorator from './actions/collection';
import DecoratorsStackBase, { Options } from './decorators-stack-base';
import SchemaCollectionDecorator from './schema/collection';
import ValidationCollectionDecorator from './validation/collection';

export default class DecoratorsStackNoCode extends DecoratorsStackBase {
constructor(dataSource: DataSource, options?: Options) {
super();

// It's actually the initial stack in this case. :)
let last: DataSource = dataSource;

// We only need those for the No Code use cases.

/* eslint-disable no-multi-assign */
last = this.action = new DataSourceDecorator(last, ActionCollectionDecorator);
last = this.schema = new DataSourceDecorator(last, SchemaCollectionDecorator);
last = this.validation = new DataSourceDecorator(last, ValidationCollectionDecorator);

this.finalizeStackSetup(last, options);
}
}
81 changes: 6 additions & 75 deletions packages/datasource-customizer/src/decorators/decorators-stack.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import {
DataSource,
DataSourceDecorator,
Logger,
MissingSchemaElementError,
} from '@forestadmin/datasource-toolkit';
import { DataSource, DataSourceDecorator } from '@forestadmin/datasource-toolkit';

import ActionCollectionDecorator from './actions/collection';
import BinaryCollectionDecorator from './binary/collection';
import ChartDataSourceDecorator from './chart/datasource';
import ComputedCollectionDecorator from './computed/collection';
import DecoratorsStackBase, { Options } from './decorators-stack-base';
import EmptyCollectionDecorator from './empty/collection';
import HookCollectionDecorator from './hook/collection';
import OperatorsEmulateCollectionDecorator from './operators-emulate/collection';
Expand All @@ -24,35 +20,10 @@ import SortEmulateCollectionDecorator from './sort-emulate/collection';
import ValidationCollectionDecorator from './validation/collection';
import WriteDataSourceDecorator from './write/datasource';

export type Options = {
ignoreMissingSchemaElementErrors?: boolean;
};

export default class DecoratorsStack {
action: DataSourceDecorator<ActionCollectionDecorator>;
binary: DataSourceDecorator<BinaryCollectionDecorator>;
chart: ChartDataSourceDecorator;
earlyComputed: DataSourceDecorator<ComputedCollectionDecorator>;
earlyOpEmulate: DataSourceDecorator<OperatorsEmulateCollectionDecorator>;
hook: DataSourceDecorator<HookCollectionDecorator>;
lateComputed: DataSourceDecorator<ComputedCollectionDecorator>;
lateOpEmulate: DataSourceDecorator<OperatorsEmulateCollectionDecorator>;
publication: PublicationDataSourceDecorator;
relation: DataSourceDecorator<RelationCollectionDecorator>;
renameField: DataSourceDecorator<RenameFieldCollectionDecorator>;
schema: DataSourceDecorator<SchemaCollectionDecorator>;
search: DataSourceDecorator<SearchCollectionDecorator>;
segment: DataSourceDecorator<SegmentCollectionDecorator>;
sortEmulate: DataSourceDecorator<SortEmulateCollectionDecorator>;
validation: DataSourceDecorator<ValidationCollectionDecorator>;
write: WriteDataSourceDecorator;
dataSource: DataSource;
override: DataSourceDecorator<OverrideCollectionDecorator>;

private customizations: Array<(logger: Logger) => Promise<void>> = [];
private options: Required<Options>;

export default class DecoratorsStack extends DecoratorsStackBase {
constructor(dataSource: DataSource, options?: Options) {
super();

let last: DataSource = dataSource;

/* eslint-disable no-multi-assign */
Expand Down Expand Up @@ -92,46 +63,6 @@ export default class DecoratorsStack {
last = this.renameField = new DataSourceDecorator(last, RenameFieldCollectionDecorator);
/* eslint-enable no-multi-assign */

this.dataSource = last;

this.options = {
ignoreMissingSchemaElementErrors: false,
...(options || {}),
};
}

queueCustomization(customization: (logger: Logger) => Promise<void>): void {
this.customizations.push(customization);
}

/**
* Apply all customizations
* Plugins may queue new customizations, or call other plugins which will queue customizations.
*
* This method will be called recursively and clears the queue at each recursion to ensure
* that all customizations are applied in the right order.
*/
async applyQueuedCustomizations(logger: Logger): Promise<void> {
const queuedCustomizations = this.customizations.slice();
this.customizations.length = 0;

while (queuedCustomizations.length) {
const firstInQueue = queuedCustomizations.shift();

try {
await firstInQueue(logger); // eslint-disable-line no-await-in-loop
} catch (e) {
if (
this.options.ignoreMissingSchemaElementErrors &&
e instanceof MissingSchemaElementError
) {
logger('Warn', e.message, e);
} else {
throw e;
}
}

await this.applyQueuedCustomizations(logger); // eslint-disable-line no-await-in-loop
}
this.finalizeStackSetup(last, options);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type { DataSource } from '@forestadmin/datasource-toolkit';

import { MissingCollectionError } from '@forestadmin/datasource-toolkit';

import DecoratorsStackNoCode from '../../src/decorators/decorators-stack-no-code';

function makeMockDataSource(): jest.Mocked<DataSource> {
return {
schema: {
charts: [] as string[],
},
collections: [],
getCollection: jest.fn(),
renderChart: jest.fn(),
};
}

describe('DecoratorsStackNoCode', () => {
describe('applyQueuedCustomizations', () => {
describe('when ignoreMissingSchemaElementErrors is false', () => {
it('should throw an error when a customization fails', async () => {
const logger = jest.fn();
const customization = jest.fn().mockRejectedValue(new Error('Customization failed'));

const stack = new DecoratorsStackNoCode(makeMockDataSource(), {
ignoreMissingSchemaElementErrors: false,
});
stack.queueCustomization(customization);

await expect(stack.applyQueuedCustomizations(logger)).rejects.toThrow(
'Customization failed',
);
});
});

describe('when ignoreMissingSchemaElementErrors is true', () => {
it('should log an error when a customization fails with a MissingCollectionError', async () => {
const logger = jest.fn();
const error = new MissingCollectionError('Customization failed');
const customization = jest.fn().mockRejectedValue(error);

const stack = new DecoratorsStackNoCode(makeMockDataSource(), {
ignoreMissingSchemaElementErrors: true,
});
stack.queueCustomization(customization);

await stack.applyQueuedCustomizations(logger);

expect(logger).toHaveBeenCalledWith('Warn', 'Customization failed', error);
});

it('should continue to apply other customizations after a MissingCollectionError', async () => {
const logger = jest.fn();
const error = new MissingCollectionError('Customization failed');
const customization1 = jest.fn().mockRejectedValue(error);
const customization2 = jest.fn();

const stack = new DecoratorsStackNoCode(makeMockDataSource(), {
ignoreMissingSchemaElementErrors: true,
});
stack.queueCustomization(customization1);
stack.queueCustomization(customization2);

await stack.applyQueuedCustomizations(logger);

expect(customization1).toHaveBeenCalled();
expect(customization2).toHaveBeenCalled();
});

it('should rethrow other errors', async () => {
const logger = jest.fn();
const error = new Error('Customization failed');
const customization = jest.fn().mockRejectedValue(error);

const stack = new DecoratorsStackNoCode(makeMockDataSource(), {
ignoreMissingSchemaElementErrors: true,
});
stack.queueCustomization(customization);

await expect(stack.applyQueuedCustomizations(logger)).rejects.toThrow(
'Customization failed',
);
});
});
});
});

0 comments on commit b13965f

Please sign in to comment.