import * as yup from 'yup'; import { yupObject, yupString } from './schema-fields'; import { filterUndefined } from './utils/objects'; import { NullishCoalesce } from './utils/types'; export type AccessType = "client" | "server" | "admin"; export type CrudOperation = "create" | "read" | "update" | "delete"; export type CrudlOperation = "create" | "read" | "update" | "delete" | "list"; export type AccessTypeXCrudOperation = `${AccessType}${Capitalize}`; export type AccessTypeXCrudlOperation = `${AccessType}${Capitalize}`; declare module 'yup' { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions export interface CustomSchemaMetadata { openapiField?: { description?: string, exampleValue?: any, hidden?: boolean, onlyShowInOperations?: Capitalize[], }, } } type ShownEndpointDocumentation = { summary: string, description: string, tags?: string[], crudOperation?: Capitalize, }; export type EndpointDocumentation = | ({ hidden: true } & Partial) | ({ hidden?: boolean } & ShownEndpointDocumentation); type InnerCrudSchema< CreateSchema extends yup.AnySchema | undefined = yup.AnySchema | undefined, ReadSchema extends yup.AnySchema | undefined = yup.AnySchema | undefined, UpdateSchema extends yup.AnySchema | undefined = yup.AnySchema | undefined, DeleteSchema extends yup.AnySchema | undefined = yup.AnySchema | undefined, > = { createSchema: CreateSchema, createDocs: EndpointDocumentation | undefined, readSchema: ReadSchema, readDocs: EndpointDocumentation | undefined, listDocs: EndpointDocumentation | undefined, updateSchema: UpdateSchema, updateDocs: EndpointDocumentation | undefined, deleteSchema: DeleteSchema, deleteDocs: EndpointDocumentation | undefined, }; export type CrudSchema< ClientSchema extends InnerCrudSchema = InnerCrudSchema, ServerSchema extends InnerCrudSchema = InnerCrudSchema, AdminSchema extends InnerCrudSchema = InnerCrudSchema, > = { client: ClientSchema, server: ServerSchema, admin: AdminSchema, hasCreate: boolean, hasRead: boolean, hasUpdate: boolean, hasDelete: boolean, }; export type CrudSchemaCreationOptions = { [K in AccessTypeXCrudOperation as `${K}Schema`]?: yup.AnySchema }; type FillInOptionalsPrepareStep = & { [K in keyof Required]: K extends keyof O ? O[K] : undefined }; type FillInOptionalsStep> = { clientCreateSchema: NullishCoalesce, clientReadSchema: NullishCoalesce, clientUpdateSchema: NullishCoalesce, clientDeleteSchema: NullishCoalesce, serverCreateSchema: NullishCoalesce, serverReadSchema: NullishCoalesce, serverUpdateSchema: NullishCoalesce, serverDeleteSchema: NullishCoalesce, adminCreateSchema: NullishCoalesce, adminReadSchema: NullishCoalesce, adminUpdateSchema: NullishCoalesce, adminDeleteSchema: NullishCoalesce, }; type FillInOptionals = FillInOptionalsStep>>>; type CrudSchemaFromOptionsInner> = CrudSchema< InnerCrudSchema, InnerCrudSchema, InnerCrudSchema >; export type CrudSchemaFromOptions = CrudSchemaFromOptionsInner>; type InnerCrudTypeOf = & (S['createSchema'] extends {} ? { Create: yup.InferType } : {}) & (S['readSchema'] extends {} ? { Read: yup.InferType } : {}) & (S['updateSchema'] extends {} ? { Update: yup.InferType } : {}) & (S['deleteSchema'] extends {} ? { Delete: yup.InferType } : {}) & (S['readSchema'] extends {} ? { List: { items: yup.InferType[], is_paginated: boolean, pagination?: { next_cursor: string | null, }, }, } : {}); export type CrudTypeOf = { Client: InnerCrudTypeOf, Server: InnerCrudTypeOf, Admin: InnerCrudTypeOf, } type CrudDocsCreationOptions = { [X in AccessTypeXCrudlOperation]?: EndpointDocumentation }; export function createCrud(options: SO & { docs?: CrudDocsCreationOptions }): CrudSchemaFromOptions { const docs = options.docs ?? {}; const client = { createSchema: options.clientCreateSchema, createDocs: docs.clientCreate, readSchema: options.clientReadSchema, readDocs: docs.clientRead, listDocs: docs.clientList, updateSchema: options.clientUpdateSchema, updateDocs: docs.clientUpdate, deleteSchema: options.clientDeleteSchema, deleteDocs: docs.clientDelete, }; const serverOverrides = filterUndefined({ createSchema: options.serverCreateSchema, createDocs: docs.serverCreate, readSchema: options.serverReadSchema, readDocs: docs.serverRead, listDocs: docs.serverList, updateSchema: options.serverUpdateSchema, updateDocs: docs.serverUpdate, deleteSchema: options.serverDeleteSchema, deleteDocs: docs.serverDelete, }); const server = { ...client, ...serverOverrides, }; const adminOverrides = filterUndefined({ createSchema: options.adminCreateSchema, createDocs: docs.adminCreate, readSchema: options.adminReadSchema, readDocs: docs.adminRead, listDocs: docs.adminList, updateSchema: options.adminUpdateSchema, updateDocs: docs.adminUpdate, deleteSchema: options.adminDeleteSchema, deleteDocs: docs.adminDelete, }); const admin = { ...server, ...adminOverrides, }; return { client: client as any, server: server as any, admin: admin as any, hasCreate: !!admin.createSchema, hasRead: !!admin.readSchema, hasUpdate: !!admin.updateSchema, hasDelete: !!admin.deleteSchema, }; } import.meta.vitest?.test("createCrud", ({ expect }) => { // Test with empty options const emptyCrud = createCrud({}); expect(emptyCrud.hasCreate).toBe(false); expect(emptyCrud.hasRead).toBe(false); expect(emptyCrud.hasUpdate).toBe(false); expect(emptyCrud.hasDelete).toBe(false); expect(emptyCrud.client.createSchema).toBeUndefined(); expect(emptyCrud.server.createSchema).toBeUndefined(); expect(emptyCrud.admin.createSchema).toBeUndefined(); // Test with client schemas only const mockSchema = yupObject().shape({ name: yupString().defined(), }); const clientOnlyCrud = createCrud({ clientCreateSchema: mockSchema, clientReadSchema: mockSchema, }); expect(clientOnlyCrud.hasCreate).toBe(true); expect(clientOnlyCrud.hasRead).toBe(true); expect(clientOnlyCrud.hasUpdate).toBe(false); expect(clientOnlyCrud.hasDelete).toBe(false); expect(clientOnlyCrud.client.createSchema).toBe(mockSchema); expect(clientOnlyCrud.server.createSchema).toBe(mockSchema); expect(clientOnlyCrud.admin.createSchema).toBe(mockSchema); // Test with server overrides const serverSchema = yupObject().shape({ name: yupString().defined(), internalField: yupString().defined(), }); const serverOverrideCrud = createCrud({ clientCreateSchema: mockSchema, serverCreateSchema: serverSchema, }); expect(serverOverrideCrud.hasCreate).toBe(true); expect(serverOverrideCrud.client.createSchema).toBe(mockSchema); expect(serverOverrideCrud.server.createSchema).toBe(serverSchema); expect(serverOverrideCrud.admin.createSchema).toBe(serverSchema); // Test with admin overrides const adminSchema = yupObject().shape({ name: yupString().defined(), internalField: yupString().defined(), adminField: yupString().defined(), }); const fullOverrideCrud = createCrud({ clientCreateSchema: mockSchema, serverCreateSchema: serverSchema, adminCreateSchema: adminSchema, }); expect(fullOverrideCrud.hasCreate).toBe(true); expect(fullOverrideCrud.client.createSchema).toBe(mockSchema); expect(fullOverrideCrud.server.createSchema).toBe(serverSchema); expect(fullOverrideCrud.admin.createSchema).toBe(adminSchema); // Test with documentation const crudWithDocs = createCrud({ clientCreateSchema: mockSchema, docs: { clientCreate: { summary: "Create a resource", description: "Creates a new resource", tags: ["resources"], }, }, }); expect(crudWithDocs.client.createDocs).toEqual({ summary: "Create a resource", description: "Creates a new resource", tags: ["resources"], }); expect(crudWithDocs.server.createDocs).toEqual({ summary: "Create a resource", description: "Creates a new resource", tags: ["resources"], }); });