{"_id":"@b2m9/zod-views","name":"@b2m9/zod-views","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@b2m9/zod-views","version":"0.1.0","description":"Derive safe create, update, and read schemas from one Zod object.","keywords":["api","patch","schema","typescript","validation","views","zod"],"homepage":"https://github.com/b2m9/zod-views#readme","bugs":{"url":"https://github.com/b2m9/zod-views/issues"},"license":"MIT","author":{"name":"Bob Massarczyk"},"repository":{"type":"git","url":"git+https://github.com/b2m9/zod-views.git"},"type":"module","sideEffects":false,"exports":{".":"./dist/index.mjs","./package.json":"./package.json"},"publishConfig":{"access":"public"},"scripts":{"build":"vp pack","dev":"vp pack --watch","test":"vp test run","check":"vp check","check:exports":"vp pack && vp dlx publint@0.3.22 && vp dlx @arethetypeswrong/cli@0.18.5 --pack --profile esm-only","prepublishOnly":"vp run build"},"devDependencies":{"@types/node":"25.6.2","@typescript/native-preview":"7.0.0-dev.20260509.2","typescript":"6.0.3","vite-plus":"catalog:","zod":"4.4.3"},"peerDependencies":{"zod":"^4.4.3"},"engines":{"node":">=22"},"packageManager":"pnpm@11.7.0","gitHead":"71b1cf71a7c909abb8127cb69e8d1aae7840127a","_id":"@b2m9/zod-views@0.1.0","_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-qXE68+zv0bXPoUipRHBiQoNB8fl1IAAkn4gZk6EVTt4VpznqAob/V3FyI+c5J3V0bvmcvd6akm1t+9X8FeVTgg==","shasum":"61aaa03b22f86425ba0baf97437b670b2cfd1919","tarball":"https://registry.npmjs.org/@b2m9/zod-views/-/zod-views-0.1.0.tgz","fileCount":5,"unpackedSize":13617,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCXHIJ7L4nDm+MzG79l89IQaWFhEhoquVmaLpWGJOuMMQIgPn/M/X9mLC4BwwDwdBUihwZelub9AvrphL/zY0yTWO0="}]},"_npmUser":{"name":"b2m9","email":"bob@b2m9.com"},"directories":{},"maintainers":[{"name":"b2m9","email":"bob@b2m9.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/zod-views_0.1.0_1785060669268_0.2667240916142717"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-26T10:11:09.093Z","0.1.0":"2026-07-26T10:11:09.392Z","modified":"2026-07-26T10:11:09.638Z"},"maintainers":[{"name":"b2m9","email":"bob@b2m9.com"}],"description":"Derive safe create, update, and read schemas from one Zod object.","homepage":"https://github.com/b2m9/zod-views#readme","keywords":["api","patch","schema","typescript","validation","views","zod"],"repository":{"type":"git","url":"git+https://github.com/b2m9/zod-views.git"},"author":{"name":"Bob Massarczyk"},"bugs":{"url":"https://github.com/b2m9/zod-views/issues"},"license":"MIT","readme":"# @b2m9/zod-views\n\nDerive strict `create` and `update` inputs and a stripping `read` schema from\none Zod object and one exhaustive field-role table.\n\nThe usual PATCH schema can silently reset stored data:\n\n```ts\nimport { z } from \"zod\";\nconst status = z.enum([\"draft\", \"live\"]).default(\"draft\");\nconst TaskCore = z.object({ id: z.uuid(), title: z.string(), status });\nconst update = TaskCore.omit({ id: true }).partial();\nupdate.parse({ title: \"Q3 Report\" }); // { title: \"Q3 Report\", status: \"draft\" }\n```\n\nThe client sent a title. It got back a status. Merge that parsed patch into an\nexisting task and its omitted status becomes `draft`. Zod applies defaults\ninside optional object fields by design. That is useful for create input, but\ndangerous at a PATCH boundary.\n\n`defineViews` gives every mutable update field an undefined-first shield:\n\n```ts\nimport { defineViews } from \"@b2m9/zod-views\";\n\nconst Task = defineViews(TaskCore, {\n  id: \"server\",\n  title: \"mutable\",\n  status: \"mutable\",\n});\n\nTask.update.parse({ title: \"Q3 Report\" }); // { title: \"Q3 Report\" }\nTask.create.parse({ title: \"New\" }); // { title: \"New\", status: \"draft\" }\n```\n\nThe table also prevents schema drift. Add a core field and TypeScript requires\nyou to classify it before the build passes:\n\n```ts\nconst ProjectCore = z.object({\n  id: z.uuid(),\n  name: z.string(),\n  internalNotes: z.string(),\n});\n\ndefineViews(ProjectCore, {\n  id: \"server\",\n  name: \"mutable\",\n  // error: Property 'internalNotes' is missing\n});\n```\n\nYou can build the same schemas by hand. This package makes two safety decisions\nmandatory: every field is classified, and an omitted update field cannot run\nits default.\n\n## Install\n\n```sh\npnpm add @b2m9/zod-views \"zod@^4.4.3\"\n```\n\nSupported environment: ESM, Node 22+, and `zod@^4.4.3`. Zod is the only peer\ndependency.\n\n## Usage\n\n```ts\nimport { defineViews } from \"@b2m9/zod-views\";\nimport { z } from \"zod\";\n\nconst UserCore = z.object({\n  id: z.uuid(),\n  orgId: z.uuid(),\n  email: z.email(),\n  password: z.string(),\n  displayName: z.string(),\n  roleId: z.uuid(),\n});\n\nconst User = defineViews(UserCore, {\n  id: \"server\",\n  orgId: \"server hidden\",\n  email: \"create-only\",\n  password: \"create-only hidden\",\n  displayName: \"mutable\",\n  roleId: \"mutable\",\n});\n\ntype UserUpdate = z.infer<typeof User.update>;\n// { displayName?: string | undefined; roleId?: string | undefined }\n```\n\nEach role combines writability with an optional visibility modifier:\n\n| Role          | Create | Update | Read |\n| ------------- | ------ | ------ | ---- |\n| `mutable`     | yes    | yes    | yes  |\n| `create-only` | yes    | no     | yes  |\n| `server`      | no     | no     | yes  |\n\nAppend ` hidden` to any role to remove the field from `read`. Visibility never\nchanges writability.\n\n## API\n\nThe public API is `defineViews` and the `FieldsFor` type. The function accepts a\nplain core object and its exhaustive table. It returns three ordinary,\nunrefined Zod objects.\n\nFor a table declared separately, use the package's only exported type:\n\n```ts\nimport { defineViews, type FieldsFor } from \"@b2m9/zod-views\";\n\nconst fields = {\n  id: \"server\",\n  title: \"mutable\",\n  status: \"mutable\",\n} satisfies FieldsFor<typeof TaskCore>;\nconst Task = defineViews(TaskCore, fields);\n```\n\nPrefer `satisfies FieldsFor<typeof TaskCore>` for a hoisted table. An annotation\nsuch as `: FieldsFor<typeof TaskCore>` widens every value to the full role\nunion. `defineViews` rejects that form because one widened role cannot determine\none exact view type. `as const` also preserves literal roles, but does not\nvalidate the table until the call.\n\n## Semantics\n\n| View     | Fields                         | Boundary            |\n| -------- | ------------------------------ | ------------------- |\n| `create` | mutable and create-only        | strict              |\n| `update` | mutable, shielded and optional | strict              |\n| `read`   | every non-hidden field         | strips unknown keys |\n\nThe update shield is `z.union([z.undefined(), field]).optional()`. An absent\nJSON property never reaches the original field schema, so defaults, transforms,\nand pipes cannot inject a value. A provided non-`undefined` value still\nvalidates through the original schema.\n\nCreate retains the original field schemas, including defaults. Read validates\nvisible fields while stripping hidden and unknown keys.\n\n## Everything else is your own Zod\n\nThe views support normal Zod composition:\n\n```ts\nconst RoleSchema = z.object({ id: z.uuid(), name: z.string() });\nconst UserExpanded = User.read.omit({ roleId: true }).extend({ role: RoleSchema });\nconst SignupInput = User.create.extend({ password: z.string().min(12) });\nconst NonEmpty = User.update.refine((patch) =>\n  Object.values(patch).some((value) => value !== undefined),\n);\n```\n\n## Use plain Zod instead\n\nUse plain Zod when the thing is not one entity exposed three ways. Unions,\ncommands, events, search parameters, and one-off request bodies do not need an\nentity view table.\n\n## Guardrails\n\nCreate and update inputs are strict. Unknown keys are rejected so typos surface.\nThis also means echoing a fetched entity into a write view fails by design.\nSend only writable fields.\n\nEmpty updates are valid. Use the `NonEmpty` refinement above if your API rejects\nthem.\n\nExplicit `undefined` short-circuits the field schema and remains present as\n`{ field: undefined }`. For a defaulted field this differs from `.partial()`,\nwhich materializes the default. JSON request bodies cannot contain `undefined`.\nIf your merge distinguishes absence from `undefined`, drop those keys before\nmerging.\n\nRead validates as well as strips. A visible field with invalid stored data\nthrows instead of returning a sanitized partial row.\n\nA wrong-type update value produces Zod's native `invalid_union` issue.\nConstraint failures remain direct issues. Message wording is not promised.\n\nThe core must be a plain object without refinements or pipes. Refine the\nderived view that owns the policy instead.\n\nClassify every field. The repetition is the audit; there is no opt-out. Missing,\nstale, and invalid entries also throw at definition time for JavaScript callers.\n\n## License\n\nMIT © 2026 Bob Massarczyk\n","readmeFilename":"README.md","_rev":"1-b904711c13a93f7687159987e626db22"}