Accept generated parse-style schemas in CultCacheTS
This commit is contained in:
parent
be0a96db7b
commit
b7cc33a846
6 changed files with 86 additions and 14 deletions
|
|
@ -14,6 +14,8 @@ This is not an ORM in a fake mustache. It is for cases where you want:
|
|||
## Current Shape
|
||||
|
||||
- document types are registered explicitly
|
||||
- a document schema only needs a `parse(...)` function, so generated JSON-schema
|
||||
contracts can plug in directly without growing a second hand-written Zod body
|
||||
- each persisted envelope stores:
|
||||
- `type`
|
||||
- `key`
|
||||
|
|
@ -166,6 +168,10 @@ const combatLogDocument = defineDocumentType({
|
|||
|
||||
If you do not supply one, `CultCacheTS` uses MessagePack plus Zod validation by default.
|
||||
|
||||
Zod is no longer mandatory. Any schema object with a `parse(...)` function will
|
||||
do, which means generated Ajv-backed JSON-schema contracts can feed the cache
|
||||
directly as long as they expose the same parse surface.
|
||||
|
||||
The important boundary is this:
|
||||
|
||||
- payload bytes are persisted as bytes
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ type HydratedEntry = CultCacheEnvelope & {
|
|||
};
|
||||
|
||||
type BackingStoreTypeReference = string | AnyCultCacheDocumentDefinition;
|
||||
type AccessorInput = string | ((value: unknown) => unknown);
|
||||
type AccessorInput = string | ((value: any) => unknown);
|
||||
|
||||
export class CultCacheBuilder {
|
||||
readonly #cache = new CultCache();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import type { ZodType } from "zod";
|
||||
import type {
|
||||
AnyCultCacheDocumentDefinition,
|
||||
CultCacheDocumentDefinition,
|
||||
CultCacheDocumentRegistry,
|
||||
CultCacheSchema,
|
||||
} from "./types";
|
||||
|
||||
import type { AnyCultCacheDocumentDefinition, CultCacheDocumentDefinition, CultCacheDocumentRegistry } from "./types";
|
||||
|
||||
export function defineDocumentType<TSchema extends ZodType>(
|
||||
export function defineDocumentType<TSchema extends CultCacheSchema>(
|
||||
definition: CultCacheDocumentDefinition<TSchema>,
|
||||
): CultCacheDocumentDefinition<TSchema> {
|
||||
if (!definition.type || definition.type.trim().length === 0) {
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@ export type {
|
|||
CultCacheDocumentFormatter,
|
||||
CultCacheDocumentIndexDefinition,
|
||||
CultCacheDocumentRegistry,
|
||||
CultCacheSchema,
|
||||
CultCacheDocumentValue,
|
||||
CultCacheEnvelope,
|
||||
CultCacheIndexScalar,
|
||||
InferCultCacheSchemaValue,
|
||||
CultCacheStoreRegistration,
|
||||
PushAllOptions,
|
||||
} from "./types";
|
||||
|
|
|
|||
21
src/types.ts
21
src/types.ts
|
|
@ -1,7 +1,12 @@
|
|||
import { z, type ZodType } from "zod";
|
||||
|
||||
export type CultCacheIndexScalar = string | number | boolean | bigint;
|
||||
|
||||
export interface CultCacheSchema<TValue = unknown> {
|
||||
parse(input: unknown): TValue;
|
||||
}
|
||||
|
||||
export type InferCultCacheSchemaValue<TSchema extends CultCacheSchema> =
|
||||
TSchema extends CultCacheSchema<infer TValue> ? TValue : never;
|
||||
|
||||
export type CultCacheDocumentFieldName<TValue> = TValue extends object ? Extract<keyof TValue, string> : never;
|
||||
|
||||
export type CultCacheDocumentAccessor<TValue> =
|
||||
|
|
@ -18,19 +23,19 @@ export interface CultCacheDocumentIndexDefinition<TValue> {
|
|||
accessor: CultCacheDocumentAccessor<TValue>;
|
||||
}
|
||||
|
||||
export interface CultCacheDocumentDefinition<TSchema extends ZodType = ZodType> {
|
||||
export interface CultCacheDocumentDefinition<TSchema extends CultCacheSchema = CultCacheSchema> {
|
||||
type: string;
|
||||
schema: TSchema;
|
||||
schemaName?: string;
|
||||
global?: boolean;
|
||||
name?: CultCacheDocumentAccessor<z.infer<TSchema>>;
|
||||
name?: CultCacheDocumentAccessor<InferCultCacheSchemaValue<TSchema>>;
|
||||
indexes?:
|
||||
| Readonly<Record<string, CultCacheDocumentAccessor<z.infer<TSchema>>>>
|
||||
| readonly CultCacheDocumentIndexDefinition<z.infer<TSchema>>[];
|
||||
formatter?: CultCacheDocumentFormatter<z.infer<TSchema>>;
|
||||
| Readonly<Record<string, CultCacheDocumentAccessor<InferCultCacheSchemaValue<TSchema>>>>
|
||||
| readonly CultCacheDocumentIndexDefinition<InferCultCacheSchemaValue<TSchema>>[];
|
||||
formatter?: CultCacheDocumentFormatter<InferCultCacheSchemaValue<TSchema>>;
|
||||
}
|
||||
|
||||
export type CultCacheDocumentValue<TDefinition extends CultCacheDocumentDefinition> = z.infer<
|
||||
export type CultCacheDocumentValue<TDefinition extends CultCacheDocumentDefinition> = InferCultCacheSchemaValue<
|
||||
TDefinition["schema"]
|
||||
>;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { z } from "zod";
|
|||
import { CultCache } from "../src/cult-cache";
|
||||
import { defineDocumentRegistry, defineDocumentType } from "../src/document";
|
||||
import { SingleFileMessagePackBackingStore } from "../src/single-file-messagepack-backing-store";
|
||||
import type { CacheBackingStore, CultCacheEnvelope } from "../src/types";
|
||||
import type { CacheBackingStore, CultCacheEnvelope, CultCacheSchema } from "../src/types";
|
||||
|
||||
test("CultCache supports registry bootstrap, global documents, and lookup by name and index", async () => {
|
||||
const itemDocument = defineDocumentType({
|
||||
|
|
@ -179,3 +179,59 @@ test("CultCache fails closed on unknown or illegal persisted polymorphic state",
|
|||
/has multiple persisted entries/,
|
||||
);
|
||||
});
|
||||
|
||||
test("CultCache accepts generated parse-style schemas without a Zod mirror", async () => {
|
||||
type GeneratedSettings = {
|
||||
schema_version: "generated.settings.v0";
|
||||
theme: string;
|
||||
retries: number;
|
||||
};
|
||||
|
||||
const generatedSchema: CultCacheSchema<GeneratedSettings> = {
|
||||
parse(input: unknown): GeneratedSettings {
|
||||
if (!input || typeof input !== "object") {
|
||||
throw new Error("generated settings must be an object");
|
||||
}
|
||||
|
||||
const value = input as Record<string, unknown>;
|
||||
if (value.schema_version !== "generated.settings.v0") {
|
||||
throw new Error("generated settings schema_version mismatch");
|
||||
}
|
||||
if (typeof value.theme !== "string") {
|
||||
throw new Error("generated settings theme must be a string");
|
||||
}
|
||||
if (typeof value.retries !== "number") {
|
||||
throw new Error("generated settings retries must be a number");
|
||||
}
|
||||
|
||||
return {
|
||||
schema_version: "generated.settings.v0",
|
||||
theme: value.theme,
|
||||
retries: value.retries,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const generatedDocument = defineDocumentType({
|
||||
type: "generated-settings",
|
||||
schema: generatedSchema,
|
||||
global: true,
|
||||
});
|
||||
|
||||
const storePath = join(await mkdtemp(join(tmpdir(), "cultcache-ts-")), "generated.msgpack");
|
||||
const cache = CultCache.builder()
|
||||
.withDocumentType(generatedDocument)
|
||||
.withGenericStore(new SingleFileMessagePackBackingStore(storePath))
|
||||
.build();
|
||||
|
||||
await cache.putGlobal(generatedDocument, {
|
||||
schema_version: "generated.settings.v0",
|
||||
theme: "ash",
|
||||
retries: 3,
|
||||
});
|
||||
|
||||
const settings = cache.getRequiredGlobal(generatedDocument);
|
||||
assert.equal(settings.schema_version, "generated.settings.v0");
|
||||
assert.equal(settings.theme, "ash");
|
||||
assert.equal(settings.retries, 3);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue