-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(customizer-stack): reduce customizer stack to avoid issues (#1139)
- Loading branch information
Showing
7 changed files
with
228 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
97 changes: 97 additions & 0 deletions
97
packages/datasource-customizer/src/decorators/decorators-stack-base.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
packages/datasource-customizer/src/decorators/decorators-stack-no-code.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
86 changes: 86 additions & 0 deletions
86
packages/datasource-customizer/test/decorators/decorators-stack-no-code.unit.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
); | ||
}); | ||
}); | ||
}); | ||
}); |