mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Add new schema util, types
This commit is contained in:
parent
c164c0d0ce
commit
97391df49d
20
packages/nodes-base/types.d.ts
vendored
Normal file
20
packages/nodes-base/types.d.ts
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
import type { StrapiApiCredential } from './credentials/StrapiApi.credentials';
|
||||
import type { StrapiTokenApiCredential } from './credentials/StrapiTokenApi.credentials';
|
||||
|
||||
type CredentialSchemaMap = {
|
||||
strapiApi: StrapiApiCredential;
|
||||
strapiTokenApi: StrapiTokenApiCredential;
|
||||
};
|
||||
|
||||
declare module 'n8n-workflow' {
|
||||
interface FunctionsBase {
|
||||
getCredentials<Type extends keyof CredentialSchemaMap | {}>(
|
||||
type: Type,
|
||||
itemIndex?: number,
|
||||
): Promise<
|
||||
Type extends keyof CredentialSchemaMap
|
||||
? CredentialSchemaMap[Type]
|
||||
: ICredentialDataDecryptedObject
|
||||
>;
|
||||
}
|
||||
}
|
236
packages/nodes-base/utils/CredentialSchema.ts
Normal file
236
packages/nodes-base/utils/CredentialSchema.ts
Normal file
|
@ -0,0 +1,236 @@
|
|||
import type { INodeProperties } from 'n8n-workflow';
|
||||
import z, { type ZodOptional, type ZodType } from 'zod';
|
||||
|
||||
function isObject(value: unknown): value is object {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function removeUndefinedProperties<T extends object>(obj: T): T {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value === undefined) {
|
||||
delete (obj as Record<string, unknown>)[key];
|
||||
} else if (isObject(value)) {
|
||||
removeUndefinedProperties(value);
|
||||
if (Object.keys(value).length === 0) {
|
||||
delete (obj as Record<string, unknown>)[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
class CredentialSchemaRootObject<
|
||||
M extends BaseMetadata,
|
||||
S extends ZodType | null,
|
||||
T extends { [k: string]: CredentialSchemaProperty<M, S> } = {},
|
||||
> {
|
||||
constructor(private shape: T) {}
|
||||
|
||||
validate<Data>(data: Data) {
|
||||
return this.toZodSchema().safeParse(data);
|
||||
}
|
||||
|
||||
toZodSchema() {
|
||||
return z.object(
|
||||
Object.fromEntries(
|
||||
Object.entries(this.shape)
|
||||
.filter(([_, property]) => property.toZodSchema())
|
||||
.map(([key, property]) => [key, property.toZodSchema()]),
|
||||
) as ZodifyObject<M, S, T>,
|
||||
);
|
||||
}
|
||||
|
||||
getProperty<K extends keyof T>(key: K): T[K]['metadata'] {
|
||||
return this.shape[key].metadata;
|
||||
}
|
||||
|
||||
toNodeProperties() {
|
||||
return Object.entries(this.shape).map(([key, schema]) => schema.toNodeProperties(key));
|
||||
}
|
||||
}
|
||||
|
||||
type ToZodSchemaReturnType<
|
||||
M extends BaseMetadata = BaseMetadata,
|
||||
S extends ZodType | null = ZodType,
|
||||
> = M['optional'] extends true ? (S extends null ? null : ZodOptional<NonNullable<S>>) : S;
|
||||
|
||||
abstract class CredentialSchemaProperty<
|
||||
M extends BaseMetadata = BaseMetadata,
|
||||
S extends ZodType | null = null,
|
||||
> {
|
||||
constructor(
|
||||
public metadata: M,
|
||||
public schema: S,
|
||||
) {}
|
||||
|
||||
toZodSchema(): ToZodSchemaReturnType<M, S> {
|
||||
if (this.schema && this.metadata.optional) {
|
||||
return this.schema.optional() as ToZodSchemaReturnType<M, S>;
|
||||
}
|
||||
|
||||
return this.schema as ToZodSchemaReturnType<M, S>;
|
||||
}
|
||||
|
||||
toNodeProperties(name: string): INodeProperties {
|
||||
return removeUndefinedProperties({
|
||||
name,
|
||||
displayName: this.metadata.label,
|
||||
description: this.metadata.description,
|
||||
default: '',
|
||||
type: 'string',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class CredentialSchemaString<
|
||||
S extends ZodType,
|
||||
M extends StringMetadata,
|
||||
> extends CredentialSchemaProperty<M, S> {
|
||||
constructor(
|
||||
public metadata: M,
|
||||
schema: S,
|
||||
) {
|
||||
super(metadata, schema);
|
||||
}
|
||||
|
||||
toNodeProperties(name: string): INodeProperties {
|
||||
return removeUndefinedProperties({
|
||||
...super.toNodeProperties(name),
|
||||
type: 'string',
|
||||
placeholder: this.metadata.placeholder,
|
||||
typeOptions: { password: this.metadata.password },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class CredentialSchemaOptions<
|
||||
V extends string,
|
||||
S extends ZodType,
|
||||
M extends OptionsMetadata<V>,
|
||||
> extends CredentialSchemaProperty<M, S> {
|
||||
constructor(
|
||||
public metadata: M,
|
||||
schema: S,
|
||||
) {
|
||||
super(metadata, schema);
|
||||
}
|
||||
|
||||
toNodeProperties(name: string): INodeProperties {
|
||||
const { options } = this.metadata;
|
||||
return removeUndefinedProperties({
|
||||
...super.toNodeProperties(name),
|
||||
type: 'options',
|
||||
options: options.map((option) => ({
|
||||
name: option.label,
|
||||
value: option.value,
|
||||
description: option.description,
|
||||
})),
|
||||
default: options.find((option) => option.default)?.value ?? options[0].value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class CredentialSchemaNotice extends CredentialSchemaProperty<BaseMetadata, null> {
|
||||
constructor(public notice: string) {
|
||||
super({ label: notice }, null);
|
||||
}
|
||||
|
||||
toNodeProperties(name: string): INodeProperties {
|
||||
return {
|
||||
...super.toNodeProperties(name),
|
||||
type: 'notice',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type BaseMetadata = {
|
||||
label: string;
|
||||
description?: string;
|
||||
default?: unknown;
|
||||
optional?: boolean;
|
||||
};
|
||||
|
||||
type StringMetadata = BaseMetadata &
|
||||
Partial<{
|
||||
password: boolean;
|
||||
placeholder: string;
|
||||
default: string;
|
||||
}>;
|
||||
|
||||
type Option<V extends string> = {
|
||||
label: string;
|
||||
value: V;
|
||||
default?: boolean;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
type NonEmptyArray<T> = [T, ...T[]];
|
||||
type OptionsMetadata<V extends string> = BaseMetadata & { options: NonEmptyArray<Option<V>> };
|
||||
|
||||
type Zodify<
|
||||
M extends BaseMetadata,
|
||||
S extends ZodType | null,
|
||||
T extends CredentialSchemaProperty<M, S>,
|
||||
> = ReturnType<T['toZodSchema']> extends z.ZodType ? ReturnType<T['toZodSchema']> : never;
|
||||
|
||||
type ZodifyObject<
|
||||
M extends BaseMetadata,
|
||||
S extends ZodType | null,
|
||||
T extends { [k: string]: CredentialSchemaProperty<M, S> },
|
||||
> = {
|
||||
[K in keyof T as ReturnType<T[K]['toZodSchema']> extends z.ZodType ? K : never]: Zodify<
|
||||
M,
|
||||
S,
|
||||
T[K]
|
||||
>;
|
||||
};
|
||||
|
||||
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||
|
||||
export const CredentialSchema = {
|
||||
create<
|
||||
M extends BaseMetadata,
|
||||
S extends ZodType | null,
|
||||
T extends { [k: string]: CredentialSchemaProperty<M, S> },
|
||||
>(shape: T) {
|
||||
return new CredentialSchemaRootObject(shape);
|
||||
},
|
||||
|
||||
password(options: Omit<Optional<StringMetadata, 'label'>, 'password'> = {}) {
|
||||
return new CredentialSchemaString(
|
||||
{
|
||||
password: true,
|
||||
label: 'Password',
|
||||
...options,
|
||||
},
|
||||
z.string(),
|
||||
);
|
||||
},
|
||||
// eslint-disable-next-line id-denylist
|
||||
string<M extends StringMetadata>(options: M) {
|
||||
return new CredentialSchemaString(options, z.string());
|
||||
},
|
||||
url(options: Optional<StringMetadata, 'label'> = {}) {
|
||||
return new CredentialSchemaString({ label: 'URL', ...options }, z.string().url());
|
||||
},
|
||||
email(options: Optional<StringMetadata, 'label'> = {}) {
|
||||
return new CredentialSchemaString({ label: 'Email', ...options }, z.string().email());
|
||||
},
|
||||
options<V extends string, M extends OptionsMetadata<V>>(options: M) {
|
||||
return new CredentialSchemaOptions(
|
||||
options,
|
||||
z.enum(
|
||||
options.options.map((option) => option.value) as NonEmptyArray<
|
||||
M['options'][number]['value']
|
||||
>,
|
||||
),
|
||||
);
|
||||
},
|
||||
notice(notice: string) {
|
||||
return new CredentialSchemaNotice(notice);
|
||||
},
|
||||
};
|
||||
|
||||
export type InferCredentialSchema<
|
||||
T extends CredentialSchemaRootObject<BaseMetadata, ZodType | null>,
|
||||
> = z.infer<ReturnType<T['toZodSchema']>>;
|
131
packages/nodes-base/utils/__tests__/CredentialSchema.test.ts
Normal file
131
packages/nodes-base/utils/__tests__/CredentialSchema.test.ts
Normal file
|
@ -0,0 +1,131 @@
|
|||
import { CredentialSchema } from '../CredentialSchema';
|
||||
|
||||
describe('CredentialSchema', () => {
|
||||
test('should convert Strapi credential to node properties', () => {
|
||||
expect(
|
||||
CredentialSchema.create({
|
||||
notice: CredentialSchema.notice(
|
||||
'Make sure you are using a user account not an admin account',
|
||||
),
|
||||
email: CredentialSchema.email({ placeholder: 'name@email.com' }),
|
||||
password: CredentialSchema.password(),
|
||||
url: CredentialSchema.url({
|
||||
placeholder: 'https://api.example.com',
|
||||
}),
|
||||
apiVersion: CredentialSchema.options({
|
||||
label: 'API Version',
|
||||
description: 'The version of api to be used',
|
||||
options: [
|
||||
{
|
||||
label: 'Version 4',
|
||||
value: 'v4',
|
||||
description: 'API version supported by Strapi 4',
|
||||
},
|
||||
{
|
||||
label: 'Version 3',
|
||||
value: 'v3',
|
||||
default: true,
|
||||
description: 'API version supported by Strapi 3',
|
||||
},
|
||||
],
|
||||
}),
|
||||
}).toNodeProperties(),
|
||||
).toEqual([
|
||||
{
|
||||
displayName: 'Make sure you are using a user account not an admin account',
|
||||
name: 'notice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
placeholder: 'name@email.com',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Password',
|
||||
name: 'password',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
password: true,
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'URL',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'https://api.example.com',
|
||||
},
|
||||
{
|
||||
displayName: 'API Version',
|
||||
name: 'apiVersion',
|
||||
default: 'v3',
|
||||
type: 'options',
|
||||
description: 'The version of api to be used',
|
||||
options: [
|
||||
{
|
||||
name: 'Version 4',
|
||||
value: 'v4',
|
||||
description: 'API version supported by Strapi 4',
|
||||
},
|
||||
{
|
||||
name: 'Version 3',
|
||||
value: 'v3',
|
||||
description: 'API version supported by Strapi 3',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should validate credentials', () => {
|
||||
const schema = CredentialSchema.create({
|
||||
notice: CredentialSchema.notice('Notice'),
|
||||
email: CredentialSchema.email(),
|
||||
password: CredentialSchema.password(),
|
||||
url: CredentialSchema.url(),
|
||||
apiVersion: CredentialSchema.options({
|
||||
label: 'API Version',
|
||||
options: [
|
||||
{
|
||||
label: 'Version 4',
|
||||
value: 'v4',
|
||||
},
|
||||
{
|
||||
label: 'Version 3',
|
||||
value: 'v3',
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const validData = {
|
||||
email: 'foo@x.com',
|
||||
url: 'https://google.com',
|
||||
password: 'foo',
|
||||
apiVersion: 'v3',
|
||||
};
|
||||
expect(schema.validate(validData).success).toEqual(true);
|
||||
|
||||
expect(
|
||||
schema.validate({
|
||||
...validData,
|
||||
email: 'hello world',
|
||||
}).success,
|
||||
).toEqual(false);
|
||||
|
||||
expect(
|
||||
schema.validate({
|
||||
...validData,
|
||||
url: 'hello world',
|
||||
}).success,
|
||||
).toEqual(false);
|
||||
|
||||
expect(schema.validate({}).success).toEqual(false);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue