mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
refactor: integrate zod in schemas (#90)
This commit is contained in:
parent
27ec6fcb00
commit
4973053fda
17 changed files with 208 additions and 108 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -14,11 +14,7 @@ node_modules
|
||||||
|
|
||||||
# logs
|
# logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log*
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
.pnpm-debug.log*
|
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
cache
|
cache
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
"oidc-provider": "^7.4.1",
|
"oidc-provider": "^7.4.1",
|
||||||
"slonik": "^23.8.3",
|
"slonik": "^23.8.3",
|
||||||
"slonik-interceptor-preset": "^1.2.10",
|
"slonik-interceptor-preset": "^1.2.10",
|
||||||
"zod": "^3.2.0"
|
"zod": "^3.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@logto/eslint-config": "^0.1.0-rc.18",
|
"@logto/eslint-config": "^0.1.0-rc.18",
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { DatabasePoolType, IdentifierSqlTokenType, sql } from 'slonik';
|
||||||
import {
|
import {
|
||||||
conditionalSql,
|
conditionalSql,
|
||||||
convertToIdentifiers,
|
convertToIdentifiers,
|
||||||
convertToPrimitive,
|
convertToPrimitiveOrSql,
|
||||||
excludeAutoSetFields,
|
excludeAutoSetFields,
|
||||||
OmitAutoSetFields,
|
OmitAutoSetFields,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
@ -61,7 +61,7 @@ export const buildInsertInto: BuildInsertInto = <Schema extends SchemaLike<strin
|
||||||
sql`, `
|
sql`, `
|
||||||
)})
|
)})
|
||||||
values (${sql.join(
|
values (${sql.join(
|
||||||
keys.map((key) => convertToPrimitive(data[key] ?? null)),
|
keys.map((key) => convertToPrimitiveOrSql(key, data[key] ?? null)),
|
||||||
sql`, `
|
sql`, `
|
||||||
)})
|
)})
|
||||||
${conditionalSql(returning, () => sql`returning *`)}
|
${conditionalSql(returning, () => sql`returning *`)}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Falsy, notFalsy } from '@logto/essentials';
|
import { Falsy, notFalsy } from '@logto/essentials';
|
||||||
import { SchemaValuePrimitive, SchemaValue } from '@logto/schemas';
|
import { SchemaValuePrimitive, SchemaValue } from '@logto/schemas';
|
||||||
import { sql, SqlSqlTokenType } from 'slonik';
|
import dayjs from 'dayjs';
|
||||||
|
import { sql, SqlSqlTokenType, SqlTokenType } from 'slonik';
|
||||||
import { FieldIdentifiers, Table } from './types';
|
import { FieldIdentifiers, Table } from './types';
|
||||||
|
|
||||||
export const conditionalSql = <T>(
|
export const conditionalSql = <T>(
|
||||||
|
@ -25,12 +26,14 @@ export const excludeAutoSetFields = <T extends string>(fields: readonly T[]) =>
|
||||||
* Note `undefined` is removed from the acceptable list,
|
* Note `undefined` is removed from the acceptable list,
|
||||||
* since you should NOT call this function if ignoring the field is the desired behavior.
|
* since you should NOT call this function if ignoring the field is the desired behavior.
|
||||||
* Calling this function with `null` means an explicit `null` setting in database is expected.
|
* Calling this function with `null` means an explicit `null` setting in database is expected.
|
||||||
|
* @param key The key of value. Will treat as `timestamp` if it ends with `_at` or 'At' AND value is a number;
|
||||||
* @param value The value to convert.
|
* @param value The value to convert.
|
||||||
* @returns A primitive that can be saved into database.
|
* @returns A primitive that can be saved into database.
|
||||||
*/
|
*/
|
||||||
export const convertToPrimitive = (
|
export const convertToPrimitiveOrSql = (
|
||||||
|
key: string,
|
||||||
value: NonNullable<SchemaValue> | null
|
value: NonNullable<SchemaValue> | null
|
||||||
): NonNullable<SchemaValuePrimitive> | null => {
|
): NonNullable<SchemaValuePrimitive> | SqlTokenType | null => {
|
||||||
if (value === null) {
|
if (value === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -39,6 +42,10 @@ export const convertToPrimitive = (
|
||||||
return JSON.stringify(value);
|
return JSON.stringify(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (['_at', 'At'].some((value) => key.endsWith(value)) && typeof value === 'number') {
|
||||||
|
return sql`to_timestamp(${value / 1000})`;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
@ -60,3 +67,5 @@ export const convertToIdentifiers = <T extends Table>(
|
||||||
{} as FieldIdentifiers<keyof T['fields']>
|
{} as FieldIdentifiers<keyof T['fields']>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const convertToTimestamp = (time = dayjs()) => sql`to_timestamp(${time.valueOf() / 1000})`;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { OidcClientMetadata } from '@logto/schemas';
|
import { OidcClientMetadata } from '@logto/schemas';
|
||||||
|
|
||||||
export const generateOidcClientMetadata = (): OidcClientMetadata => ({
|
export const generateOidcClientMetadata = (): OidcClientMetadata => ({
|
||||||
redirect_uris: [],
|
redirectUris: [],
|
||||||
post_logout_redirect_uris: [],
|
postLogoutRedirectUris: [],
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import { buildInsertInto } from '@/database/insert';
|
import { buildInsertInto } from '@/database/insert';
|
||||||
import pool from '@/database/pool';
|
import pool from '@/database/pool';
|
||||||
import { convertToIdentifiers } from '@/database/utils';
|
import { convertToIdentifiers, convertToTimestamp } from '@/database/utils';
|
||||||
import { conditional } from '@logto/essentials';
|
import { conditional } from '@logto/essentials';
|
||||||
import {
|
import {
|
||||||
OidcModelInstanceDBEntry,
|
OidcModelInstanceDBEntry,
|
||||||
OidcModelInstancePayload,
|
OidcModelInstancePayload,
|
||||||
OidcModelInstances,
|
OidcModelInstances,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { sql, ValueExpressionType } from 'slonik';
|
import { sql, ValueExpressionType } from 'slonik';
|
||||||
|
|
||||||
export type WithConsumed<T> = T & { consumed?: boolean };
|
export type WithConsumed<T> = T & { consumed?: boolean };
|
||||||
|
@ -64,7 +63,7 @@ export const findPayloadByPayloadField = async <
|
||||||
export const consumeInstanceById = async (modelName: string, id: string) => {
|
export const consumeInstanceById = async (modelName: string, id: string) => {
|
||||||
await pool.query(sql`
|
await pool.query(sql`
|
||||||
update ${table}
|
update ${table}
|
||||||
set ${fields.consumedAt}=${dayjs().valueOf()}
|
set ${fields.consumedAt}=${convertToTimestamp()}
|
||||||
where ${fields.modelName}=${modelName}
|
where ${fields.modelName}=${modelName}
|
||||||
and ${fields.id}=${id}
|
and ${fields.id}=${id}
|
||||||
`);
|
`);
|
||||||
|
|
|
@ -32,7 +32,8 @@
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.3.2",
|
||||||
"ts-node": "^10.0.0",
|
"ts-node": "^10.0.0",
|
||||||
"typescript": "^4.3.5"
|
"typescript": "^4.3.5",
|
||||||
|
"zod": "^3.8.1"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "@logto"
|
"extends": "@logto"
|
||||||
|
@ -40,5 +41,8 @@
|
||||||
"prettier": "@logto/eslint-config/.prettierrc",
|
"prettier": "@logto/eslint-config/.prettierrc",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@logto/phrases": "^0.1.0"
|
"@logto/phrases": "^0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.8.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
|
||||||
import { OidcClientMetadata, GeneratedSchema } from '../foundations';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import {
|
||||||
|
OidcClientMetadata,
|
||||||
|
oidcClientMetadataGuard,
|
||||||
|
GeneratedSchema,
|
||||||
|
Guard,
|
||||||
|
} from '../foundations';
|
||||||
|
|
||||||
import { ApplicationType } from './custom-types';
|
import { ApplicationType } from './custom-types';
|
||||||
|
|
||||||
|
@ -12,6 +19,14 @@ export type ApplicationDBEntry = {
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const guard: Guard<ApplicationDBEntry> = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
type: z.nativeEnum(ApplicationType),
|
||||||
|
oidcClientMetadata: oidcClientMetadataGuard,
|
||||||
|
createdAt: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
export const Applications: GeneratedSchema<ApplicationDBEntry> = Object.freeze({
|
export const Applications: GeneratedSchema<ApplicationDBEntry> = Object.freeze({
|
||||||
table: 'applications',
|
table: 'applications',
|
||||||
tableSingular: 'application',
|
tableSingular: 'application',
|
||||||
|
@ -23,4 +38,5 @@ export const Applications: GeneratedSchema<ApplicationDBEntry> = Object.freeze({
|
||||||
createdAt: 'created_at',
|
createdAt: 'created_at',
|
||||||
},
|
},
|
||||||
fieldKeys: ['id', 'name', 'type', 'oidcClientMetadata', 'createdAt'],
|
fieldKeys: ['id', 'name', 'type', 'oidcClientMetadata', 'createdAt'],
|
||||||
|
guard,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
|
||||||
import { OidcModelInstancePayload, GeneratedSchema } from '../foundations';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import {
|
||||||
|
OidcModelInstancePayload,
|
||||||
|
oidcModelInstancePayloadGuard,
|
||||||
|
GeneratedSchema,
|
||||||
|
Guard,
|
||||||
|
} from '../foundations';
|
||||||
|
|
||||||
export type OidcModelInstanceDBEntry = {
|
export type OidcModelInstanceDBEntry = {
|
||||||
modelName: string;
|
modelName: string;
|
||||||
|
@ -10,6 +17,14 @@ export type OidcModelInstanceDBEntry = {
|
||||||
consumedAt?: number;
|
consumedAt?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const guard: Guard<OidcModelInstanceDBEntry> = z.object({
|
||||||
|
modelName: z.string(),
|
||||||
|
id: z.string(),
|
||||||
|
payload: oidcModelInstancePayloadGuard,
|
||||||
|
expiresAt: z.number(),
|
||||||
|
consumedAt: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const OidcModelInstances: GeneratedSchema<OidcModelInstanceDBEntry> = Object.freeze({
|
export const OidcModelInstances: GeneratedSchema<OidcModelInstanceDBEntry> = Object.freeze({
|
||||||
table: 'oidc_model_instances',
|
table: 'oidc_model_instances',
|
||||||
tableSingular: 'oidc_model_instance',
|
tableSingular: 'oidc_model_instance',
|
||||||
|
@ -21,4 +36,5 @@ export const OidcModelInstances: GeneratedSchema<OidcModelInstanceDBEntry> = Obj
|
||||||
consumedAt: 'consumed_at',
|
consumedAt: 'consumed_at',
|
||||||
},
|
},
|
||||||
fieldKeys: ['modelName', 'id', 'payload', 'expiresAt', 'consumedAt'],
|
fieldKeys: ['modelName', 'id', 'payload', 'expiresAt', 'consumedAt'],
|
||||||
|
guard,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
|
||||||
import { GeneratedSchema } from '../foundations';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { GeneratedSchema, Guard } from '../foundations';
|
||||||
|
|
||||||
import { PasswordEncryptionMethod } from './custom-types';
|
import { PasswordEncryptionMethod } from './custom-types';
|
||||||
|
|
||||||
|
@ -14,6 +16,16 @@ export type UserDBEntry = {
|
||||||
passwordEncryptionSalt?: string;
|
passwordEncryptionSalt?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const guard: Guard<UserDBEntry> = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
username: z.string().optional(),
|
||||||
|
primaryEmail: z.string().optional(),
|
||||||
|
primaryPhone: z.string().optional(),
|
||||||
|
passwordEncrypted: z.string().optional(),
|
||||||
|
passwordEncryptionMethod: z.nativeEnum(PasswordEncryptionMethod).optional(),
|
||||||
|
passwordEncryptionSalt: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const Users: GeneratedSchema<UserDBEntry> = Object.freeze({
|
export const Users: GeneratedSchema<UserDBEntry> = Object.freeze({
|
||||||
table: 'users',
|
table: 'users',
|
||||||
tableSingular: 'user',
|
tableSingular: 'user',
|
||||||
|
@ -35,4 +47,5 @@ export const Users: GeneratedSchema<UserDBEntry> = Object.freeze({
|
||||||
'passwordEncryptionMethod',
|
'passwordEncryptionMethod',
|
||||||
'passwordEncryptionSalt',
|
'passwordEncryptionSalt',
|
||||||
],
|
],
|
||||||
|
guard,
|
||||||
});
|
});
|
||||||
|
|
2
packages/schemas/src/foundations/index.ts
Normal file
2
packages/schemas/src/foundations/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './schemas';
|
||||||
|
export * from './jsonb-types';
|
22
packages/schemas/src/foundations/jsonb-types.ts
Normal file
22
packages/schemas/src/foundations/jsonb-types.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const oidcModelInstancePayloadGuard = z
|
||||||
|
.object({
|
||||||
|
userCode: z.string().optional(),
|
||||||
|
uid: z.string().optional(),
|
||||||
|
grantId: z.string().optional(),
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Try to use `.passthrough()` if type has been fixed.
|
||||||
|
* https://github.com/colinhacks/zod/issues/452
|
||||||
|
*/
|
||||||
|
.catchall(z.unknown());
|
||||||
|
|
||||||
|
export type OidcModelInstancePayload = z.infer<typeof oidcModelInstancePayloadGuard>;
|
||||||
|
|
||||||
|
export const oidcClientMetadataGuard = z.object({
|
||||||
|
redirectUris: z.string().array(),
|
||||||
|
postLogoutRedirectUris: z.string().array(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type OidcClientMetadata = z.infer<typeof oidcClientMetadataGuard>;
|
|
@ -1,3 +1,11 @@
|
||||||
|
import { ZodObject, ZodType } from 'zod';
|
||||||
|
|
||||||
|
export type Guard<T extends Record<string, unknown>> = ZodObject<
|
||||||
|
{
|
||||||
|
[key in keyof T]: ZodType<T[key]>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
export type SchemaValuePrimitive = string | number | boolean | undefined;
|
export type SchemaValuePrimitive = string | number | boolean | undefined;
|
||||||
export type SchemaValue = SchemaValuePrimitive | Record<string, unknown>;
|
export type SchemaValue = SchemaValuePrimitive | Record<string, unknown>;
|
||||||
export type SchemaLike<Key extends string> = {
|
export type SchemaLike<Key extends string> = {
|
||||||
|
@ -12,16 +20,6 @@ export type GeneratedSchema<Schema extends SchemaLike<string>> = keyof Schema ex
|
||||||
[key in keyof Schema]: string;
|
[key in keyof Schema]: string;
|
||||||
};
|
};
|
||||||
fieldKeys: ReadonlyArray<keyof Schema>;
|
fieldKeys: ReadonlyArray<keyof Schema>;
|
||||||
|
guard: Guard<Schema>;
|
||||||
}>
|
}>
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
export type OidcModelInstancePayload = Record<string, unknown> & {
|
|
||||||
userCode?: string;
|
|
||||||
uid?: string;
|
|
||||||
grantId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OidcClientMetadata = {
|
|
||||||
redirect_uris: string[];
|
|
||||||
post_logout_redirect_uris: string[];
|
|
||||||
};
|
|
|
@ -7,43 +7,8 @@ import uniq from 'lodash.uniq';
|
||||||
import { conditional, conditionalString } from '@logto/essentials';
|
import { conditional, conditionalString } from '@logto/essentials';
|
||||||
|
|
||||||
import { findFirstParentheses, getType, normalizeWhitespaces, removeParentheses } from './utils';
|
import { findFirstParentheses, getType, normalizeWhitespaces, removeParentheses } from './utils';
|
||||||
|
import { FileData, Table, Field, Type, GeneratedType, TableWithType } from './types';
|
||||||
type Field = {
|
import { generateSchema } from './schema';
|
||||||
name: string;
|
|
||||||
type?: string;
|
|
||||||
customType?: string;
|
|
||||||
tsType?: string;
|
|
||||||
required: boolean;
|
|
||||||
isArray: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
type FieldWithType = Omit<Field, 'type' | 'customType'> & { type: string };
|
|
||||||
|
|
||||||
type Type = {
|
|
||||||
name: string;
|
|
||||||
type: 'enum';
|
|
||||||
values: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type GeneratedType = Type & {
|
|
||||||
tsName: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Table = {
|
|
||||||
name: string;
|
|
||||||
fields: Field[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type TableWithType = {
|
|
||||||
name: string;
|
|
||||||
fields: FieldWithType[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type FileData = {
|
|
||||||
types: Type[];
|
|
||||||
tables: Table[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const directory = 'tables';
|
const directory = 'tables';
|
||||||
|
|
||||||
|
@ -175,19 +140,23 @@ const generate = async () => {
|
||||||
assert(finalType, `Type ${customType ?? 'N/A'} not found`);
|
assert(finalType, `Type ${customType ?? 'N/A'} not found`);
|
||||||
|
|
||||||
if (tsType) {
|
if (tsType) {
|
||||||
tsTypes.push(tsType);
|
tsTypes.push(tsType, `${camelcase(tsType)}Guard`);
|
||||||
} else if (type === undefined) {
|
} else if (!type) {
|
||||||
customTypes.push(finalType);
|
customTypes.push(finalType);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...rest, type: finalType };
|
return { ...rest, tsType, type: finalType, isEnum: !tsType && !type };
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (tableWithTypes.length > 0) {
|
if (tableWithTypes.length > 0) {
|
||||||
tsTypes.push('GeneratedSchema');
|
tsTypes.push('GeneratedSchema', 'Guard');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const importZod = conditionalString(
|
||||||
|
tableWithTypes.length > 0 && "import { z } from 'zod';\n\n"
|
||||||
|
);
|
||||||
|
|
||||||
const importTsTypes = conditionalString(
|
const importTsTypes = conditionalString(
|
||||||
tsTypes.length > 0 &&
|
tsTypes.length > 0 &&
|
||||||
[
|
[
|
||||||
|
@ -212,39 +181,10 @@ const generate = async () => {
|
||||||
|
|
||||||
const content =
|
const content =
|
||||||
header +
|
header +
|
||||||
|
importZod +
|
||||||
importTsTypes +
|
importTsTypes +
|
||||||
importTypes +
|
importTypes +
|
||||||
tableWithTypes
|
tableWithTypes.map((table) => generateSchema(table)).join('\n') +
|
||||||
.map(({ name, fields }) => {
|
|
||||||
const databaseEntryType = `${pluralize(
|
|
||||||
camelcase(name, { pascalCase: true }),
|
|
||||||
1
|
|
||||||
)}DBEntry`;
|
|
||||||
return [
|
|
||||||
`export type ${databaseEntryType} = {`,
|
|
||||||
...fields.map(
|
|
||||||
({ name, type, isArray, required }) =>
|
|
||||||
` ${camelcase(name)}${conditionalString(
|
|
||||||
!required && '?'
|
|
||||||
)}: ${type}${conditionalString(isArray && '[]')};`
|
|
||||||
),
|
|
||||||
'};',
|
|
||||||
'',
|
|
||||||
`export const ${camelcase(name, {
|
|
||||||
pascalCase: true,
|
|
||||||
})}: GeneratedSchema<${databaseEntryType}> = Object.freeze({`,
|
|
||||||
` table: '${name}',`,
|
|
||||||
` tableSingular: '${pluralize(name, 1)}',`,
|
|
||||||
' fields: {',
|
|
||||||
...fields.map(({ name }) => ` ${camelcase(name)}: '${name}',`),
|
|
||||||
' },',
|
|
||||||
' fieldKeys: [',
|
|
||||||
...fields.map(({ name }) => ` '${camelcase(name)}',`),
|
|
||||||
' ],',
|
|
||||||
'});',
|
|
||||||
].join('\n');
|
|
||||||
})
|
|
||||||
.join('\n') +
|
|
||||||
'\n';
|
'\n';
|
||||||
await fs.writeFile(path.join(generatedDirectory, getOutputFileName(file) + '.ts'), content);
|
await fs.writeFile(path.join(generatedDirectory, getOutputFileName(file) + '.ts'), content);
|
||||||
})
|
})
|
||||||
|
|
48
packages/schemas/src/gen/schema.ts
Normal file
48
packages/schemas/src/gen/schema.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import pluralize from 'pluralize';
|
||||||
|
import camelcase from 'camelcase';
|
||||||
|
import { conditionalString } from '@logto/essentials';
|
||||||
|
import { TableWithType } from './types';
|
||||||
|
|
||||||
|
export const generateSchema = ({ name, fields }: TableWithType) => {
|
||||||
|
const databaseEntryType = `${pluralize(camelcase(name, { pascalCase: true }), 1)}DBEntry`;
|
||||||
|
return [
|
||||||
|
`export type ${databaseEntryType} = {`,
|
||||||
|
...fields.map(
|
||||||
|
({ name, type, isArray, required }) =>
|
||||||
|
` ${camelcase(name)}${conditionalString(!required && '?')}: ${type}${conditionalString(
|
||||||
|
isArray && '[]'
|
||||||
|
)};`
|
||||||
|
),
|
||||||
|
'};',
|
||||||
|
'',
|
||||||
|
`const guard: Guard<${databaseEntryType}> = z.object({`,
|
||||||
|
...fields.map(({ name, type, isArray, isEnum, required, tsType }) => {
|
||||||
|
if (tsType) {
|
||||||
|
return ` ${camelcase(name)}: ${camelcase(tsType)}Guard${conditionalString(
|
||||||
|
!required && '.optional()'
|
||||||
|
)},`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ` ${camelcase(name)}: z.${
|
||||||
|
isEnum ? `nativeEnum(${type})` : `${type}()`
|
||||||
|
}${conditionalString(isArray && '.array()')}${conditionalString(
|
||||||
|
!required && '.optional()'
|
||||||
|
)},`;
|
||||||
|
}),
|
||||||
|
' });',
|
||||||
|
'',
|
||||||
|
`export const ${camelcase(name, {
|
||||||
|
pascalCase: true,
|
||||||
|
})}: GeneratedSchema<${databaseEntryType}> = Object.freeze({`,
|
||||||
|
` table: '${name}',`,
|
||||||
|
` tableSingular: '${pluralize(name, 1)}',`,
|
||||||
|
' fields: {',
|
||||||
|
...fields.map(({ name }) => ` ${camelcase(name)}: '${name}',`),
|
||||||
|
' },',
|
||||||
|
' fieldKeys: [',
|
||||||
|
...fields.map(({ name }) => ` '${camelcase(name)}',`),
|
||||||
|
' ],',
|
||||||
|
' guard,',
|
||||||
|
'});',
|
||||||
|
].join('\n');
|
||||||
|
};
|
36
packages/schemas/src/gen/types.ts
Normal file
36
packages/schemas/src/gen/types.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
export type Field = {
|
||||||
|
name: string;
|
||||||
|
type?: string;
|
||||||
|
customType?: string;
|
||||||
|
tsType?: string;
|
||||||
|
required: boolean;
|
||||||
|
isArray: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
export type FieldWithType = Omit<Field, 'type' | 'customType'> & { type: string; isEnum: boolean };
|
||||||
|
|
||||||
|
export type Type = {
|
||||||
|
name: string;
|
||||||
|
type: 'enum';
|
||||||
|
values: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GeneratedType = Type & {
|
||||||
|
tsName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Table = {
|
||||||
|
name: string;
|
||||||
|
fields: Field[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TableWithType = {
|
||||||
|
name: string;
|
||||||
|
fields: FieldWithType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FileData = {
|
||||||
|
types: Type[];
|
||||||
|
tables: Table[];
|
||||||
|
};
|
|
@ -62,7 +62,7 @@ importers:
|
||||||
ts-jest: ^27.0.5
|
ts-jest: ^27.0.5
|
||||||
tsc-watch: ^4.4.0
|
tsc-watch: ^4.4.0
|
||||||
typescript: ^4.3.5
|
typescript: ^4.3.5
|
||||||
zod: ^3.2.0
|
zod: ^3.8.1
|
||||||
dependencies:
|
dependencies:
|
||||||
'@logto/essentials': 1.1.0-rc.2
|
'@logto/essentials': 1.1.0-rc.2
|
||||||
'@logto/phrases': link:../phrases
|
'@logto/phrases': link:../phrases
|
||||||
|
@ -87,7 +87,7 @@ importers:
|
||||||
oidc-provider: 7.5.4
|
oidc-provider: 7.5.4
|
||||||
slonik: 23.8.5
|
slonik: 23.8.5
|
||||||
slonik-interceptor-preset: 1.2.10
|
slonik-interceptor-preset: 1.2.10
|
||||||
zod: 3.5.1
|
zod: 3.8.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@logto/eslint-config': 0.1.0-rc.18_2055f56ab8dafa07df5c7ad406c8a4ab
|
'@logto/eslint-config': 0.1.0-rc.18_2055f56ab8dafa07df5c7ad406c8a4ab
|
||||||
'@logto/ts-config': 0.1.0-rc.18_54a571252e94826a463f89cda755e2f0
|
'@logto/ts-config': 0.1.0-rc.18_54a571252e94826a463f89cda755e2f0
|
||||||
|
@ -139,6 +139,7 @@ importers:
|
||||||
prettier: ^2.3.2
|
prettier: ^2.3.2
|
||||||
ts-node: ^10.0.0
|
ts-node: ^10.0.0
|
||||||
typescript: ^4.3.5
|
typescript: ^4.3.5
|
||||||
|
zod: ^3.8.1
|
||||||
dependencies:
|
dependencies:
|
||||||
'@logto/phrases': link:../phrases
|
'@logto/phrases': link:../phrases
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
@ -155,6 +156,7 @@ importers:
|
||||||
prettier: 2.3.2
|
prettier: 2.3.2
|
||||||
ts-node: 10.1.0_13403c2f2d9ddab699dd2f492f123cbf
|
ts-node: 10.1.0_13403c2f2d9ddab699dd2f492f123cbf
|
||||||
typescript: 4.3.5
|
typescript: 4.3.5
|
||||||
|
zod: 3.8.1
|
||||||
|
|
||||||
packages/ui:
|
packages/ui:
|
||||||
specifiers:
|
specifiers:
|
||||||
|
@ -16664,9 +16666,8 @@ packages:
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/zod/3.5.1:
|
/zod/3.8.1:
|
||||||
resolution: {integrity: sha512-Gg9GTai0iDHowuYM9VNhdFMmesgt44ufzqaE5CPHshpuK5fCzbibdqCnrWuYH6ZmOn/N+BlGmwZtVSijhKmhKw==}
|
resolution: {integrity: sha512-u4Uodl7dLh8nXZwqXL1SM5FAl5b4lXYHOxMUVb9lqhlEAZhA2znX+0oW480m0emGFMxpoRHzUncAqRkc4h8ZJA==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/zwitch/1.0.5:
|
/zwitch/1.0.5:
|
||||||
resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==}
|
resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==}
|
||||||
|
|
Loading…
Reference in a new issue