UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

161 lines 5.37 kB
/** * Validate data using a Standard Schema validator synchronously if possible */ function validateSchemaSync(schema, data) { const result = schema["~standard"].validate(data); if (result instanceof Promise) { return result.then((resolved) => { if (resolved.issues?.length) { const message = resolved.issues .map((issue) => { const path = issue.path !== undefined ? Array.isArray(issue.path) ? issue.path.join(".") : String(issue.path) : ""; return path ? `${path}: ${issue.message}` : issue.message; }) .join("; "); throw new Error(`Validation failed: ${message}`); } if ("value" in resolved) { return resolved.value; } throw new Error("Invalid Standard Schema result: missing value"); }); } if (result.issues?.length) { const message = result.issues .map((issue) => { const path = issue.path !== undefined ? Array.isArray(issue.path) ? issue.path.join(".") : String(issue.path) : ""; return path ? `${path}: ${issue.message}` : issue.message; }) .join("; "); throw new Error(`Validation failed: ${message}`); } if ("value" in result) { return result.value; } throw new Error("Invalid Standard Schema result: missing value"); } /** * Builder class for creating Entities/Aggregates. */ class EntityBuilder { cfg; constructor(cfg) { this.cfg = cfg; } /** * Define the schema for this entity using any Standard Schema compatible validator. */ props(schema) { return new EntityBuilder({ name: this.cfg.name, schema, // Methods declared before props were typed against the previous schema; // canonical builder order is props-then-methods, so this widening only // affects the discouraged reverse order. methods: this.cfg.methods, }); } /** * Define methods to attach to entity instances. * The method builder receives the typed base instance: validated props plus * `with` and `toJSON`. */ methods(build) { return new EntityBuilder({ name: this.cfg.name, schema: this.cfg.schema, methods: build, }); } /** * Finalize and build the entity definition. */ build() { if (!this.cfg.schema) { throw new Error(`Entity "${this.cfg.name}" is missing props schema`); } const schema = this.cfg.schema; const methodsBuilder = this.cfg.methods; const createInstance = (raw) => { const validationResult = validateSchemaSync(schema, raw); const buildInstance = (parsed) => { // Cast parsed to a plain object for spreading const parsedObj = parsed; const base = { ...parsedObj, // Immutable update - creates a new validated instance // Note: Re-validates the merged props for data integrity with(patch) { const patchObj = patch; return createInstance({ ...parsedObj, ...patchObj, }); }, toJSON() { return parsed; }, }; const methods = methodsBuilder?.(base) ?? {}; const instance = Object.freeze({ ...base, ...methods, }); return instance; }; if (validationResult instanceof Promise) { return validationResult.then(buildInstance); } return buildInstance(validationResult); }; const def = { name: this.cfg.name, schema, create: createInstance, fromJSON: createInstance, // Type is undefined at runtime - used only for TypeScript type inference via `typeof Entity.Type` Type: undefined, }; return def; } } /** * Create a new entity builder. * * Entities validate props, attach domain methods, and return frozen immutable * instances. `with(...)` revalidates the merged props. `.Type` is type-only and * should be used with `typeof Entity.Type`. * * @example * ```ts * const Todo = defineEntity("Todo") * .props(z.object({ * id: z.string(), * title: z.string(), * assigneeIds: z.array(z.string()).default([]), * })) * .methods((self) => ({ * addAssignee(id: string) { * if (self.assigneeIds.includes(id)) return self; * return self.with({ assigneeIds: [...self.assigneeIds, id] }); * }, * })) * .build(); * * type Todo = typeof Todo.Type; * ``` */ export function defineEntity(name) { return new EntityBuilder({ name, }); } //# sourceMappingURL=entity.js.map