Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: validate custom actions #786

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
8 changes: 7 additions & 1 deletion packages/datasource-customizer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,15 @@
},
"dependencies": {
"@forestadmin/datasource-toolkit": "1.7.0",
"ajv": "^8.12.0",
"ajv-errors": "^3.0.0",
"ajv-keywords": "^5.1.0",
"file-type": "^16.5.4",
"luxon": "^3.2.1",
"object-hash": "^3.0.0",
"uuid": "^9.0.0"
"uuid": "^9.0.0",
"zod": "^3.21.4",
"zod-error": "^1.5.0",
"zod-validation-error": "^1.3.1"
}
}
16 changes: 16 additions & 0 deletions packages/datasource-customizer/src/collection-customizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@ import { RelationDefinition } from './decorators/relation/types';
import { SearchDefinition } from './decorators/search/types';
import { SegmentDefinition } from './decorators/segment/types';
import { WriteDefinition } from './decorators/write/write-replace/types';
import {
ActionConfigurationValidationError,
CollectionCustomizationValidationError,
} from './errors';
import addExternalRelation from './plugins/add-external-relation';
import importField from './plugins/import-field';
import { TCollectionName, TColumnName, TFieldName, TSchema, TSortClause } from './templates';
import { OneToManyEmbeddedDefinition, Plugin } from './types';
import ActionValidator from './validators/action';

export default class CollectionCustomizer<
S extends TSchema = TSchema,
Expand Down Expand Up @@ -124,6 +129,17 @@ export default class CollectionCustomizer<
* })
*/
addAction(name: string, definition: ActionDefinition<S, N>): this {
try {
ActionValidator.validateActionConfiguration(
name,
definition as unknown as ActionDefinition<TSchema, string>,
);
} catch (error) {
if (error instanceof ActionConfigurationValidationError) {
throw new CollectionCustomizationValidationError(this.name, error.message);
}
}

return this.pushCustomization(async () => {
this.stack.action
.getCollection(this.name)
Expand Down
150 changes: 126 additions & 24 deletions packages/datasource-customizer/src/decorators/actions/types/actions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ActionResult, ActionScope } from '@forestadmin/datasource-toolkit';
import { ActionResult, ActionScope, ActionScopeEnum } from '@forestadmin/datasource-toolkit';
import { ENUM } from 'sequelize';
import { z } from 'zod';

import { DynamicField } from './fields';
import { TCollectionName, TSchema } from '../../../templates';
Expand All @@ -7,38 +9,138 @@ import ActionContextSingle from '../context/single';
import ResultBuilder from '../result-builder';

export { ActionContext, ActionContextSingle };
const scope = [
...Object.values(ActionScopeEnum),
...Object.values(ActionScopeEnum).map(scope => scope.toLowerCase()),
];

export interface BaseAction<
// export const actionSchema = {
// type: 'object',
// properties: {
// generateFile: { type: 'boolean' },
// scope: {
// type: 'string',
// enum: [
// ...Object.values(ActionScopeEnum),
// ...Object.values(ActionScopeEnum).map(scope => scope.toLowerCase()),
// ],
// },
// form: { type: 'array' }, // validated in fieldActionSchema
// execute: { typeof: 'function' },
// },
// required: ['scope', 'execute'],
// additionalProperties: false,
// };

function actionSchema<
S extends TSchema,
N extends TCollectionName<S>,
Scope extends ActionScope,
Context extends ActionContext<S, N>,
> {
generateFile?: boolean;
scope: Scope;
form?: DynamicField<Context>[];
execute(
context: Context,
resultBuilder: ResultBuilder,
): void | ActionResult | Promise<void> | Promise<ActionResult>;
>() {
const returnType = z.union([z.void(), z.any() as z.ZodSchema<ActionResult>]);

return z.object({
generateFile: z.boolean().optional(),
scope: z.enum(['Bulk', 'Global', 'Single']),
form: (z.any() as z.ZodSchema<DynamicField<Context>>).array().optional(),
execute: z
.function()
.args(z.any() as z.ZodSchema<Context>, z.any() as z.ZodSchema<ResultBuilder>)
.returns(z.union([returnType, returnType.promise()])),
});
}

export type ActionGlobal<
S extends TSchema = TSchema,
N extends TCollectionName<S> = TCollectionName<S>,
> = BaseAction<S, N, 'Global', ActionContext<S, N>>;
// function actionSchema<HandlerArgType extends z.ZodTypeAny, ResultOrValueType extends z.ZodTypeAny>(
// fieldValueType: ResultOrValueType,
// ) {
// return z.object({
// generateFile: z.boolean().optional(),
// scope: z.enum([scope[0], ...scope.slice(1)]),
// form: z.object({}).array().optional(),
// execute: z.function(),
// });
// }

export type ActionBulk<
S extends TSchema = TSchema,
N extends TCollectionName<S> = TCollectionName<S>,
> = BaseAction<S, N, 'Bulk', ActionContext<S, N>>;

export type ActionSingle<
S extends TSchema = TSchema,
N extends TCollectionName<S> = TCollectionName<S>,
> = BaseAction<S, N, 'Single', ActionContextSingle<S, N>>;
type t = ReturnType<
typeof actionSchema<
TSchema,
TCollectionName<TSchema>,
'Global',
ActionContext<TSchema, TCollectionName<TSchema>>
>
>;

export type ActionDefinition<
S extends TSchema = TSchema,
N extends TCollectionName<S> = TCollectionName<S>,
> = ActionSingle<S, N> | ActionBulk<S, N> | ActionGlobal<S, N>;
> = z.infer<t>;

const action: ActionDefinition = {
scope: 'Bulk',
form: [{ type: 'Boolean', label: 'aaa' }],
execute: async (toto, baby) => {
console.log('aa');
},
};

const action2: DynamicField<ActionContext> = {
type: 'Collection',
};

// export interface BaseAction<
// S extends TSchema,
// N extends TCollectionName<S>,
// Scope extends ActionScope,
// Context extends ActionContext<S, N>,
// > {
// generateFile?: boolean;
// scope: Scope;
// form?: DynamicField<Context>[];
// execute(
// context: Context,
// resultBuilder: ResultBuilder,
// ): void | ActionResult | Promise<void> | Promise<ActionResult>;
// }

// export type ActionGlobal<
// S extends TSchema = TSchema,
// N extends TCollectionName<S> = TCollectionName<S>,
// > = BaseAction<S, N, 'Global', ActionContext<S, N>>;
// type t = ReturnType<
// typeof actionSchema<
// TSchema,
// TCollectionName<TSchema>,
// 'Global',
// ActionContext<TSchema, TCollectionName<TSchema>>
// >
// >;
// export type ActionGlobal = z.infer<t>;
// const ag: ActionGlobal = {
// scope: 'Toto',
// };

// const action: z.infer<ReturnType<typeof actionSchema>> = {
// scope: 'bulk',
// generateFile: true,
// form: [],
// execute: async (toto, baby) => {
// console.log('aa');
// },
// };
//

// export type ActionBulk<
// S extends TSchema = TSchema,
// N extends TCollectionName<S> = TCollectionName<S>,
// > = BaseAction<S, N, 'Bulk', ActionContext<S, N>>;
//
// export type ActionSingle<
// S extends TSchema = TSchema,
// N extends TCollectionName<S> = TCollectionName<S>,
// > = BaseAction<S, N, 'Single', ActionContextSingle<S, N>>;
//
// export type ActionDefinition<
// S extends TSchema = TSchema,
// N extends TCollectionName<S> = TCollectionName<S>,
// > = ActionSingle<S, N> | ActionBulk<S, N> | ActionGlobal<S, N>;
Loading