@lucidcms/core
Version:
The core of the Lucid CMS. It's responsible for spinning up the API and serving the CMS.
1 lines • 12.4 kB
Source Map (JSON)
{"version":3,"file":"base-repository.mjs","names":["constants"],"sources":["../../../../src/libs/repositories/parents/base-repository.ts"],"sourcesContent":["import type { ColumnDataType, InsertObject, UpdateObject } from \"kysely\";\nimport z, { type ZodObject, type ZodType } from \"zod\";\nimport constants from \"../../../constants/constants.js\";\nimport type { FilterOperator } from \"../../../types/query-params.js\";\nimport type { LucidErrorData } from \"../../../types.js\";\nimport { tidyZodError } from \"../../../utils/errors/index.js\";\nimport type DatabaseAdapter from \"../../db/adapter-base.js\";\nimport type { Insert, KyselyDB, LucidDB, Update } from \"../../db/types.js\";\nimport { copy } from \"../../i18n/index.js\";\nimport logger from \"../../logger/index.js\";\nimport type {\n\tExecuteMeta,\n\tQueryResult,\n\tValidationConfigExtend,\n} from \"../types.js\";\n\nabstract class BaseRepository<\n\tTable extends keyof LucidDB,\n\tT extends LucidDB[Table] = LucidDB[Table],\n> {\n\tconstructor(\n\t\tprotected readonly db: KyselyDB,\n\t\tprotected readonly dbAdapter: DatabaseAdapter,\n\t\tpublic tableName: keyof LucidDB,\n\t) {}\n\t/**\n\t * A Zod schema for the table.\n\t */\n\n\t// biome-ignore lint/suspicious/noExplicitAny: explanation\n\tprotected abstract tableSchema: ZodObject<any>;\n\t/**\n\t * The column data types for the table. Repositories need to keep these in sync with the migrations and the database.\n\t */\n\tprotected abstract columnFormats: Partial<Record<keyof T, ColumnDataType>>;\n\t/**\n\t * The query configuration for the table. The main query builder fn uses this to map filter and sort query params to table columns, along with deciding which operators to use.\n\t */\n\tprotected abstract queryConfig?: {\n\t\ttableKeys?: {\n\t\t\tfilters?: Record<string, string>;\n\t\t\tsorts?: Record<string, string>;\n\t\t};\n\t\toperators?: Record<string, FilterOperator>;\n\t};\n\n\t/**\n\t * Formats values that need special handling (like JSON or booleans)\n\t * Leaves other values and column names unchanged\n\t */\n\tprotected formatData<Type extends \"insert\" | \"update\">(\n\t\tdata: Partial<Insert<T>> | Partial<Update<T>>,\n\t\tconfig: {\n\t\t\ttype: Type;\n\t\t\tdynamicColumns?: Record<string, ColumnDataType>;\n\t\t},\n\t): Type extends \"insert\"\n\t\t? InsertObject<LucidDB, Table>\n\t\t: UpdateObject<LucidDB, Table> {\n\t\tconst formatted: Record<string, unknown> = {};\n\t\tconst columnFormats =\n\t\t\tconfig.dynamicColumns !== undefined\n\t\t\t\t? {\n\t\t\t\t\t\t...this.columnFormats,\n\t\t\t\t\t\t...config.dynamicColumns,\n\t\t\t\t\t}\n\t\t\t\t: this.columnFormats;\n\t\tfor (const [key, value] of Object.entries(data)) {\n\t\t\tconst columnType = columnFormats[key as keyof T];\n\t\t\tformatted[key] = columnType\n\t\t\t\t? this.dbAdapter.formatInsertValue(columnType, value)\n\t\t\t\t: value;\n\t\t}\n\n\t\t// biome-ignore lint/suspicious/noExplicitAny: explanation\n\t\treturn formatted as any;\n\t}\n\t/**\n\t * Creates a validation schema based on selected columns\n\t *\n\t * - when selectAll is true, returns the full schema\n\t * - when the select array is passed, picks only those columns from the schema\n\t * - otherwise, makes all fields optional\n\t */\n\tprotected createValidationSchema<V extends boolean = false>(\n\t\tconfig: ValidationConfigExtend<V>,\n\t): ZodType {\n\t\tconst baseSchema = config.schema || this.tableSchema;\n\n\t\tlet selectSchema: ZodType;\n\t\tif (config.selectAll) {\n\t\t\tselectSchema = baseSchema;\n\t\t} else if (Array.isArray(config.select) && config.select.length > 0) {\n\t\t\tselectSchema = baseSchema.pick(\n\t\t\t\tconfig.select.reduce<Record<string, true>>((acc, key) => {\n\t\t\t\t\tacc[key as string] = true;\n\t\t\t\t\treturn acc;\n\t\t\t\t}, {}),\n\t\t\t);\n\t\t} else {\n\t\t\tselectSchema = baseSchema.partial();\n\t\t}\n\n\t\treturn this.wrapSchemaForMode(selectSchema, config.mode);\n\t}\n\t/**\n\t * Responsible for creating schemas based on the mode\n\t */\n\tprivate wrapSchemaForMode(\n\t\tschema: ZodType,\n\t\tmode: \"single\" | \"multiple\" | \"multiple-count\" | \"count\",\n\t): ZodType {\n\t\tswitch (mode) {\n\t\t\tcase \"count\": {\n\t\t\t\treturn z\n\t\t\t\t\t.object({ count: z.union([z.number(), z.string()]) })\n\t\t\t\t\t.optional();\n\t\t\t}\n\t\t\tcase \"multiple-count\":\n\t\t\t\treturn z.tuple([\n\t\t\t\t\tz.array(schema),\n\t\t\t\t\tz.object({ count: z.union([z.number(), z.string()]) }).optional(),\n\t\t\t\t]);\n\t\t\tcase \"multiple\":\n\t\t\t\treturn z.array(schema);\n\t\t\tcase \"single\":\n\t\t\t\treturn schema;\n\t\t}\n\t}\n\t/**\n\t * Merges the given schema with the tableSchema\n\t */\n\t// biome-ignore lint/suspicious/noExplicitAny: explanation\n\tprotected mergeSchema(schema?: ZodObject<any>) {\n\t\tif (!schema) return this.tableSchema;\n\t\treturn this.tableSchema.merge(schema.shape);\n\t}\n\t/**\n\t * Checks if the response data exists and successfully validates against a schema.\n\t *\n\t * Type narrows the response to not be undefined when the validation is enabled.\n\t */\n\tprotected async validateResponse<QueryData, V extends boolean = false>(\n\t\texecuteResponse: Awaited<\n\t\t\tReturnType<typeof this.executeQuery<QueryData | undefined>>\n\t\t>,\n\t\tconfig?: ValidationConfigExtend<V>,\n\t): Promise<QueryResult<QueryData, V>> {\n\t\tconst res = executeResponse.response as QueryResult<QueryData, V>;\n\n\t\tif (config?.enabled !== true) return res;\n\n\t\t//* undefined and null checks\n\t\tif (res.data === undefined || res.data === null) {\n\t\t\treturn {\n\t\t\t\terror: {\n\t\t\t\t\t...config.defaultError,\n\t\t\t\t\tstatus: config.defaultError?.status ?? 404,\n\t\t\t\t},\n\t\t\t\tdata: undefined,\n\t\t\t};\n\t\t}\n\n\t\tconst schema = this.createValidationSchema(config);\n\t\tif (!schema) return res;\n\n\t\tconst validationResult = await schema.safeParseAsync(res.data);\n\n\t\tif (!validationResult.success) {\n\t\t\tconst validationError = tidyZodError(validationResult.error);\n\t\t\tlogger.error({\n\t\t\t\tevent: \"query.response.validation.failed\",\n\t\t\t\tmessage: \"Query response validation failed\",\n\t\t\t\tscope: constants.logScopes.query,\n\t\t\t\tdata: {\n\t\t\t\t\ttable: executeResponse.meta.tableName,\n\t\t\t\t\tmethod: executeResponse.meta.method,\n\t\t\t\t\texecutionTime: executeResponse.meta.executionTime,\n\t\t\t\t\tvalidationError,\n\t\t\t\t},\n\t\t\t});\n\t\t\treturn {\n\t\t\t\tdata: undefined,\n\t\t\t\terror: {\n\t\t\t\t\t...config?.defaultError,\n\t\t\t\t\tmessage:\n\t\t\t\t\t\tconfig?.defaultError?.message ??\n\t\t\t\t\t\tcopy(\"server:core.errors.validation.name\"),\n\t\t\t\t\ttype: config?.defaultError?.type ?? \"validation\",\n\t\t\t\t\tstatus: config?.defaultError?.status ?? 400,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tdata: res.data as NonNullable<QueryData>,\n\t\t\terror: undefined,\n\t\t};\n\t}\n\t/**\n\t * Handles executing a query and logging\n\t * @todo add query data to debug log, add a sanitise data method that each repo can extend to mark certain columns to have data redacted, ie passwords, tokens, user data etc.\n\t */\n\tprotected async executeQuery<QueryData>(\n\t\texecuteFn: () => Promise<QueryData>,\n\t\tconfig: {\n\t\t\tmethod: string;\n\t\t\ttableName?: string;\n\t\t},\n\t): Promise<{\n\t\tresponse:\n\t\t\t| { error: LucidErrorData; data: undefined }\n\t\t\t| { error: undefined; data: QueryData };\n\t\tmeta: ExecuteMeta;\n\t}> {\n\t\tconst startTime = process.hrtime();\n\n\t\ttry {\n\t\t\tconst result = await executeFn();\n\n\t\t\tconst endTime = process.hrtime(startTime);\n\t\t\tconst executionTime = (endTime[0] * 1000 + endTime[1] / 1000000).toFixed(\n\t\t\t\t2,\n\t\t\t);\n\n\t\t\tlogger.debug({\n\t\t\t\tevent: \"query.execution.completed\",\n\t\t\t\tmessage: \"Query execution completed\",\n\t\t\t\tscope: constants.logScopes.query,\n\t\t\t\tdata: {\n\t\t\t\t\ttable: config?.tableName ?? this.tableName,\n\t\t\t\t\tmethod: config.method,\n\t\t\t\t\texecutionTime: `${executionTime}ms`,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\treturn {\n\t\t\t\tresponse: {\n\t\t\t\t\tdata: result,\n\t\t\t\t\terror: undefined,\n\t\t\t\t},\n\t\t\t\tmeta: {\n\t\t\t\t\tmethod: config.method,\n\t\t\t\t\texecutionTime: `${executionTime}ms`,\n\t\t\t\t\ttableName: config?.tableName ?? this.tableName,\n\t\t\t\t},\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tconst endTime = process.hrtime(startTime);\n\t\t\tconst executionTime = (endTime[0] * 1000 + endTime[1] / 1000000).toFixed(\n\t\t\t\t2,\n\t\t\t);\n\n\t\t\tlogger.error({\n\t\t\t\terror,\n\t\t\t\tevent: \"query.execution.failed\",\n\t\t\t\tmessage: \"Query execution failed\",\n\t\t\t\tscope: constants.logScopes.query,\n\t\t\t\tdata: {\n\t\t\t\t\ttable: config?.tableName ?? this.tableName,\n\t\t\t\t\tmethod: config.method,\n\t\t\t\t\texecutionTime: `${executionTime}ms`,\n\t\t\t\t\terrorMessage:\n\t\t\t\t\t\terror instanceof Error\n\t\t\t\t\t\t\t? error.message\n\t\t\t\t\t\t\t: \"An unknown error occurred\",\n\t\t\t\t},\n\t\t\t});\n\n\t\t\treturn {\n\t\t\t\tresponse: {\n\t\t\t\t\tdata: undefined,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tmessage: copy(\n\t\t\t\t\t\t\t\"server:core.errors.unknown\",\n\t\t\t\t\t\t\terror instanceof Error\n\t\t\t\t\t\t\t\t? { defaultMessage: error.message }\n\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tstatus: 500,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tmeta: {\n\t\t\t\t\tmethod: config.method,\n\t\t\t\t\texecutionTime: `${executionTime}ms`,\n\t\t\t\t\ttableName: config?.tableName ?? this.tableName,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t}\n}\n\nexport default BaseRepository;\n"],"mappings":"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"}