Skip to content

Commit 158e7be

Browse files
committed
feat: ability to define entity references up to a specific version
1 parent 9b51ba1 commit 158e7be

File tree

5 files changed

+1058
-3
lines changed

5 files changed

+1058
-3
lines changed

README.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,137 @@ You can refer to entities from a Zod schema using the `entityReference` method.
149149
SyncedEnvironment.safeParse(synced_env_data) // { type: "ok", value: { id: "test", environment: { name: "test", v: 2, variables: [{ name: "hello", value: "there", masked: false }] } } } <- migrated to latest version
150150
```
151151

152+
### Version-Bounded Parsing and Migration
153+
154+
Sometimes you need to parse and migrate data only up to a specific version, not all the way to the latest. This is particularly useful for recursive entity definitions where entities reference themselves. The `upTo` series of functions provide this capability.
155+
156+
#### Why Version-Bounded Migration?
157+
158+
Consider a recursive tree structure where nodes can contain child nodes. When migrating from v1 to v2, if you use regular `entityReference`, the children might be migrated to a future version (e.g., v3 or v4) that didn't exist when you wrote the v1→v2 migration. This can break your migration logic.
159+
160+
Version-bounded functions ensure that entities are only migrated up to a specific version, maintaining consistency in your migration functions.
161+
162+
#### Available Functions
163+
164+
- **`isUpToVersion(data, version)`** - Type guard that checks if data is valid for any version up to and including the specified version
165+
- **`safeParseUpToVersion(data, version)`** - Parses and migrates data up to a specific version (not beyond)
166+
- **`entityRefUptoVersion(entity, version)`** - Creates a Zod schema for version-bounded entity references
167+
168+
#### Example: Recursive Tree Structure
169+
170+
```ts
171+
import { createVersionedEntity, defineVersion, entityRefUptoVersion } from "verzod"
172+
import { z } from "zod"
173+
174+
// Define a tree structure that references itself
175+
const TreeNode = createVersionedEntity({
176+
latestVersion: 3,
177+
versionMap: {
178+
1: defineVersion({
179+
initial: true,
180+
schema: z.object({
181+
v: z.literal(1),
182+
name: z.string(),
183+
children: z.array(z.lazy(() => entityRefUptoVersion(TreeNode, 1)))
184+
})
185+
}),
186+
2: defineVersion({
187+
initial: false,
188+
schema: z.object({
189+
v: z.literal(2),
190+
name: z.string(),
191+
depth: z.number(),
192+
children: z.array(z.lazy(() => entityRefUptoVersion(TreeNode, 2)))
193+
}),
194+
up(old) {
195+
// When migrating v1→v2, children are guaranteed to be at v2 or lower
196+
// They won't be v3 even if v3 exists in the codebase
197+
return {
198+
v: 2,
199+
name: old.name,
200+
depth: 0,
201+
children: old.children.map(child => {
202+
// Manually migrate each child to v2
203+
const result = TreeNode.safeParseUpToVersion(child, 2)
204+
if (result.type !== "ok") throw new Error("Failed to migrate child")
205+
return result.value
206+
})
207+
}
208+
}
209+
}),
210+
3: defineVersion({
211+
initial: false,
212+
schema: z.object({
213+
v: z.literal(3),
214+
name: z.string(),
215+
depth: z.number(),
216+
path: z.string(),
217+
children: z.array(z.lazy(() => entityRefUptoVersion(TreeNode, 3)))
218+
}),
219+
up(old) {
220+
// Similarly, when migrating v2→v3, children are at v3 or lower
221+
return {
222+
v: 3,
223+
name: old.name,
224+
depth: old.depth,
225+
path: `/${old.name}`,
226+
children: old.children.map(child => {
227+
const result = TreeNode.safeParseUpToVersion(child, 3)
228+
if (result.type !== "ok") throw new Error("Failed to migrate child")
229+
return result.value
230+
})
231+
}
232+
}
233+
})
234+
},
235+
getVersion(data) {
236+
return (data as any)?.v ?? null
237+
}
238+
})
239+
```
240+
241+
#### Using Version-Bounded Functions
242+
243+
```ts
244+
const v1Tree = {
245+
v: 1,
246+
name: "root",
247+
children: [
248+
{ v: 1, name: "child1", children: [] },
249+
{ v: 1, name: "child2", children: [] }
250+
]
251+
}
252+
253+
// Check if data is valid up to v2 (returns false for v3 data)
254+
TreeNode.isUpToVersion(v1Tree, 2) // true
255+
TreeNode.isUpToVersion(v3Tree, 2) // false
256+
257+
// Parse and migrate only up to v2
258+
const v2Result = TreeNode.safeParseUpToVersion(v1Tree, 2)
259+
// Result: tree migrated to v2, not v3
260+
261+
// Use in other schemas
262+
const TreeContainer = z.object({
263+
id: z.string(),
264+
// This ensures the tree is at most v2
265+
tree: entityRefUptoVersion(TreeNode, 2)
266+
})
267+
```
268+
269+
#### Type Safety
270+
271+
The version-bounded functions maintain full type safety:
272+
273+
```ts
274+
import { InferredEntityUpToVersion, KnownEntityVersion } from "verzod"
275+
276+
// Get the type at a specific version
277+
type TreeV2 = InferredEntityUpToVersion<typeof TreeNode, 2>
278+
279+
// Get all valid version numbers
280+
type TreeVersions = KnownEntityVersion<typeof TreeNode> // 1 | 2 | 3
281+
```
282+
152283
153284
<br />
154285
<br />

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "verzod",
3-
"version": "0.2.4",
3+
"version": "0.3.0",
44
"license": "MIT",
55
"description": "A simple versioning and migration library based on Zod schemas",
66
"author": "Andrew Bastin (andrewbastin.k@gmail.com)",

0 commit comments

Comments
 (0)