clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/prf-provision-custody
lofi / package / schema / nested_test.ts
nested_test.ts 7.5K
1// The nested-namespace naming layer, exercised without an engine: name2// mangling, definition validation, ref-target rewriting, the flattened3// runtime registry, and the per-namespace permissions surface. Engine4// round-trips live in ./nested_conformance_test.ts.5import { assert } from "../runtime/test-assert.ts";6import {7 flattenNestedSchema,8 NESTED_SEPARATOR,9 nestedAppDeployTarget,10 nestedAppTables,11 s,12} from "./mod.ts";1314function throws(operation: () => unknown, fragment: string, label: string): void {15 try {16 operation();17 } catch (error) {18 const message = error instanceof Error ? error.message : String(error);19 assert(20 message.includes(fragment),21 `${label}: threw "${message}", expected it to mention "${fragment}"`,22 );23 return;24 }25 throw new Error(`${label}: expected a throw mentioning "${fragment}"`);26}2728function tableName(handle: unknown): string {29 return (handle as { _table: string })._table;30}3132const root = s.defineNestedApp({33 taskapp: {34 projects: s.table({ name: s.string() }),35 tasks: s.table({ projectId: s.ref("projects"), text: s.string() }),36 },37 notesapp: {38 notes: s.table({ title: s.string() }),39 },40});4142Deno.test("defineNestedApp flattens to mangled global names behind unprefixed handles", () => {43 assert(44 tableName(root.taskapp.tasks) === `taskapp${NESTED_SEPARATOR}tasks`,45 `tasks handle targets ${tableName(root.taskapp.tasks)}`,46 );47 assert(48 tableName(root.notesapp.notes) === `notesapp${NESTED_SEPARATOR}notes`,49 `notes handle targets ${tableName(root.notesapp.notes)}`,50 );51 assert(52 typeof (root.taskapp.tasks as { where?: unknown }).where === "function",53 "namespace handles are not queryable",54 );55 assert(56 !("taskapp__tasks" in root.taskapp),57 "mangled names leaked into the namespace object",58 );59});6061Deno.test("nested table handles are constructed once and shared with the registry", () => {62 const registry = nestedAppTables(root);63 assert(registry !== null, "nested root exposes no table registry");64 assert(registry.length === 3, `registry holds ${registry.length} handles, expected 3`);65 for (const handle of [root.taskapp.projects, root.taskapp.tasks, root.notesapp.notes]) {66 assert(67 registry.includes(handle as object),68 `registry is missing the ${tableName(handle)} handle by identity`,69 );70 }71});7273Deno.test("nestedAppTables returns null for flat apps so the runtime keeps its one-level walk", () => {74 const flat = s.defineApp({ tasks: s.table({ text: s.string() }) });75 assert(nestedAppTables(flat) === null, "flat app misdetected as nested");76 assert(nestedAppTables(undefined) === null, "undefined misdetected as nested");77});7879Deno.test("the deploy target exposes every table under its mangled global name", () => {80 const target = nestedAppDeployTarget(root) as Record<string, unknown>;81 for (const name of ["taskapp__projects", "taskapp__tasks", "notesapp__notes"]) {82 assert(name in target, `deploy target is missing "${name}"`);83 }84 assert("wasmSchema" in target, "deploy target is missing wasmSchema");85 throws(86 () => nestedAppDeployTarget(root.taskapp),87 "requires a root value",88 "deploy target on a namespace",89 );90});9192Deno.test("nested definitions reject names that break the mangling scheme", () => {93 const table = s.table({ name: s.string() });94 throws(95 () => flattenNestedSchema({ ["bad__ns"]: { items: table } }),96 "reserved separator",97 "separator in namespace",98 );99 throws(100 () => flattenNestedSchema({ ns: { ["bad__table"]: table } }),101 "reserved separator",102 "separator in table",103 );104 throws(105 () => flattenNestedSchema({ ["bad.ns"]: { items: table } }),106 `must not contain "."`,107 "dot in namespace",108 );109 throws(() => flattenNestedSchema({}), "at least one namespace", "empty definition");110 throws(() => flattenNestedSchema({ ns: {} }), "at least one table", "empty namespace");111});112113Deno.test("ref targets resolve within the namespace or as <namespace>.<table> across namespaces", () => {114 const flat = flattenNestedSchema({115 taskapp: {116 projects: s.table({ name: s.string() }),117 tasks: s.table({ projectId: s.ref("projects") }),118 },119 notesapp: {120 notes: s.table({ taskId: s.ref("taskapp.tasks").optional() }),121 },122 }) as Record<string, { columns: Record<string, { _references?: string }> }>;123 assert(124 flat.taskapp__tasks.columns.projectId._references === "taskapp__projects",125 `same-namespace ref rewrote to ${flat.taskapp__tasks.columns.projectId._references}`,126 );127 assert(128 flat.notesapp__notes.columns.taskId._references === "taskapp__tasks",129 `cross-namespace ref rewrote to ${flat.notesapp__notes.columns.taskId._references}`,130 );131132 throws(133 () =>134 flattenNestedSchema({135 ns: { items: s.table({ ownerId: s.ref("missing") }) },136 }),137 "is not a table in this namespace",138 "unknown bare ref target",139 );140 throws(141 () =>142 flattenNestedSchema({143 ns: { items: s.table({ ownerId: s.ref("other.missing") }) },144 }),145 "does not name a declared",146 "unknown qualified ref target",147 );148 throws(149 () =>150 flattenNestedSchema({151 ns: { items: s.table({ ownerId: s.ref("ns__items") }) },152 }),153 "reserved separator",154 "mangled ref target",155 );156});157158Deno.test("ref rewriting clones builders instead of mutating the author's definition", () => {159 const projectRef = s.ref("projects");160 flattenNestedSchema({161 taskapp: {162 projects: s.table({ name: s.string() }),163 tasks: s.table({ projectId: projectRef }),164 },165 });166 assert(167 (projectRef as unknown as { _references?: string })._references === "projects",168 "flattening mutated the shared ref builder",169 );170});171172Deno.test("defineNestedPermissions exposes unprefixed policy names and compiles mangled keys", () => {173 let observedPolicyKeys: string[] = [];174 const compiled = s.defineNestedPermissions(root.taskapp, ({ policy }) => {175 observedPolicyKeys = Object.keys(policy);176 policy.tasks.allowRead.always();177 policy.tasks.allowInsert.always();178 policy.projects.allowRead.always();179 });180 assert(181 observedPolicyKeys.includes("tasks") && observedPolicyKeys.includes("projects"),182 `policy context exposes ${observedPolicyKeys.join(", ")}`,183 );184 assert(185 !observedPolicyKeys.includes("notes") && !observedPolicyKeys.includes("taskapp__tasks"),186 "policy context leaked other namespaces or mangled names",187 );188 const compiledKeys = Object.keys(compiled).sort();189 assert(190 compiledKeys.join(",") === "taskapp__projects,taskapp__tasks",191 `compiled bundle keyed by ${compiledKeys.join(", ")}`,192 );193});194195Deno.test("defineNestedPermissions rejects values that are not nested namespaces", () => {196 const flat = s.defineApp({ tasks: s.table({ text: s.string() }) });197 throws(198 () => s.defineNestedPermissions(flat, () => {}),199 "requires a namespace value",200 "flat app as namespace",201 );202});203204Deno.test("per-namespace permission bundles merge collision-free; duplicates throw", () => {205 const taskBundle = s.defineNestedPermissions(root.taskapp, ({ policy }) => {206 policy.tasks.allowRead.always();207 });208 const noteBundle = s.defineNestedPermissions(root.notesapp, ({ policy }) => {209 policy.notes.allowRead.always();210 });211 const merged = s.mergeNestedPermissions(taskBundle, noteBundle);212 const keys = Object.keys(merged).sort();213 assert(214 keys.join(",") === "notesapp__notes,taskapp__tasks",215 `merged bundle keyed by ${keys.join(", ")}`,216 );217 throws(218 () => s.mergeNestedPermissions(taskBundle, taskBundle),219 "more than once",220 "duplicate bundle merge",221 );222});