@marktiderman/genesis-core 1.1.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ // src/schema/write-outcome.ts
2
+ var DataWriteError = class extends Error {
3
+ outcome;
4
+ current;
5
+ currentVersion;
6
+ constructor(outcome, message, options = {}) {
7
+ super(message);
8
+ this.name = "DataWriteError";
9
+ this.outcome = outcome;
10
+ this.current = options.current;
11
+ this.currentVersion = options.currentVersion;
12
+ }
13
+ };
14
+ function isDataWriteError(error) {
15
+ return error instanceof Error && error.name === "DataWriteError" && typeof error.outcome === "string";
16
+ }
17
+
18
+ export { DataWriteError, isDataWriteError };
19
+ //# sourceMappingURL=chunk-4W4W7F4I.js.map
20
+ //# sourceMappingURL=chunk-4W4W7F4I.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schema/write-outcome.ts"],"names":[],"mappings":";AAyCO,IAAM,cAAA,GAAN,cAA6B,KAAA,CAAM;AAAA,EAC/B,OAAA;AAAA,EACA,OAAA;AAAA,EACA,cAAA;AAAA,EAET,WAAA,CAAY,OAAA,EAAuB,OAAA,EAAiB,OAAA,GAAiC,EAAC,EAAG;AACvF,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,gBAAA;AACZ,IAAA,IAAA,CAAK,OAAA,GAAU,OAAA;AACf,IAAA,IAAA,CAAK,UAAU,OAAA,CAAQ,OAAA;AACvB,IAAA,IAAA,CAAK,iBAAiB,OAAA,CAAQ,cAAA;AAAA,EAChC;AACF;AAMO,SAAS,iBAAiB,KAAA,EAAyC;AACxE,EAAA,OACE,iBAAiB,KAAA,IACjB,KAAA,CAAM,SAAS,gBAAA,IACf,OAAQ,MAAgC,OAAA,KAAY,QAAA;AAExD","file":"chunk-4W4W7F4I.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Why a write did not save (DATA-STUDIO.md \"Write safety\")\n//\n// A write that changes nothing is never reported as \"permission denied\" by\n// assumption. The provider re-reads the row and says what actually happened.\n// ---------------------------------------------------------------------------\n\nimport type { DataRow } from \"./types\";\n\n/**\n * - `conflict`: someone else changed the row since it was read. `current`\n * carries the row as it is now.\n * - `gone`: the row was deleted, or is no longer visible to this user. The\n * two cannot be told apart without a privileged look at the table, which\n * would show this user a fact about a row they cannot see.\n * - `forbidden`: the row is visible and unchanged, but the app or the\n * database does not let this user make this change. Only reported when\n * that is proven, never assumed from \"zero rows changed\".\n * - `invalid`: the store rejected the value (type, constraint, not-null).\n * - `audit_failed`: the audit entry could not be written, so the edit was\n * rolled back with it.\n * - `unauthenticated`: no signed-in user, or the session expired.\n * - `error`: anything else. The edit did not commit.\n */\nexport type WriteOutcome =\n | \"conflict\"\n | \"gone\"\n | \"forbidden\"\n | \"invalid\"\n | \"audit_failed\"\n | \"unauthenticated\"\n | \"error\";\n\nexport interface DataWriteErrorOptions {\n /** The row as it is now, for a `conflict`. Only fields the user may read. */\n current?: DataRow;\n /** The row's current version token, for a `conflict`. */\n currentVersion?: string | null;\n}\n\n/** Thrown by a provider when a write did not commit. */\nexport class DataWriteError extends Error {\n readonly outcome: WriteOutcome;\n readonly current?: DataRow;\n readonly currentVersion?: string | null;\n\n constructor(outcome: WriteOutcome, message: string, options: DataWriteErrorOptions = {}) {\n super(message);\n this.name = \"DataWriteError\";\n this.outcome = outcome;\n this.current = options.current;\n this.currentVersion = options.currentVersion;\n }\n}\n\n/**\n * `instanceof` fails when two copies of this package are installed, so check\n * the shape instead.\n */\nexport function isDataWriteError(error: unknown): error is DataWriteError {\n return (\n error instanceof Error &&\n error.name === \"DataWriteError\" &&\n typeof (error as { outcome?: unknown }).outcome === \"string\"\n );\n}\n"]}
@@ -121,5 +121,5 @@ function createSupabaseProvider(client) {
121
121
  }
122
122
 
123
123
  export { createSupabaseProvider };
