@beignet/core
Version:
Core framework primitives for Beignet
129 lines • 5.37 kB
TypeScript
import type { StandardSchemaV1 } from "@standard-schema/spec";
/**
* Any Standard Schema compatible validator.
*/
export type StandardSchema = StandardSchemaV1<unknown, unknown>;
/**
* Infer the parsed output type from a Standard Schema.
*/
export type InferOutput<T extends StandardSchemaV1> = StandardSchemaV1.InferOutput<T>;
type AnyMethods = Record<string, any>;
/**
* Base entity instance passed to the `.methods(...)` builder: validated props
* plus the `with` and `toJSON` helpers, before domain methods attach.
*
* `with(...)` is typed as returning the base instance here because the final
* methods object is still being defined while the builder runs. At runtime
* the returned instance carries the entity's methods, and `EntityInstance`
* maps base-instance returns back to the full instance type so consumers keep
* typed method chaining.
*/
export type EntityBaseInstance<Schema extends StandardSchema> = InferOutput<Schema> & {
/**
* Create a new validated entity instance with patched properties.
*/
with(patch: Partial<InferOutput<Schema>>): EntityBaseInstance<Schema> | Promise<EntityBaseInstance<Schema>>;
/**
* Convert the entity to a plain JSON object for persistence.
*/
toJSON(): InferOutput<Schema>;
};
/**
* Replace base-instance returns with the full entity instance type, unwrapping
* promises, so methods that return `self` or `self.with(...)` chain with the
* entity's methods attached.
*/
type WithEntityInstance<R, Instance, Schema extends StandardSchema> = R extends Promise<infer P> ? Promise<WithEntityInstance<P, Instance, Schema>> : R extends EntityBaseInstance<Schema> ? Instance : R;
/**
* Entity instance with validated props, attached methods, and immutable update
* helpers. Method returns that are typed as the base instance (`self` or
* `self.with(...)`) are mapped to the full instance type so chaining keeps
* the entity's methods.
*/
export type EntityInstance<Name extends string, Schema extends StandardSchema, Methods extends AnyMethods> = InferOutput<Schema> & {
/**
* Create a new validated entity instance with patched properties.
*/
with(patch: Partial<InferOutput<Schema>>): EntityInstance<Name, Schema, Methods> | Promise<EntityInstance<Name, Schema, Methods>>;
/**
* Convert the entity to a plain JSON object for persistence.
*/
toJSON(): InferOutput<Schema>;
} & {
[K in keyof Methods]: Methods[K] extends (...args: infer A) => infer R ? (...args: A) => WithEntityInstance<R, EntityInstance<Name, Schema, Methods>, Schema> : Methods[K];
};
/**
* Entity definition returned by `defineEntity(...).build()`.
*/
export interface EntityDef<Name extends string, Schema extends StandardSchema, Methods extends AnyMethods> {
/** Name used for debugging and introspection. */
name: Name;
/** Standard Schema used to validate entity props. */
schema: Schema;
/**
* Create a new frozen entity instance from props.
*
* Returns a promise when the underlying Standard Schema validates async.
*/
create(props: InferOutput<Schema>): EntityInstance<Name, Schema, Methods> | Promise<EntityInstance<Name, Schema, Methods>>;
/**
* Reconstruct an entity instance from JSON, usually from persistence.
*/
fromJSON(json: InferOutput<Schema>): EntityInstance<Name, Schema, Methods> | Promise<EntityInstance<Name, Schema, Methods>>;
/** Type-only alias for the entity instance type. Undefined at runtime. */
Type: EntityInstance<Name, Schema, Methods>;
}
/**
* Builder class for creating Entities/Aggregates.
*/
declare class EntityBuilder<Name extends string, Schema extends StandardSchema, Methods extends AnyMethods> {
private readonly cfg;
constructor(cfg: {
name: Name;
schema?: Schema;
methods?: (self: EntityBaseInstance<Schema>) => Methods;
});
/**
* Define the schema for this entity using any Standard Schema compatible validator.
*/
props<S extends StandardSchema>(schema: S): EntityBuilder<Name, S, Methods>;
/**
* Define methods to attach to entity instances.
* The method builder receives the typed base instance: validated props plus
* `with` and `toJSON`.
*/
methods<M extends AnyMethods>(build: (self: EntityBaseInstance<Schema>) => M): EntityBuilder<Name, Schema, M>;
/**
* Finalize and build the entity definition.
*/
build(): EntityDef<Name, Schema, Methods>;
}
/**
* 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 declare function defineEntity<Name extends string>(name: Name): EntityBuilder<Name, StandardSchema, Record<string, never>>;
export {};
//# sourceMappingURL=entity.d.ts.map