UNPKG

@inaiat/fastify-papr

Version:
1 lines 14.4 kB
{"version":3,"sources":["../src/index.ts","../src/fastify-papr-plugin.ts","../src/papr-helper.ts","../src/mongo-validation-error.ts"],"sourcesContent":["/**\n * @module fastify-papr\n * A Fastify plugin for Papr (MongoDB schema validation)\n */\n\n// Export main plugin functions\nexport { asCollection, fastifyPaprPlugin } from './fastify-papr-plugin.js'\n\n// Export validation error types and handling\nexport type {\n DocumentValidationError,\n ValidationDetail,\n ValidationErrors,\n ValidationProperty,\n} from './mongo-validation-error.js'\nexport { extractValidationErrors, isMongoServerError, MongoValidationError } from './mongo-validation-error.js'\n\n// Export plugin types\nexport type { FastifyPapr, FastifyPaprOptions, ModelRegistration, ModelRegistrationPair } from './types.js'\n\n// Default export\nexport { fastifyPaprPlugin as default } from './fastify-papr-plugin.js'\n","import type { FastifyPluginAsync } from 'fastify'\n\nimport fp from 'fastify-plugin'\nimport type { IndexDescription } from 'mongodb'\nimport type { BaseSchema, SchemaOptions } from 'papr'\nimport { paprHelper } from './papr-helper.js'\nimport type { FastifyPapr, FastifyPaprOptions, ModelRegistration } from './types.js'\n\ndeclare module 'fastify' {\n interface FastifyInstance {\n /**\n * Papr models accessible through the fastify instance\n * Models can be accessed directly or through a named database connection\n */\n papr: FastifyPapr\n }\n}\n\n/**\n * Helper function to create a model registration\n * @param name Collection name\n * @param schema Papr schema definition with options\n * @param indexes Optional MongoDB indexes to create\n * @returns A model registration object\n */\nexport const asCollection = <TSchema extends BaseSchema>(\n name: string,\n schema: [TSchema, SchemaOptions<Partial<TSchema>>],\n indexes?: IndexDescription[],\n): ModelRegistration => ({\n name,\n schema,\n indexes,\n})\n\n/**\n * Main Fastify plugin for Papr integration\n * Registers models to MongoDB and decorates fastify with them\n */\nexport const fastifyPaprPlugin: FastifyPluginAsync<FastifyPaprOptions> = async (mutable_fastify, options) => {\n const helper = paprHelper(\n mutable_fastify,\n options.db,\n options.disableSchemaReconciliation,\n )\n\n const models = await helper.register(options.models)\n const { name: dbName } = options\n\n if (dbName) {\n if (mutable_fastify.papr) {\n if (mutable_fastify.papr[dbName]) {\n throw new Error(`Connection name already registered: ${dbName}`)\n }\n mutable_fastify.log.info(`Registering connection name: ${dbName}`)\n mutable_fastify.papr = {\n ...mutable_fastify.papr,\n\n [dbName]: models,\n }\n } else {\n mutable_fastify.decorate('papr', {\n [dbName]: models,\n })\n }\n } else {\n if (mutable_fastify.papr) {\n const items = Object.keys(mutable_fastify.papr).join(', ')\n throw new Error(`Models already registered: ${items}`)\n } else {\n mutable_fastify.decorate('papr', models)\n }\n }\n}\n\n/**\n * Default export as a Fastify plugin\n * Compatible with Fastify v4 and v5\n */\nexport default fp(fastifyPaprPlugin, {\n name: 'fastify-papr-plugin',\n fastify: '4.x || 5.x',\n})\n","import type { FastifyInstance } from 'fastify'\nimport type { Db, IndexDescription } from 'mongodb'\nimport type { BaseSchema, SchemaOptions } from 'papr'\nimport Papr from 'papr'\nimport type { ModelRegistrationPair } from './types.js'\nimport { type FastifyPapr } from './types.js'\n\n/**\n * Creates a helper for managing Papr models\n * Handles model registration and index creation\n *\n * @param fastify Fastify instance for logging\n * @param db MongoDB database connection\n * @param disableSchemaReconciliation Whether to skip schema validation reconciliation\n * @returns Object with registration methods\n */\nexport const paprHelper = (fastify: Readonly<FastifyInstance>, db: Db, disableSchemaReconciliation = false) => {\n const papr = new Papr()\n papr.initialize(db)\n\n /**\n * Registers a model with Papr\n * @param collectionName MongoDB collection name\n * @param collectionSchema Schema definition and options\n * @returns Registered Papr model\n */\n const registerModel = async <TSchema extends BaseSchema, TOptions extends SchemaOptions<TSchema>>(\n collectionName: string,\n collectionSchema: [TSchema, TOptions],\n ) => {\n const model = papr.model(collectionName, collectionSchema)\n\n if (!disableSchemaReconciliation) {\n await papr.updateSchema(model)\n }\n\n return model\n }\n\n /**\n * Creates MongoDB indexes for a collection\n * @param collectionName MongoDB collection name\n * @param indexes Array of index descriptions\n * @returns Array of created index names\n */\n const registerIndexes = async (collectionName: string, indexes: readonly IndexDescription[]) =>\n db.collection(collectionName).createIndexes(indexes as IndexDescription[])\n\n return {\n /**\n * Registers multiple models at once\n * @param models Model registration definitions\n * @returns Object with registered models\n */\n async register(models: ModelRegistrationPair<FastifyPapr>) {\n return Object.fromEntries(\n await Promise.all(\n Object.entries(models).map(async ([name, paprModel]) => {\n const model = await registerModel(paprModel.name, paprModel.schema)\n fastify.log.info(`Model ${name} decorated`)\n if (paprModel.indexes) {\n const index = await registerIndexes(paprModel.name, paprModel.indexes)\n fastify.log.info(`Indexes for ${paprModel.name} => ${index.join(', ')} created.`)\n }\n\n return [name, model] as const\n }),\n ),\n )\n },\n }\n}\n","import { MongoServerError } from 'mongodb'\n\n/**\n * Represents details about a property that failed validation\n */\nexport type ValidationDetail = {\n /** The validation operator that failed (e.g., 'maxLength', 'minimum') */\n operatorName?: string\n /** The specification that was violated */\n specifiedAs?: Record<string, unknown>\n /** Human-readable reason for the validation failure */\n reason?: string\n /** The actual value that was rejected */\n consideredValue?: unknown\n /** The type of the value that was rejected */\n consideredType?: unknown\n}\n\n/**\n * Represents a property that failed MongoDB validation\n */\nexport type ValidationProperty = {\n /** Name of the property that failed validation */\n propertyName: string\n /** Details about why validation failed */\n details: ValidationDetail[]\n}\n\n/**\n * MongoDB validation error structure as returned by the server\n */\nexport type DocumentValidationError = {\n /** ID of the document that failed validation */\n failingDocumentId?: string\n /** Error details from MongoDB */\n details?: {\n /** The validation operator (usually '$jsonSchema') */\n operatorName?: string\n /** Rules that weren't satisfied by the document */\n schemaRulesNotSatisfied?: [\n {\n /** The specific rule operator that failed */\n operatorName: string\n /** Properties that didn't satisfy the validation rules */\n propertiesNotSatisfied: ValidationProperty[]\n },\n ]\n }\n}\n\n/**\n * A simplified representation of MongoDB validation errors\n * Maps operator names to property details for easier client-side handling\n */\nexport type ValidationErrors = Record<string, Record<string, ValidationDetail[]>[]>[] | undefined\n\n/**\n * Checks if an error is a MongoDB server error\n * @param error The error to check\n * @returns True if the error is a MongoDB server error\n */\nexport function isMongoServerError(error: unknown): error is MongoServerError {\n if (error instanceof MongoServerError) {\n return true\n }\n\n const errorWithCode = error as { code?: number }\n return error instanceof Error && typeof errorWithCode.code === 'number'\n}\n\n/**\n * Transforms validation error properties into a more readable format\n * @param properties The properties that failed validation\n * @returns A mapped array of property errors with details\n * @internal\n */\nconst formatValidationProperties = (properties: readonly ValidationProperty[]) =>\n properties?.map(prop => ({ [prop.propertyName]: prop.details }))\n\n/**\n * Attempts to extract validation error details from a MongoDB server error\n * @param error The MongoDB server error to process\n * @returns A simplified representation of validation errors or undefined if not a validation error\n */\nexport function extractValidationErrors(\n error: Readonly<MongoServerError>,\n): ValidationErrors | undefined {\n // Check if this is actually a MongoDB validation error\n if (!isMongoServerError(error) || error.code !== 121) {\n return undefined\n }\n\n // Safely extract schema rules not satisfied\n const errInfo = error.errInfo as DocumentValidationError | undefined\n if (!errInfo?.details?.schemaRulesNotSatisfied?.length) {\n return undefined\n }\n\n const schemaRulesNotSatisfied = errInfo.details.schemaRulesNotSatisfied\n\n // Transform the schema rules into a more user-friendly format\n const result: ValidationErrors = schemaRulesNotSatisfied.map(rule => ({\n [rule.operatorName]: formatValidationProperties(rule.propertiesNotSatisfied || []),\n }))\n\n return result.length > 0 ? result : undefined\n}\n\n/**\n * Error class that simplifies handling of MongoDB validation errors\n * Extracts and provides easy access to validation details\n */\nexport class MongoValidationError extends Error {\n /** The extracted validation rules that weren't satisfied */\n validationErrors?: ValidationErrors\n\n /** Flag indicating whether this is a document validation error */\n hasValidationFailures: boolean\n\n /**\n * Creates an instance from a MongoDB server error\n * @param mongoError The original MongoDB error\n */\n constructor(mongoError: Readonly<MongoServerError>) {\n super(mongoError.message, { cause: mongoError })\n this.validationErrors = extractValidationErrors(mongoError)\n this.hasValidationFailures = this.validationErrors !== undefined\n }\n\n /**\n * Returns the validation errors as a JSON string\n * @returns A JSON string of the validation errors or undefined if not a validation error\n */\n getValidationErrorsAsString(): string | undefined {\n return this.hasValidationFailures ? JSON.stringify(this.validationErrors) : undefined\n }\n\n /**\n * Finds validation errors for a specific field\n * @param fieldName The field name to find errors for\n * @returns Array of validation details for the field or undefined if none found\n */\n getFieldErrors(fieldName: string): ValidationDetail[] | undefined {\n if (!this.hasValidationFailures || !this.validationErrors?.length) {\n return undefined\n }\n\n for (const ruleSet of this.validationErrors) {\n for (const [, properties] of Object.entries(ruleSet)) {\n for (const property of properties) {\n const field = Object.keys(property).find(key => key === fieldName)\n if (field) {\n return property[field]\n }\n }\n }\n }\n\n return undefined\n }\n}\n"],"mappings":"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","names":["Papr","fp"]}