124
- //# sourceMappingURL=chunk-RKR5K7Q3.js.map
125
- //# sourceMappingURL=chunk-RKR5K7Q3.js.map
124
+ //# sourceMappingURL=chunk-6AA4ISUA.js.map
125
+ //# sourceMappingURL=chunk-6AA4ISUA.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/provider/supabase-provider.ts"],"names":[],"mappings":"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","file":"chunk-RKR5K7Q3.js","sourcesContent":["import type {\n DataProvider,\n FilterParam,\n} from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Minimal Supabase client shape — no hard dependency on @supabase/supabase-js\n// ---------------------------------------------------------------------------\n\n/** A chainable query builder returned by `client.from(table)`. */\ninterface SupabaseQueryBuilder {\n select(\n columns?: string,\n options?: { count?: \"exact\" | \"planned\" | \"estimated\" },\n ): SupabaseQueryBuilder;\n insert(data: Record<string, unknown> | Record<string, unknown>[]): SupabaseQueryBuilder;\n update(data: Record<string, unknown>): SupabaseQueryBuilder;\n delete(): SupabaseQueryBuilder;\n eq(column: string, value: unknown): SupabaseQueryBuilder;\n neq(column: string, value: unknown): SupabaseQueryBuilder;\n gt(column: string, value: unknown): SupabaseQueryBuilder;\n lt(column: string, value: unknown): SupabaseQueryBuilder;\n gte(column: string, value: unknown): SupabaseQueryBuilder;\n lte(column: string, value: unknown): SupabaseQueryBuilder;\n ilike(column: string, value: string): SupabaseQueryBuilder;\n in(column: string, values: unknown[]): SupabaseQueryBuilder;\n is(column: string, value: null): SupabaseQueryBuilder;\n not(column: string, operator: string, value: unknown): SupabaseQueryBuilder;\n or(filters: string): SupabaseQueryBuilder;\n order(column: string, options?: { ascending?: boolean }): SupabaseQueryBuilder;\n range(from: number, to: number): SupabaseQueryBuilder;\n single(): SupabaseQueryBuilder;\n then: Promise<{ data: unknown; error: unknown; count?: number | null }>[\"then\"];\n}\n\n/**\n * Minimal Supabase client interface — the public shape this package has\n * always exposed for a caller that wants to type its own client or a\n * wrapper around one. Unchanged: `from()` still returns `SupabaseQueryBuilder`\n * so existing consumers that annotate a variable with this type and chain\n * off it keep type-checking exactly as before.\n */\nexport interface SupabaseClient {\n from(table: string): SupabaseQueryBuilder;\n}\n\n/**\n * What `createSupabaseProvider` actually requires from its `client` argument\n * — deliberately looser than the public `SupabaseClient` above, and used\n * ONLY for that one parameter. A real `@supabase/supabase-js` client's\n * `from()` returns a generic, table-typed `PostgrestQueryBuilder` whose chain\n * methods don't structurally match `SupabaseQueryBuilder`, which used to\n * force callers to pass their client through `as never`. Since `unknown` is\n * a supertype of everything, any real client's `from(table: string)`\n * satisfies THIS shape without a cast; `createSupabaseProvider` narrows to\n * `SupabaseQueryBuilder` internally, once, right after each `from()` call.\n * Loosening only the input here — rather than `SupabaseClient` itself —\n * means a caller relying on `SupabaseClient`'s stricter return type for\n * their own chaining is unaffected.\n */\nexport interface SupabaseClientInput {\n from(table: string): unknown;\n}\n\n// ---------------------------------------------------------------------------\n// Provider factory\n// ---------------------------------------------------------------------------\n\nfunction escapeIlike(value: string): string {\n return value.replace(/[%_\\\\]/g, (char) => `\\\\${char}`);\n}\n\nexport function createSupabaseProvider(client: SupabaseClientInput): DataProvider {\n // ---------------------------------------------------------------------------\n // Helpers\n // ---------------------------------------------------------------------------\n\n function applyFilters(\n query: SupabaseQueryBuilder,\n filters: FilterParam[] | undefined,\n ): SupabaseQueryBuilder {\n if (!filters) return query;\n for (const f of filters) {\n switch (f.operator) {\n case \"eq\":\n query = query.eq(f.field, f.value);\n break;\n case \"neq\":\n query = query.neq(f.field, f.value);\n break;\n case \"gt\":\n query = query.gt(f.field, f.value);\n break;\n case \"lt\":\n query = query.lt(f.field, f.value);\n break;\n case \"gte\":\n query = query.gte(f.field, f.value);\n break;\n case \"lte\":\n query = query.lte(f.field, f.value);\n break;\n case \"contains\":\n query = query.ilike(f.field, `%${escapeIlike(String(f.value))}%`);\n break;\n case \"in\":\n query = query.in(f.field, f.value);\n break;\n case \"is_null\":\n query = query.is(f.field, null);\n break;\n case \"is_not_null\":\n query = query.not(f.field, \"is\", null);\n break;\n case \"between\": {\n const [lo, hi] = f.value;\n query = query.gte(f.field, lo).lte(f.field, hi);\n break;\n }\n default:\n throw new Error(`[SupabaseProvider] Unknown filter operator \"${(f as FilterParam).operator}\"`);\n break;\n }\n }\n return query;\n }\n\n function throwIfError(result: { error: unknown }): void {\n if (result.error) {\n const pgError = result.error as { message?: string; code?: string; hint?: string };\n const message = pgError.message ?? String(result.error);\n const err = new Error(message, { cause: result.error });\n Object.assign(err, { code: pgError.code, hint: pgError.hint });\n throw err;\n }\n }\n\n // ---------------------------------------------------------------------------\n // DataProvider implementation\n // ---------------------------------------------------------------------------\n\n const provider: DataProvider = {\n async getList(resource, params) {\n let query = (client.from(resource) as SupabaseQueryBuilder).select(\n params.select ?? \"*\",\n { count: \"exact\" },\n );\n\n query = applyFilters(query, params.filters);\n\n // Search — convert to OR of ilike.\n if (params.search && params.search.query && params.search.fields.length > 0) {\n const escaped = escapeIlike(params.search.query);\n const safeFieldPattern = /^[a-zA-Z_][a-zA-Z0-9_.]*$/;\n const clauses = params.search.fields\n .filter((f) => safeFieldPattern.test(f))\n .map((f) => `${f}.ilike.%${escaped}%`)\n .join(\",\");\n if (clauses) {\n query = query.or(clauses);\n }\n }\n\n // Sort.\n if (params.sort) {\n query = query.order(params.sort.field, {\n ascending: params.sort.order === \"asc\",\n });\n }\n\n // Pagination (offset).\n if (params.pagination) {\n const { page, perPage } = params.pagination;\n const from = (page - 1) * perPage;\n const to = from + perPage - 1;\n query = query.range(from, to);\n }\n\n const result = (await query) as {\n data: unknown[];\n error: unknown;\n count?: number | null;\n };\n throwIfError(result);\n\n return { data: result.data ?? [], total: result.count ?? 0 } as never;\n },\n\n async getOne(resource, params) {\n const result = (await (client.from(resource) as SupabaseQueryBuilder)\n .select(params.select ?? \"*\")\n .eq(\"id\", params.id)\n .single()) as { data: unknown; error: unknown };\n throwIfError(result);\n return { data: result.data } as never;\n },\n\n async getMany(resource, params) {\n if (params.ids.length === 0) return { data: [] } as never;\n const result = (await (client.from(resource) as SupabaseQueryBuilder)\n .select(\"*\")\n .in(\"id\", params.ids)) as { data: unknown[]; error: unknown };\n throwIfError(result);\n return { data: result.data ?? [] } as never;\n },\n\n async create(resource, params) {\n const result = (await (client.from(resource) as SupabaseQueryBuilder)\n .insert(params.data as Record<string, unknown>)\n .select(\"*\")\n .single()) as { data: unknown; error: unknown };\n throwIfError(result);\n return { data: result.data } as never;\n },\n\n async update(resource, params) {\n const result = (await (client.from(resource) as SupabaseQueryBuilder)\n .update(params.data as Record<string, unknown>)\n .eq(\"id\", params.id)\n .select(\"*\")\n .single()) as { data: unknown; error: unknown };\n throwIfError(result);\n return { data: result.data } as never;\n },\n\n async deleteOne(resource, params) {\n const result = (await (client.from(resource) as SupabaseQueryBuilder)\n .delete()\n .eq(\"id\", params.id)\n .select(\"id\")\n .single()) as { data: { id: string | number } | null; error: unknown };\n throwIfError(result);\n if (!result.data) {\n throw new Error(`[SupabaseProvider] Delete failed: no row found with id \"${params.id}\" in \"${resource}\"`);\n }\n return { data: result.data };\n },\n };\n\n return provider;\n}\n"]}
1
+ {"version":3,"sources":["../src/provider/supabase-provider.ts"],"names":[],"mappings":"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","file":"chunk-6AA4ISUA.js","sourcesContent":["import type {\n DataProvider,\n FilterParam,\n} from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Minimal Supabase client shape — no hard dependency on @supabase/supabase-js\n// ---------------------------------------------------------------------------\n\n/** A chainable query builder returned by `client.from(table)`. */\ninterface SupabaseQueryBuilder {\n select(\n columns?: string,\n options?: { count?: \"exact\" | \"planned\" | \"estimated\" },\n ): SupabaseQueryBuilder;\n insert(data: Record<string, unknown> | Record<string, unknown>[]): SupabaseQueryBuilder;\n update(data: Record<string, unknown>): SupabaseQueryBuilder;\n delete(): SupabaseQueryBuilder;\n eq(column: string, value: unknown): SupabaseQueryBuilder;\n neq(column: string, value: unknown): SupabaseQueryBuilder;\n gt(column: string, value: unknown): SupabaseQueryBuilder;\n lt(column: string, value: unknown): SupabaseQueryBuilder;\n gte(column: string, value: unknown): SupabaseQueryBuilder;\n lte(column: string, value: unknown): SupabaseQueryBuilder;\n ilike(column: string, value: string): SupabaseQueryBuilder;\n in(column: string, values: unknown[]): SupabaseQueryBuilder;\n is(column: string, value: null): SupabaseQueryBuilder;\n not(column: string, operator: string, value: unknown): SupabaseQueryBuilder;\n or(filters: string): SupabaseQueryBuilder;\n order(column: string, options?: { ascending?: boolean }): SupabaseQueryBuilder;\n range(from: number, to: number): SupabaseQueryBuilder;\n single(): SupabaseQueryBuilder;\n then: Promise<{ data: unknown; error: unknown; count?: number | null }>[\"then\"];\n}\n\n/**\n * Minimal Supabase client interface — the public shape this package has\n * always exposed for a caller that wants to type its own client or a\n * wrapper around one. Unchanged: `from()` still returns `SupabaseQueryBuilder`\n * so existing consumers that annotate a variable with this type and chain\n * off it keep type-checking exactly as before.\n */\nexport interface SupabaseClient {\n from(table: string): SupabaseQueryBuilder;\n}\n\n/**\n * What `createSupabaseProvider` actually requires from its `client` argument\n * — deliberately looser than the public `SupabaseClient` above, and used\n * ONLY for that one parameter. A real `@supabase/supabase-js` client's\n * `from()` returns a generic, table-typed `PostgrestQueryBuilder` whose chain\n * methods don't structurally match `SupabaseQueryBuilder`, which used to\n * force callers to pass their client through `as never`. Since `unknown` is\n * a supertype of everything, any real client's `from(table: string)`\n * satisfies THIS shape without a cast; `createSupabaseProvider` narrows to\n * `SupabaseQueryBuilder` internally, once, right after each `from()` call.\n * Loosening only the input here — rather than `SupabaseClient` itself —\n * means a caller relying on `SupabaseClient`'s stricter return type for\n * their own chaining is unaffected.\n */\nexport interface SupabaseClientInput {\n from(table: string): unknown;\n}\n\n// ---------------------------------------------------------------------------\n// Provider factory\n// ---------------------------------------------------------------------------\n\nfunction escapeIlike(value: string): string {\n return value.replace(/[%_\\\\]/g, (char) => `\\\\${char}`);\n}\n\nexport function createSupabaseProvider(client: SupabaseClientInput): DataProvider {\n // ---------------------------------------------------------------------------\n // Helpers\n // ---------------------------------------------------------------------------\n\n function applyFilters(\n query: SupabaseQueryBuilder,\n filters: FilterParam[] | undefined,\n ): SupabaseQueryBuilder {\n if (!filters) return query;\n for (const f of filters) {\n switch (f.operator) {\n case \"eq\":\n query = query.eq(f.field, f.value);\n break;\n case \"neq\":\n query = query.neq(f.field, f.value);\n break;\n case \"gt\":\n query = query.gt(f.field, f.value);\n break;\n case \"lt\":\n query = query.lt(f.field, f.value);\n break;\n case \"gte\":\n query = query.gte(f.field, f.value);\n break;\n case \"lte\":\n query = query.lte(f.field, f.value);\n break;\n case \"contains\":\n query = query.ilike(f.field, `%${escapeIlike(String(f.value))}%`);\n break;\n case \"in\":\n query = query.in(f.field, f.value);\n break;\n case \"is_null\":\n query = query.is(f.field, null);\n break;\n case \"is_not_null\":\n query = query.not(f.field, \"is\", null);\n break;\n case \"between\": {\n const [lo, hi] = f.value;\n query = query.gte(f.field, lo).lte(f.field, hi);\n break;\n }\n default:\n throw new Error(`[SupabaseProvider] Unknown filter operator \"${(f as FilterParam).operator}\"`);\n break;\n }\n }\n return query;\n }\n\n function throwIfError(result: { error: unknown }): void {\n if (result.error) {\n const pgError = result.error as { message?: string; code?: string; hint?: string };\n const message = pgError.message ?? String(result.error);\n const err = new Error(message, { cause: result.error });\n Object.assign(err, { code: pgError.code, hint: pgError.hint });\n throw err;\n }\n }\n\n // ---------------------------------------------------------------------------\n // DataProvider implementation\n // ---------------------------------------------------------------------------\n\n const provider: DataProvider = {\n async getList(resource, params) {\n let query = (client.from(resource) as SupabaseQueryBuilder).select(\n params.select ?? \"*\",\n { count: \"exact\" },\n );\n\n query = applyFilters(query, params.filters);\n\n // Search — convert to OR of ilike.\n if (params.search && params.search.query && params.search.fields.length > 0) {\n const escaped = escapeIlike(params.search.query);\n const safeFieldPattern = /^[a-zA-Z_][a-zA-Z0-9_.]*$/;\n const clauses = params.search.fields\n .filter((f) => safeFieldPattern.test(f))\n .map((f) => `${f}.ilike.%${escaped}%`)\n .join(\",\");\n if (clauses) {\n query = query.or(clauses);\n }\n }\n\n // Sort.\n if (params.sort) {\n query = query.order(params.sort.field, {\n ascending: params.sort.order === \"asc\",\n });\n }\n\n // Pagination (offset).\n if (params.pagination) {\n const { page, perPage } = params.pagination;\n const from = (page - 1) * perPage;\n const to = from + perPage - 1;\n query = query.range(from, to);\n }\n\n const result = (await query) as {\n data: unknown[];\n error: unknown;\n count?: number | null;\n };\n throwIfError(result);\n\n return { data: result.data ?? [], total: result.count ?? 0 } as never;\n },\n\n async getOne(resource, params) {\n const result = (await (client.from(resource) as SupabaseQueryBuilder)\n .select(params.select ?? \"*\")\n .eq(\"id\", params.id)\n .single()) as { data: unknown; error: unknown };\n throwIfError(result);\n return { data: result.data } as never;\n },\n\n async getMany(resource, params) {\n if (params.ids.length === 0) return { data: [] } as never;\n const result = (await (client.from(resource) as SupabaseQueryBuilder)\n .select(\"*\")\n .in(\"id\", params.ids)) as { data: unknown[]; error: unknown };\n throwIfError(result);\n return { data: result.data ?? [] } as never;\n },\n\n async create(resource, params) {\n const result = (await (client.from(resource) as SupabaseQueryBuilder)\n .insert(params.data as Record<string, unknown>)\n .select(\"*\")\n .single()) as { data: unknown; error: unknown };\n throwIfError(result);\n return { data: result.data } as never;\n },\n\n async update(resource, params) {\n const result = (await (client.from(resource) as SupabaseQueryBuilder)\n .update(params.data as Record<string, unknown>)\n .eq(\"id\", params.id)\n .select(\"*\")\n .single()) as { data: unknown; error: unknown };\n throwIfError(result);\n return { data: result.data } as never;\n },\n\n async deleteOne(resource, params) {\n const result = (await (client.from(resource) as SupabaseQueryBuilder)\n .delete()\n .eq(\"id\", params.id)\n .select(\"id\")\n .single()) as { data: { id: string | number } | null; error: unknown };\n throwIfError(result);\n if (!result.data) {\n throw new Error(`[SupabaseProvider] Delete failed: no row found with id \"${params.id}\" in \"${resource}\"`);\n }\n return { data: result.data };\n },\n };\n\n return provider;\n}\n"]}
@@ -0,0 +1,41 @@
1
+ // src/schema/keys.ts
2
+ function keyFields(contract) {
3
+ return typeof contract.key === "string" ? [contract.key] : contract.key;
4
+ }
5
+ function keyValue(row, field) {
6
+ const value = row[field];
7
+ if (typeof value === "string" || typeof value === "number") return value;
8
+ if (typeof value === "bigint" || typeof value === "boolean") return String(value);
9
+ throw new Error(
10
+ `Record key field "${field}" is ${value === null ? "null" : typeof value}; a key value must be a string or a number.`
11
+ );
12
+ }
13
+ function encodeRecordKey(contract, row) {
14
+ const fields = keyFields(contract);
15
+ if (fields.length === 0) throw new Error("This resource has no key, so its rows cannot be addressed.");
16
+ if (fields.length === 1) return keyValue(row, fields[0]);
17
+ return JSON.stringify(fields.map((field) => keyValue(row, field)));
18
+ }
19
+ function decodeRecordKey(contract, id) {
20
+ const fields = keyFields(contract);
21
+ if (fields.length === 0) throw new Error("This resource has no key, so its rows cannot be addressed.");
22
+ if (fields.length === 1) return { [fields[0]]: id };
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(String(id));
26
+ } catch {
27
+ throw new Error(`"${String(id)}" is not a composite key for (${fields.join(", ")}).`);
28
+ }
29
+ if (!Array.isArray(parsed) || parsed.length !== fields.length || !parsed.every((v) => typeof v === "string" || typeof v === "number")) {
30
+ throw new Error(`"${String(id)}" is not a composite key for (${fields.join(", ")}).`);
31
+ }
32
+ const out = {};
33
+ fields.forEach((field, i) => {
34
+ out[field] = parsed[i];
35
+ });
36
+ return out;
37
+ }
38
+
39
+ export { decodeRecordKey, encodeRecordKey, keyFields };
40
+ //# sourceMappingURL=chunk-KV2HW4G4.js.map
41
+ //# sourceMappingURL=chunk-KV2HW4G4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schema/keys.ts"],"names":[],"mappings":";AAYO,SAAS,UAAU,QAAA,EAA4D;AACpF,EAAA,OAAO,OAAO,SAAS,GAAA,KAAQ,QAAA,GAAW,CAAC,QAAA,CAAS,GAAG,IAAI,QAAA,CAAS,GAAA;AACtE;AAEA,SAAS,QAAA,CAAS,KAAc,KAAA,EAAgC;AAC9D,EAAA,MAAM,KAAA,GAAQ,IAAI,KAAK,CAAA;AACvB,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,OAAO,KAAA,KAAU,UAAU,OAAO,KAAA;AACnE,EAAA,IAAI,OAAO,UAAU,QAAA,IAAY,OAAO,UAAU,SAAA,EAAW,OAAO,OAAO,KAAK,CAAA;AAChF,EAAA,MAAM,IAAI,KAAA;AAAA,IACR,qBAAqB,KAAK,CAAA,KAAA,EAAQ,UAAU,IAAA,GAAO,MAAA,GAAS,OAAO,KAAK,CAAA,2CAAA;AAAA,GAE1E;AACF;AASO,SAAS,eAAA,CACd,UACA,GAAA,EACY;AACZ,EAAA,MAAM,MAAA,GAAS,UAAU,QAAQ,CAAA;AACjC,EAAA,IAAI,OAAO,MAAA,KAAW,CAAA,EAAG,MAAM,IAAI,MAAM,4DAA4D,CAAA;AACrG,EAAA,IAAI,MAAA,CAAO,WAAW,CAAA,EAAG,OAAO,SAAS,GAAA,EAAK,MAAA,CAAO,CAAC,CAAE,CAAA;AACxD,EAAA,OAAO,IAAA,CAAK,SAAA,CAAU,MAAA,CAAO,GAAA,CAAI,CAAC,UAAU,QAAA,CAAS,GAAA,EAAK,KAAK,CAAC,CAAC,CAAA;AACnE;AAQO,SAAS,eAAA,CACd,UACA,EAAA,EACiC;AACjC,EAAA,MAAM,MAAA,GAAS,UAAU,QAAQ,CAAA;AACjC,EAAA,IAAI,OAAO,MAAA,KAAW,CAAA,EAAG,MAAM,IAAI,MAAM,4DAA4D,CAAA;AACrG,EAAA,IAAI,MAAA,CAAO,MAAA,KAAW,CAAA,EAAG,OAAO,EAAE,CAAC,MAAA,CAAO,CAAC,CAAE,GAAG,EAAA,EAAG;AAEnD,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI;AACF,IAAA,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,MAAA,CAAO,EAAE,CAAC,CAAA;AAAA,EAChC,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,CAAA,EAAI,MAAA,CAAO,EAAE,CAAC,CAAA,8BAAA,EAAiC,MAAA,CAAO,IAAA,CAAK,IAAI,CAAC,CAAA,EAAA,CAAI,CAAA;AAAA,EACtF;AACA,EAAA,IACE,CAAC,MAAM,OAAA,CAAQ,MAAM,KACrB,MAAA,CAAO,MAAA,KAAW,OAAO,MAAA,IACzB,CAAC,OAAO,KAAA,CAAM,CAAC,MAAM,OAAO,CAAA,KAAM,YAAY,OAAO,CAAA,KAAM,QAAQ,CAAA,EACnE;AACA,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,CAAA,EAAI,MAAA,CAAO,EAAE,CAAC,CAAA,8BAAA,EAAiC,MAAA,CAAO,IAAA,CAAK,IAAI,CAAC,CAAA,EAAA,CAAI,CAAA;AAAA,EACtF;AACA,EAAA,MAAM,MAAuC,EAAC;AAC9C,EAAA,MAAA,CAAO,OAAA,CAAQ,CAAC,KAAA,EAAO,CAAA,KAAM;AAC3B,IAAA,GAAA,CAAI,KAAK,CAAA,GAAI,MAAA,CAAO,CAAC,CAAA;AAAA,EACvB,CAAC,CAAA;AACD,EAAA,OAAO,GAAA;AACT","file":"chunk-KV2HW4G4.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Record identity for single and composite keys (DATA-STUDIO.md decision 13)\n//\n// `DataProvider` methods take one `Identifier`. A composite key travels\n// through that same slot as a canonical string, so every existing provider\n// signature stays as it is and a junction table still has one id per row.\n// ---------------------------------------------------------------------------\n\nimport type { Identifier } from \"../provider/types\";\nimport type { DataRow, ResourceContract } from \"./types\";\n\n/** The key of a contract as a list, whichever form `key` was declared in. */\nexport function keyFields(contract: Pick<ResourceContract, \"key\">): readonly string[] {\n return typeof contract.key === \"string\" ? [contract.key] : contract.key;\n}\n\nfunction keyValue(row: DataRow, field: string): string | number {\n const value = row[field];\n if (typeof value === \"string\" || typeof value === \"number\") return value;\n if (typeof value === \"bigint\" || typeof value === \"boolean\") return String(value);\n throw new Error(\n `Record key field \"${field}\" is ${value === null ? \"null\" : typeof value}; ` +\n \"a key value must be a string or a number.\",\n );\n}\n\n/**\n * One `Identifier` for a row.\n *\n * A single-field key returns the raw value, so `id`-keyed resources look\n * exactly as they always have. A composite key returns a JSON array of the\n * values in key order, e.g. `'[\"c-1\",\"u-7\"]'`.\n */\nexport function encodeRecordKey(\n contract: Pick<ResourceContract, \"key\">,\n row: DataRow,\n): Identifier {\n const fields = keyFields(contract);\n if (fields.length === 0) throw new Error(\"This resource has no key, so its rows cannot be addressed.\");\n if (fields.length === 1) return keyValue(row, fields[0]!);\n return JSON.stringify(fields.map((field) => keyValue(row, field)));\n}\n\n/**\n * The inverse of {@link encodeRecordKey}: field name → value.\n *\n * Throws on an id that does not fit the key, rather than guessing, because a\n * guessed key addresses the wrong row.\n */\nexport function decodeRecordKey(\n contract: Pick<ResourceContract, \"key\">,\n id: Identifier,\n): Record<string, string | number> {\n const fields = keyFields(contract);\n if (fields.length === 0) throw new Error(\"This resource has no key, so its rows cannot be addressed.\");\n if (fields.length === 1) return { [fields[0]!]: id };\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(String(id));\n } catch {\n throw new Error(`\"${String(id)}\" is not a composite key for (${fields.join(\", \")}).`);\n }\n if (\n !Array.isArray(parsed) ||\n parsed.length !== fields.length ||\n !parsed.every((v) => typeof v === \"string\" || typeof v === \"number\")\n ) {\n throw new Error(`\"${String(id)}\" is not a composite key for (${fields.join(\", \")}).`);\n }\n const out: Record<string, string | number> = {};\n fields.forEach((field, i) => {\n out[field] = parsed[i] as string | number;\n });\n return out;\n}\n"]}
@@ -3,6 +3,6 @@ export { UseOneOptions, useOne } from './use-one.js';
3
3
  export { UseResourceFormOptions, UseResourceFormReturn, buildArrayItemDefaults, useResourceForm } from './use-resource-form.js';
4
4
  export { UseWizardReturn, WizardStep, useWizard } from './use-wizard.js';
5
5
  import '@tanstack/react-query';
6
- import '../types-BzcZrTjl.js';
7
- import 'react-hook-form';
6
+ import '../types-DUbRb8qV.js';
8
7
  import '../form-types.js';
8
+ import 'react-hook-form';
@@ -1,5 +1,6 @@
1
1
  import { UseQueryResult } from '@tanstack/react-query';
2
- import { B as BaseRecord, I as Identifier } from '../types-BzcZrTjl.js';
2
+ import { B as BaseRecord, I as Identifier } from '../types-DUbRb8qV.js';
3
+ import '../form-types.js';
3
4
 
4
5
  interface UseOneOptions {
5
6
  /** Supabase select/join syntax, e.g. "*, author:profiles(name)". */
@@ -1,5 +1,6 @@
1
1
  import { UseQueryResult, UseMutationResult } from '@tanstack/react-query';
2
- import { h as SortParam, F as FilterParam, g as ListResult, L as ListParams, I as Identifier, B as BaseRecord } from '../types-BzcZrTjl.js';
2
+ import { z as SortParam, k as FilterParam, p as ListResult, L as ListParams, I as Identifier, B as BaseRecord } from '../types-DUbRb8qV.js';
3
+ import '../form-types.js';
3
4
 
4
5
  interface UseResourceOptions<T> {
5
6
  defaultSort?: SortParam;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export { A as ArrayFilter, B as BaseRecord, C as CreateParams, a as CreateResult, D as DataProvider, b as DeleteParams, c as DeleteResult, F as FilterParam, G as GetManyParams, d as GetManyResult, e as GetOneParams, f as GetOneResult, I as Identifier, L as ListParams, g as ListResult, N as NullFilter, P as PaginationParam, R as RangeFilter, S as ScalarFilter, h as SortParam, U as UpdateParams, i as UpdateResult } from './types-BzcZrTjl.js';
1
+ export { A as ArrayFilter, a as AuditEntry, B as BaseRecord, b as BulkChange, c as BulkResult, C as CreateParams, d as CreateResult, D as DataProvider, e as DataRow, f as DeleteParams, g as DeleteResult, F as FieldPermissions, h as FieldReference, i as FieldSchema, j as FilterOperator, k as FilterParam, G as GetManyParams, l as GetManyResult, m as GetOneParams, n as GetOneResult, H as HistoryParams, I as Identifier, o as ImportParams, L as ListParams, p as ListResult, N as NullFilter, P as PaginationParam, q as ProviderCapabilities, R as RangeFilter, r as RecordComment, s as ResourceAccess, t as ResourceContract, u as ResourceRelation, v as ResourceSchema, w as ResourceSummary, x as RowAction, y as RowFailure, S as ScalarFilter, z as SortParam, E as StorageType, U as UpdateParams, J as UpdateResult, V as VersionField } from './types-DUbRb8qV.js';
2
+ export { D as DataWriteError, a as DataWriteErrorOptions, W as WriteOutcome, i as isDataWriteError } from './write-outcome-aYToY3dK.js';
2
3
  export { DataProviderRoot, DataProviderRootProps, GenesisProvider, GenesisProviderProps, MockProviderOptions, SupabaseClient, SupabaseClientInput, createMockProvider, createSupabaseProvider, useDataProvider, useStorage } from './provider/index.js';
3
4
  export { S as StorageAdapter } from './types-DKcxHXxq.js';
4
5
  export { createLocalStorageAdapter, createMemoryStorageAdapter, createNoopStorageAdapter } from './storage/index.js';
@@ -6,7 +7,8 @@ export { UseResourceOptions, UseResourceReturn, useResource } from './hooks/use-
6
7
  export { UseOneOptions, useOne } from './hooks/use-one.js';
7
8
  export { UseResourceFormOptions, UseResourceFormReturn, buildArrayItemDefaults, useResourceForm } from './hooks/use-resource-form.js';
8
9
  export { UseWizardReturn, WizardStep, useWizard } from './hooks/use-wizard.js';
9
- export { FieldType, ResourceFormFieldDef, SelectOption } from './form-types.js';
10
+ export { FieldType, ResourceFormFieldDef, SelectOption, FieldType as WidgetHint } from './form-types.js';
11
+ export { decodeRecordKey, encodeRecordKey, keyFields } from './schema/index.js';
10
12
  export { titleCase } from './utils.js';
11
13
  import 'react';
12
14
  import '@tanstack/react-query';
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
- export { createSupabaseProvider } from './chunk-RKR5K7Q3.js';
1
+ export { createSupabaseProvider } from './chunk-6AA4ISUA.js';
2
2
  import './chunk-XCX2TRBL.js';
3
+ export { decodeRecordKey, encodeRecordKey, keyFields } from './chunk-KV2HW4G4.js';
4
+ export { DataWriteError, isDataWriteError } from './chunk-4W4W7F4I.js';
3
5
  import './chunk-65SXNHWE.js';
4
6
  import './chunk-EL7YGOTY.js';
5
7
  export { buildArrayItemDefaults, useResourceForm } from './chunk-5FTUB2M6.js';
@@ -1,8 +1,10 @@
1
- import { D as DataProvider } from '../types-BzcZrTjl.js';
2
- export { A as ArrayFilter, B as BaseRecord, C as CreateParams, a as CreateResult, b as DeleteParams, c as DeleteResult, F as FilterParam, G as GetManyParams, d as GetManyResult, e as GetOneParams, f as GetOneResult, I as Identifier, L as ListParams, g as ListResult, N as NullFilter, P as PaginationParam, R as RangeFilter, S as ScalarFilter, h as SortParam, U as UpdateParams, i as UpdateResult } from '../types-BzcZrTjl.js';
1
+ import { D as DataProvider } from '../types-DUbRb8qV.js';
2
+ export { A as ArrayFilter, B as BaseRecord, b as BulkChange, c as BulkResult, C as CreateParams, d as CreateResult, f as DeleteParams, g as DeleteResult, k as FilterParam, G as GetManyParams, l as GetManyResult, m as GetOneParams, n as GetOneResult, H as HistoryParams, I as Identifier, o as ImportParams, L as ListParams, p as ListResult, N as NullFilter, P as PaginationParam, R as RangeFilter, y as RowFailure, S as ScalarFilter, z as SortParam, U as UpdateParams, J as UpdateResult } from '../types-DUbRb8qV.js';
3
+ export { D as DataWriteError, a as DataWriteErrorOptions, W as WriteOutcome, i as isDataWriteError } from '../write-outcome-aYToY3dK.js';
3
4
  import * as react from 'react';
4
5
  import { ReactNode } from 'react';
5
6
  import { S as StorageAdapter } from '../types-DKcxHXxq.js';
7
+ import '../form-types.js';
6
8
 
7
9
  interface MockProviderOptions {
8
10
  /** Simulated latency in ms (default 0). */
@@ -1,4 +1,5 @@
1
- export { createSupabaseProvider } from '../chunk-RKR5K7Q3.js';
1
+ export { createSupabaseProvider } from '../chunk-6AA4ISUA.js';
2
+ export { DataWriteError, isDataWriteError } from '../chunk-4W4W7F4I.js';
2
3
  export { DataProviderRoot, GenesisProvider, createMockProvider, useDataProvider, useStorage } from '../chunk-6V2TIN3G.js';
3
4
  import '../chunk-ZYJUEJ4Z.js';
4
5
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,24 @@
1
+ import { t as ResourceContract, I as Identifier, e as DataRow } from '../types-DUbRb8qV.js';
2
+ export { a as AuditEntry, F as FieldPermissions, h as FieldReference, i as FieldSchema, j as FilterOperator, q as ProviderCapabilities, r as RecordComment, s as ResourceAccess, u as ResourceRelation, v as ResourceSchema, w as ResourceSummary, x as RowAction, E as StorageType, V as VersionField } from '../types-DUbRb8qV.js';
3
+ export { D as DataWriteError, a as DataWriteErrorOptions, W as WriteOutcome, i as isDataWriteError } from '../write-outcome-aYToY3dK.js';
4
+ export { FieldType as WidgetHint } from '../form-types.js';
5
+
6
+ /** The key of a contract as a list, whichever form `key` was declared in. */
7
+ declare function keyFields(contract: Pick<ResourceContract, "key">): readonly string[];
8
+ /**
9
+ * One `Identifier` for a row.
10
+ *
11
+ * A single-field key returns the raw value, so `id`-keyed resources look
12
+ * exactly as they always have. A composite key returns a JSON array of the
13
+ * values in key order, e.g. `'["c-1","u-7"]'`.
14
+ */
15
+ declare function encodeRecordKey(contract: Pick<ResourceContract, "key">, row: DataRow): Identifier;
16
+ /**
17
+ * The inverse of {@link encodeRecordKey}: field name → value.
18
+ *
19
+ * Throws on an id that does not fit the key, rather than guessing, because a
20
+ * guessed key addresses the wrong row.
21
+ */
22
+ declare function decodeRecordKey(contract: Pick<ResourceContract, "key">, id: Identifier): Record<string, string | number>;
23
+
24
+ export { DataRow, ResourceContract, decodeRecordKey, encodeRecordKey, keyFields };
@@ -0,0 +1,4 @@
1
+ export { decodeRecordKey, encodeRecordKey, keyFields } from '../chunk-KV2HW4G4.js';
2
+ export { DataWriteError, isDataWriteError } from '../chunk-4W4W7F4I.js';
3
+ //# sourceMappingURL=index.js.map
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,516 @@
1
+ import { FieldType } from './form-types.js';
2
+
3
+ /**
4
+ * Portable storage type of a field — what kind of value the store holds,
5
+ * independent of how a UI should edit it. The exact native type, when the
6
+ * store reports one, is on {@link FieldSchema.dbType}.
7
+ *
8
+ * Exactly the six types the switchboard shipped as `FieldType`, so existing
9
+ * exhaustive maps over it keep compiling. Finer distinctions (date versus
10
+ * timestamp, enum, uuid) live in `dbType` and `widget`, not here.
11
+ */
12
+ type StorageType = "string" | "number" | "boolean" | "datetime" | "json" | "relation";
13
+ /** A canonical, provider-agnostic row. Keys are field names from the schema. */
14
+ type DataRow = Record<string, unknown>;
15
+ /**
16
+ * What the CURRENT user may do with one field. Reported by a provider's
17
+ * `describe()`; never inferred in the browser.
18
+ *
19
+ * `read: false` means the value is never sent to this user, so the column
20
+ * must not render at all (an answer key, a payout amount). `update: false`
21
+ * with `read: true` renders the column read-only.
22
+ */
23
+ interface FieldPermissions {
24
+ readonly read: boolean;
25
+ readonly update: boolean;
26
+ /** Why `update` (or `read`) is false, in one human sentence. */
27
+ readonly reason?: string;
28
+ }
29
+ /** A foreign-key style link from this field to another resource's field. */
30
+ interface FieldReference {
31
+ readonly resource: string;
32
+ readonly field: string;
33
+ }
34
+ interface FieldSchema {
35
+ /** Canonical field name in the resource's shape (provider-agnostic). */
36
+ readonly name: string;
37
+ /** Portable storage type. */
38
+ readonly type: StorageType;
39
+ /** True when a row may omit this field. */
40
+ readonly optional?: boolean;
41
+ /**
42
+ * True when the field holds an ORDERED LIST of `type` values (e.g. a
43
+ * multi-relation like `deps`/`serves`). Promotion emits a Postgres ARRAY
44
+ * column (`<base>[]`) instead of a lossy scalar, so the cardinality survives
45
+ * L1→L3 (CMT-1742-014).
46
+ */
47
+ readonly array?: boolean;
48
+ /** Human note (surfaces in describe()/drift reports). */
49
+ readonly description?: string;
50
+ /** Human label, e.g. "Date of birth". Absent means a UI derives one from `name`. */
51
+ readonly label?: string;
52
+ /** The store's exact type, e.g. `timestamp with time zone` or `public.status`. */
53
+ readonly dbType?: string;
54
+ /** Which control fits this field. A hint: a UI may still choose another. */
55
+ readonly widget?: FieldType;
56
+ /** True when the store accepts null for this field. */
57
+ readonly nullable?: boolean;
58
+ /** True when the store fills the field itself on insert. */
59
+ readonly hasDefault?: boolean;
60
+ /** True when the store computes the field; it is never written. */
61
+ readonly generated?: boolean;
62
+ /** Allowed values, for enum-like fields. */
63
+ readonly enumValues?: readonly string[];
64
+ /** The resource and field this one points at, when it is a link. */
65
+ readonly references?: FieldReference;
66
+ /** The current user's rights on this field, when a provider reports them. */
67
+ readonly permissions?: FieldPermissions;
68
+ }
69
+ /**
70
+ * One declaration of a resource: its fields and its identity.
71
+ *
72
+ * `key` names the identity field, or several for a composite key (a junction
73
+ * table keyed on `(coach_id, client_id)`). Use {@link keyFields} to read it as
74
+ * a list without caring which form it took.
75
+ */
76
+ interface ResourceContract<Row extends DataRow = DataRow> {
77
+ /** Resource name the app addresses, e.g. "tickets" or "public.tickets". */
78
+ readonly name: string;
79
+ /**
80
+ * Identity field name, or names for a composite key, in key order.
81
+ *
82
+ * Typed `string` (not `keyof Row & string`) on purpose: `keyof Row` is a
83
+ * dependent type that TypeScript checks contravariantly, which would make
84
+ * `ResourceContract<WorkCycleRow>` non-assignable to the erased
85
+ * `ResourceContract<DataRow>` that generic consumers accept. The key is
86
+ * validated against the field schema at runtime instead.
87
+ */
88
+ readonly key: string | readonly string[];
89
+ readonly fields: readonly FieldSchema[];
90
+ /** Optional doc string. */
91
+ readonly description?: string;
92
+ /** Phantom marker so `Row` is inferable from a contract. */
93
+ readonly __row?: Row;
94
+ }
95
+ /** What the current user may do with a whole resource. */
96
+ interface ResourceAccess {
97
+ readonly read: boolean;
98
+ /** True when at least one field can be updated on rows this user can see. */
99
+ readonly update: boolean;
100
+ /** Why `update` (or `read`) is false, in one human sentence. */
101
+ readonly reason?: string;
102
+ /** True when this user may add rows. Absent means the provider doesn't say. */
103
+ readonly create?: boolean;
104
+ /** True when this user may delete rows they can see. */
105
+ readonly delete?: boolean;
106
+ /** True when this user may read a row's audit history. */
107
+ readonly history?: boolean;
108
+ /** True when this user may read and add comments on rows they can see. */
109
+ readonly comment?: boolean;
110
+ /** Why `create`, `delete`, `history` or `comment` is false, in one human sentence each. */
111
+ readonly reasons?: {
112
+ readonly create?: string;
113
+ readonly delete?: string;
114
+ readonly history?: string;
115
+ readonly comment?: string;
116
+ };
117
+ }
118
+ /**
119
+ * How another resource relates to this one, from the store's foreign keys.
120
+ *
121
+ * - `one`: a field on THIS resource points at one row of `resource`.
122
+ * - `many`: rows of `resource` point at this row through their `field`.
123
+ * - `through`: many rows of `resource`, linked by the junction resource in
124
+ * `through` (a table whose key is two foreign keys).
125
+ */
126
+ interface ResourceRelation {
127
+ readonly kind: "one" | "many" | "through";
128
+ /** The related resource. */
129
+ readonly resource: string;
130
+ /**
131
+ * `one`: the field on this resource. `many`: the field on `resource` that
132
+ * holds this row's key. `through`: the junction's field holding this row's key.
133
+ */
134
+ readonly field: string;
135
+ /** For `through`: the junction resource, and its field pointing at `resource`. */
136
+ readonly through?: {
137
+ readonly resource: string;
138
+ readonly otherField: string;
139
+ };
140
+ /** True when at most one related row can exist (a unique foreign key). */
141
+ readonly unique?: boolean;
142
+ }
143
+ /**
144
+ * The column a provider checks on every update so an edit never silently
145
+ * overwrites someone else's. Only reported when the store itself advances it
146
+ * on every successful update (a trigger such as `moddatetime`), because a
147
+ * column nothing advances detects nothing.
148
+ */
149
+ interface VersionField {
150
+ /**
151
+ * The field holding the version. A provider with versioned writes returns
152
+ * each row with its exact version token in this field, as a string, and
153
+ * `expectedVersion` echoes `String(row[field])` back unchanged.
154
+ */
155
+ readonly field: string;
156
+ /**
157
+ * `row`: no column tracks versions, so the token is a fingerprint of the
158
+ * row's values, and `field` is a name the provider adds to each record
159
+ * (it is not one of the resource's fields).
160
+ */
161
+ readonly kind: "integer" | "timestamp" | "row";
162
+ }
163
+ /**
164
+ * A resource as `describe()` reports it for the CURRENT user: the contract,
165
+ * plus what this user may do. Fields the user may not read are left out
166
+ * entirely, not marked hidden.
167
+ */
168
+ interface ResourceSchema<Row extends DataRow = DataRow> extends ResourceContract<Row> {
169
+ /** Human label, e.g. "Clients". */
170
+ readonly label?: string;
171
+ readonly access: ResourceAccess;
172
+ /** Present only when writes can be version-checked. */
173
+ readonly version?: VersionField;
174
+ /** The field that best names a row to a person (a name or title), for links and pickers. */
175
+ readonly labelField?: string;
176
+ /** Resources this one links to or is linked from. Only ones this user may see. */
177
+ readonly relations?: readonly ResourceRelation[];
178
+ /** Fields the user may set when creating a row. Absent when creating isn't allowed. */
179
+ readonly createFields?: readonly string[];
180
+ /** Row actions this user may run: each calls the app's own function on one row. */
181
+ readonly actions?: readonly RowAction[];
182
+ }
183
+ /** A named operation on one row, carried out by the app's own database function. */
184
+ interface RowAction {
185
+ readonly name: string;
186
+ readonly label: string;
187
+ /** Ask before running, with this question. */
188
+ readonly confirm?: string;
189
+ /** One sentence on what it does. */
190
+ readonly description?: string;
191
+ }
192
+ /** A comment on a record. */
193
+ interface RecordComment {
194
+ readonly id: string;
195
+ /** The signed-in user who wrote it (the token's `sub`). */
196
+ readonly author: string;
197
+ readonly body: string;
198
+ /** ISO 8601, as the store recorded it. */
199
+ readonly createdAt: string;
200
+ }
201
+ /** One entry of `listResources()`: enough to draw a sidebar. */
202
+ interface ResourceSummary {
203
+ readonly name: string;
204
+ readonly label?: string;
205
+ readonly description?: string;
206
+ readonly access: ResourceAccess;
207
+ }
208
+ /** Filter operators a provider may support. Mirrors `FilterParam["operator"]`. */
209
+ type FilterOperator = "eq" | "neq" | "gt" | "lt" | "gte" | "lte" | "contains" | "in" | "between" | "is_null" | "is_not_null";
210
+ /**
211
+ * What a provider can do. A UI hides any control the provider cannot serve,
212
+ * instead of letting it error (DATA-STUDIO.md decision 9).
213
+ */
214
+ interface ProviderCapabilities {
215
+ /** Server-side text search across fields. */
216
+ readonly search: boolean;
217
+ /** Server-side sort. */
218
+ readonly sort: boolean;
219
+ /** Server-side filter operators. Empty means no server filtering. */
220
+ readonly filterOperators: readonly FilterOperator[];
221
+ /** `updateMany` / `deleteMany` in one transaction. */
222
+ readonly bulkWrites: boolean;
223
+ /** Updates honour `expectedVersion` and reject stale writes. */
224
+ readonly versionedWrites: boolean;
225
+ /** Records may be keyed by more than one field. */
226
+ readonly compositeKeys: boolean;
227
+ /** Server-side sort by several fields at once (`ListParams.sorts`). */
228
+ readonly multiSort?: boolean;
229
+ /** `create` and `deleteOne` are served, subject to each resource's access. */
230
+ readonly create?: boolean;
231
+ readonly delete?: boolean;
232
+ /** `history` is served, subject to each resource's access. */
233
+ readonly history?: boolean;
234
+ /** `importRows` is served, with a dry run. */
235
+ readonly import?: boolean;
236
+ /** `undo` is served. */
237
+ readonly undo?: boolean;
238
+ /** `runAction` is served. */
239
+ readonly actions?: boolean;
240
+ /** `comments` and `addComment` are served. */
241
+ readonly comments?: boolean;
242
+ }
243
+ /** One audit record, as committed alongside the edit it describes. */
244
+ interface AuditEntry {
245
+ readonly id: string;
246
+ /** ISO 8601 timestamp, as the store recorded it. */
247
+ readonly occurredAt: string;
248
+ /** The signed-in user who made the edit (the token's `sub`). */
249
+ readonly actor: string | null;
250
+ readonly resource: string;
251
+ /** The record's key, field name → value. */
252
+ readonly key: DataRow;
253
+ /** `run` is a row action; `rowAction` names it. */
254
+ readonly action: "create" | "update" | "delete" | "run";
255
+ /** For `run`: which row action. */
256
+ readonly rowAction?: string;
257
+ /** Values of the changed fields before the edit. */
258
+ readonly before: DataRow | null;
259
+ /** Values of the changed fields after the edit. */
260
+ readonly after: DataRow | null;
261
+ }
262
+
263
+ /** Record identifier — supports both string UUIDs and numeric auto-increment IDs. */
264
+ type Identifier = string | number;
265
+ /** Base record type — all resources must have an id. */
266
+ type BaseRecord = {
267
+ id: Identifier;
268
+ [key: string]: unknown;
269
+ };
270
+ /** Pagination parameters (offset-based, with optional cursor for future use). */
271
+ interface PaginationParam {
272
+ page: number;
273
+ perPage: number;
274
+ cursor?: string;
275
+ }
276
+ /** Sort direction for a single field. */
277
+ interface SortParam {
278
+ field: string;
279
+ order: "asc" | "desc";
280
+ }
281
+ /**
282
+ * Filter expressions — discriminated union by operator.
283
+ * Filters are AND-ed together in queries.
284
+ */
285
+ /** Operators that compare a field against a single scalar value. */
286
+ interface ScalarFilter {
287
+ field: string;
288
+ operator: "eq" | "neq" | "gt" | "lt" | "gte" | "lte" | "contains";
289
+ value: string | number | boolean;
290
+ }
291
+ /** Operator that checks field membership in a list. */
292
+ interface ArrayFilter {
293
+ field: string;
294
+ operator: "in";
295
+ value: (string | number | boolean)[];
296
+ }
297
+ /** Operator that checks field is within a range. */
298
+ interface RangeFilter {
299
+ field: string;
300
+ operator: "between";
301
+ value: [string | number, string | number];
302
+ }
303
+ /** Operators that check for null/non-null — no value needed. */
304
+ interface NullFilter {
305
+ field: string;
306
+ operator: "is_null" | "is_not_null";
307
+ }
308
+ type FilterParam = ScalarFilter | ArrayFilter | RangeFilter | NullFilter;
309
+ /** Parameters for `getList`. */
310
+ interface ListParams {
311
+ pagination?: PaginationParam;
312
+ sort?: SortParam;
313
+ /**
314
+ * Sort by several fields, first one first. Wins over `sort` on a provider
315
+ * whose capabilities report `multiSort`; others use only `sort`.
316
+ */
317
+ sorts?: SortParam[];
318
+ filters?: FilterParam[];
319
+ /** Supabase select syntax, e.g. "*, author:profiles(name)". Mock provider ignores this. */
320
+ select?: string;
321
+ /** Multi-field text search (case-insensitive). */
322
+ search?: {
323
+ query: string;
324
+ fields: string[];
325
+ };
326
+ }
327
+ /** Result of `getList`. */
328
+ interface ListResult<T> {
329
+ data: T[];
330
+ total: number;
331
+ }
332
+ /** Parameters for `getOne`. */
333
+ interface GetOneParams {
334
+ id: Identifier;
335
+ /** Supabase select syntax, e.g. "*, author:profiles(name)". Defaults to "*". */
336
+ select?: string;
337
+ }
338
+ /** Result of `getOne`. */
339
+ interface GetOneResult<T> {
340
+ data: T;
341
+ }
342
+ /** Parameters for `getMany`. */
343
+ interface GetManyParams {
344
+ ids: Identifier[];
345
+ }
346
+ /** Result of `getMany`. */
347
+ interface GetManyResult<T> {
348
+ data: T[];
349
+ }
350
+ /** Parameters for `create`. */
351
+ interface CreateParams<T> {
352
+ data: Partial<T>;
353
+ }
354
+ /** Result of `create`. */
355
+ interface CreateResult<T> {
356
+ data: T;
357
+ /** The audit entry committed with the new row, from a provider that audits. */
358
+ audit?: AuditEntry;
359
+ }
360
+ /** Parameters for `update`. */
361
+ interface UpdateParams<T> {
362
+ /**
363
+ * The record's identifier. For a composite key, the canonical string from
364
+ * `encodeRecordKey` (`@marktiderman/genesis-core/schema`).
365
+ */
366
+ id: Identifier;
367
+ data: Partial<T>;
368
+ /**
369
+ * The version token the caller read the row at. A provider that reports
370
+ * `versionedWrites` in its capabilities rejects the update with a
371
+ * `DataWriteError` (`conflict`) when the row has moved on since, instead of
372
+ * overwriting someone else's edit. Providers without versioned writes
373
+ * ignore it.
374
+ */
375
+ expectedVersion?: string | null;
376
+ }
377
+ /** Result of `update`. */
378
+ interface UpdateResult<T> {
379
+ data: T;
380
+ /** The row's new version token, from a provider with versioned writes. */
381
+ version?: string | null;
382
+ /** The audit entry committed with the edit, from a provider that audits. */
383
+ audit?: AuditEntry;
384
+ }
385
+ /** Parameters for `deleteOne`. */
386
+ interface DeleteParams {
387
+ id: Identifier;
388
+ /** The version token the row was read at; see `UpdateParams.expectedVersion`. */
389
+ expectedVersion?: string | null;
390
+ }
391
+ /** Result of `deleteOne`. */
392
+ interface DeleteResult {
393
+ data: {
394
+ id: Identifier;
395
+ };
396
+ /** The audit entry committed with the deletion, from a provider that audits. */
397
+ audit?: AuditEntry;
398
+ }
399
+ /** One row's change in a bulk update. */
400
+ interface BulkChange {
401
+ id: Identifier;
402
+ data: Record<string, unknown>;
403
+ /** The version token the row was read at. */
404
+ expectedVersion?: string | null;
405
+ }
406
+ /** Why one row of a bulk write or import didn't go through. */
407
+ interface RowFailure {
408
+ /** The row's position in the request, from 0. */
409
+ index: number;
410
+ id?: Identifier;
411
+ outcome: string;
412
+ message: string;
413
+ }
414
+ /** Result of a bulk write: every row changed, or none did. */
415
+ interface BulkResult {
416
+ /** Rows changed; always the number requested when `ok`. */
417
+ count: number;
418
+ ok: boolean;
419
+ /** Present when `ok` is false: every row that failed, so the caller can fix them all at once. */
420
+ failures?: RowFailure[];
421
+ /** One audit entry per row changed. */
422
+ audit?: AuditEntry[];
423
+ }
424
+ /** Parameters for `importRows`. */
425
+ interface ImportParams {
426
+ rows: Record<string, unknown>[];
427
+ /** Check every row against the database and change nothing. */
428
+ dryRun: boolean;
429
+ }
430
+ /** Parameters for `history`. */
431
+ interface HistoryParams {
432
+ id: Identifier;
433
+ /** Most recent first; default and maximum are the provider's. */
434
+ limit?: number;
435
+ }
436
+ interface DataProvider {
437
+ getList<T extends BaseRecord = BaseRecord>(resource: string, params: ListParams): Promise<ListResult<T>>;
438
+ getOne<T extends BaseRecord = BaseRecord>(resource: string, params: GetOneParams): Promise<GetOneResult<T>>;
439
+ getMany<T extends BaseRecord = BaseRecord>(resource: string, params: GetManyParams): Promise<GetManyResult<T>>;
440
+ create<T extends BaseRecord = BaseRecord>(resource: string, params: CreateParams<T>): Promise<CreateResult<T>>;
441
+ update<T extends BaseRecord = BaseRecord>(resource: string, params: UpdateParams<T>): Promise<UpdateResult<T>>;
442
+ deleteOne(resource: string, params: DeleteParams): Promise<DeleteResult>;
443
+ /**
444
+ * The resource's real shape for the CURRENT user: fields, key, version
445
+ * field, and per-field read and update rights. Fields the user may not read
446
+ * are omitted entirely.
447
+ */
448
+ describe?(resource: string): Promise<ResourceSchema>;
449
+ /** Every resource this user may see, for a sidebar or a picker. */
450
+ listResources?(): Promise<ResourceSummary[]>;
451
+ /**
452
+ * What this provider can do, optionally for one resource. A UI hides the
453
+ * controls a provider cannot serve instead of letting them error.
454
+ */
455
+ capabilities?(resource?: string): Promise<ProviderCapabilities>;
456
+ /**
457
+ * A record's audit history, most recent first, limited to the fields this
458
+ * user may read.
459
+ */
460
+ history?(resource: string, params: HistoryParams): Promise<AuditEntry[]>;
461
+ /**
462
+ * Update many rows in ONE transaction. All of them change or none do: a
463
+ * single conflict, refusal or invalid value rolls back the rest, and the
464
+ * result lists every row that failed.
465
+ */
466
+ updateMany?(resource: string, params: {
467
+ changes: BulkChange[];
468
+ }): Promise<BulkResult>;
469
+ /** Delete many rows in one transaction, all or none, each at its version. */
470
+ deleteMany?(resource: string, params: {
471
+ items: {
472
+ id: Identifier;
473
+ expectedVersion?: string | null;
474
+ }[];
475
+ }): Promise<BulkResult>;
476
+ /**
477
+ * Add rows from an import. With `dryRun`, every row is tried against the
478
+ * database and nothing is kept, so the result lists every row that would
479
+ * fail; without it, all rows are added in one transaction or none are.
480
+ */
481
+ importRows?(resource: string, params: ImportParams): Promise<BulkResult>;
482
+ /**
483
+ * Undo one audited change with a compensating write: put the changed
484
+ * fields back, remove an added row, or restore a deleted one. Refused when
485
+ * the row has changed since.
486
+ */
487
+ undo?(resource: string, params: {
488
+ auditId: string;
489
+ }): Promise<{
490
+ audit?: AuditEntry;
491
+ }>;
492
+ /**
493
+ * Run one of the resource's row actions (`ResourceSchema.actions`) on a
494
+ * row, at the version it was read. The app's function does the work; the
495
+ * row's change is audited.
496
+ */
497
+ runAction?(resource: string, params: {
498
+ id: Identifier;
499
+ action: string;
500
+ expectedVersion?: string | null;
501
+ }): Promise<{
502
+ data: Record<string, unknown> | null;
503
+ audit?: AuditEntry;
504
+ }>;
505
+ /** A record's comments, oldest first. */
506
+ comments?(resource: string, params: {
507
+ id: Identifier;
508
+ }): Promise<RecordComment[]>;
509
+ /** Add a comment to a record, as the signed-in user. */
510
+ addComment?(resource: string, params: {
511
+ id: Identifier;
512
+ body: string;
513
+ }): Promise<RecordComment>;
514
+ }
515
+
516
+ export type { ArrayFilter as A, BaseRecord as B, CreateParams as C, DataProvider as D, StorageType as E, FieldPermissions as F, GetManyParams as G, HistoryParams as H, Identifier as I, UpdateResult as J, ListParams as L, NullFilter as N, PaginationParam as P, RangeFilter as R, ScalarFilter as S, UpdateParams as U, VersionField as V, AuditEntry as a, BulkChange as b, BulkResult as c, CreateResult as d, DataRow as e, DeleteParams as f, DeleteResult as g, FieldReference as h, FieldSchema as i, FilterOperator as j, FilterParam as k, GetManyResult as l, GetOneParams as m, GetOneResult as n, ImportParams as o, ListResult as p, ProviderCapabilities as q, RecordComment as r, ResourceAccess as s, ResourceContract as t, ResourceRelation as u, ResourceSchema as v, ResourceSummary as w, RowAction as x, RowFailure as y, SortParam as z };
@@ -0,0 +1,38 @@
1
+ import { e as DataRow } from './types-DUbRb8qV.js';
2
+
3
+ /**
4
+ * - `conflict`: someone else changed the row since it was read. `current`
5
+ * carries the row as it is now.
6
+ * - `gone`: the row was deleted, or is no longer visible to this user. The
7
+ * two cannot be told apart without a privileged look at the table, which
8
+ * would show this user a fact about a row they cannot see.
9
+ * - `forbidden`: the row is visible and unchanged, but the app or the
10
+ * database does not let this user make this change. Only reported when
11
+ * that is proven, never assumed from "zero rows changed".
12
+ * - `invalid`: the store rejected the value (type, constraint, not-null).
13
+ * - `audit_failed`: the audit entry could not be written, so the edit was
14
+ * rolled back with it.
15
+ * - `unauthenticated`: no signed-in user, or the session expired.
16
+ * - `error`: anything else. The edit did not commit.
17
+ */
18
+ type WriteOutcome = "conflict" | "gone" | "forbidden" | "invalid" | "audit_failed" | "unauthenticated" | "error";
19
+ interface DataWriteErrorOptions {
20
+ /** The row as it is now, for a `conflict`. Only fields the user may read. */
21
+ current?: DataRow;
22
+ /** The row's current version token, for a `conflict`. */
23
+ currentVersion?: string | null;
24
+ }
25
+ /** Thrown by a provider when a write did not commit. */
26
+ declare class DataWriteError extends Error {
27
+ readonly outcome: WriteOutcome;
28
+ readonly current?: DataRow;
29
+ readonly currentVersion?: string | null;
30
+ constructor(outcome: WriteOutcome, message: string, options?: DataWriteErrorOptions);
31
+ }
32
+ /**
33
+ * `instanceof` fails when two copies of this package are installed, so check
34
+ * the shape instead.
35
+ */
36
+ declare function isDataWriteError(error: unknown): error is DataWriteError;
37
+
38
+ export { DataWriteError as D, type WriteOutcome as W, type DataWriteErrorOptions as a, isDataWriteError as i };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marktiderman/genesis-core",
3
- "version": "1.1.2",
3
+ "version": "1.3.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -38,16 +38,16 @@
38
38
  "./storage": {
39
39
  "types": "./dist/storage/index.d.ts",
40
40
  "import": "./dist/storage/index.js"
41
+ },
42
+ "./schema": {
43
+ "types": "./dist/schema/index.d.ts",
44
+ "import": "./dist/schema/index.js"
41
45
  }
42
46
  },
43
47
  "files": [
44
48
  "dist",
45
49
  "README.md"
46
50
  ],
47
- "scripts": {
48
- "build": "tsup",
49
- "clean": "rm -rf dist"
50
- },
51
51
  "peerDependencies": {
52
52
  "react": "^19.0.0",
53
53
  "@tanstack/react-query": "^5.0.0",
@@ -56,12 +56,19 @@
56
56
  "@hookform/resolvers": "^3.0.0 || ^4.0.0 || ^5.0.0"
57
57
  },
58
58
  "devDependencies": {
59
+ "@hookform/resolvers": "^5.0.1",
59
60
  "@tanstack/react-query": "^5.75.0",
60
61
  "@types/react": "^19.2.14",
61
62
  "react-hook-form": "^7.56.4",
62
- "zod": "^3.25.67",
63
- "@hookform/resolvers": "^5.0.1",
64
63
  "tsup": "^8.3.0",
65
- "typescript": "^5.9.3"
64
+ "typescript": "^5.9.3",
65
+ "vitest": "^2.1.0",
66
+ "zod": "^3.25.67"
67
+ },
68
+ "scripts": {
69
+ "build": "tsup && node ../../scripts/check-exports-resolve.mjs",
70
+ "clean": "rm -rf dist",
71
+ "typecheck": "tsc --noEmit",
72
+ "test": "vitest run"
66
73
  }
67
- }
74
+ }
@@ -1,119 +0,0 @@
1
- /** Record identifier — supports both string UUIDs and numeric auto-increment IDs. */
2
- type Identifier = string | number;
3
- /** Base record type — all resources must have an id. */
4
- type BaseRecord = {
5
- id: Identifier;
6
- [key: string]: unknown;
7
- };
8
- /** Pagination parameters (offset-based, with optional cursor for future use). */
9
- interface PaginationParam {
10
- page: number;
11
- perPage: number;
12
- cursor?: string;
13
- }
14
- /** Sort direction for a single field. */
15
- interface SortParam {
16
- field: string;
17
- order: "asc" | "desc";
18
- }
19
- /**
20
- * Filter expressions — discriminated union by operator.
21
- * Filters are AND-ed together in queries.
22
- */
23
- /** Operators that compare a field against a single scalar value. */
24
- interface ScalarFilter {
25
- field: string;
26
- operator: "eq" | "neq" | "gt" | "lt" | "gte" | "lte" | "contains";
27
- value: string | number | boolean;
28
- }
29
- /** Operator that checks field membership in a list. */
30
- interface ArrayFilter {
31
- field: string;
32
- operator: "in";
33
- value: (string | number | boolean)[];
34
- }
35
- /** Operator that checks field is within a range. */
36
- interface RangeFilter {
37
- field: string;
38
- operator: "between";
39
- value: [string | number, string | number];
40
- }
41
- /** Operators that check for null/non-null — no value needed. */
42
- interface NullFilter {
43
- field: string;
44
- operator: "is_null" | "is_not_null";
45
- }
46
- type FilterParam = ScalarFilter | ArrayFilter | RangeFilter | NullFilter;
47
- /** Parameters for `getList`. */
48
- interface ListParams {
49
- pagination?: PaginationParam;
50
- sort?: SortParam;
51
- filters?: FilterParam[];
52
- /** Supabase select syntax, e.g. "*, author:profiles(name)". Mock provider ignores this. */
53
- select?: string;
54
- /** Multi-field text search (case-insensitive). */
55
- search?: {
56
- query: string;
57
- fields: string[];
58
- };
59
- }
60
- /** Result of `getList`. */
61
- interface ListResult<T> {
62
- data: T[];
63
- total: number;
64
- }
65
- /** Parameters for `getOne`. */
66
- interface GetOneParams {
67
- id: Identifier;
68
- /** Supabase select syntax, e.g. "*, author:profiles(name)". Defaults to "*". */
69
- select?: string;
70
- }
71
- /** Result of `getOne`. */
72
- interface GetOneResult<T> {
73
- data: T;
74
- }
75
- /** Parameters for `getMany`. */
76
- interface GetManyParams {
77
- ids: Identifier[];
78
- }
79
- /** Result of `getMany`. */
80
- interface GetManyResult<T> {
81
- data: T[];
82
- }
83
- /** Parameters for `create`. */
84
- interface CreateParams<T> {
85
- data: Partial<T>;
86
- }
87
- /** Result of `create`. */
88
- interface CreateResult<T> {
89
- data: T;
90
- }
91
- /** Parameters for `update`. */
92
- interface UpdateParams<T> {
93
- id: Identifier;
94
- data: Partial<T>;
95
- }
96
- /** Result of `update`. */
97
- interface UpdateResult<T> {
98
- data: T;
99
- }
100
- /** Parameters for `deleteOne`. */
101
- interface DeleteParams {
102
- id: Identifier;
103
- }
104
- /** Result of `deleteOne`. */
105
- interface DeleteResult {
106
- data: {
107
- id: Identifier;
108
- };
109
- }
110
- interface DataProvider {
111
- getList<T extends BaseRecord = BaseRecord>(resource: string, params: ListParams): Promise<ListResult<T>>;
112
- getOne<T extends BaseRecord = BaseRecord>(resource: string, params: GetOneParams): Promise<GetOneResult<T>>;
113
- getMany<T extends BaseRecord = BaseRecord>(resource: string, params: GetManyParams): Promise<GetManyResult<T>>;
114
- create<T extends BaseRecord = BaseRecord>(resource: string, params: CreateParams<T>): Promise<CreateResult<T>>;
115
- update<T extends BaseRecord = BaseRecord>(resource: string, params: UpdateParams<T>): Promise<UpdateResult<T>>;
116
- deleteOne(resource: string, params: DeleteParams): Promise<DeleteResult>;
117
- }
118
-
119
- export type { ArrayFilter as A, BaseRecord as B, CreateParams as C, DataProvider as D, FilterParam as F, GetManyParams as G, Identifier as I, ListParams as L, NullFilter as N, PaginationParam as P, RangeFilter as R, ScalarFilter as S, UpdateParams as U, CreateResult as a, DeleteParams as b, DeleteResult as c, GetManyResult as d, GetOneParams as e, GetOneResult as f, ListResult as g, SortParam as h, UpdateResult as i };