-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathpackage-version.zod.ts
More file actions
226 lines (183 loc) · 9.79 KB
/
Copy pathpackage-version.zod.ts
File metadata and controls
226 lines (183 loc) · 9.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { z } from 'zod';
/**
* Package Version Protocol
*
* A **package version** is an **immutable** release snapshot of a package.
* Once published (`status = 'published'`), its `manifestJson` and `checksum`
* fields are frozen — publishing is the act of sealing the snapshot.
*
* Lifecycle:
* draft → published → deprecated
*
* Installing a package means pointing a `sys_package_installation` row at a
* specific `sys_package_version` UUID. Upgrading swaps that pointer atomically.
*
* See `docs/adr/0003-package-as-first-class-citizen.md` for the full rationale.
*/
// ---------------------------------------------------------------------------
// Enums
// ---------------------------------------------------------------------------
/**
* Package version lifecycle status.
*
* - `draft` — being authored; can be mutated; cannot be installed in production
* - `published` — immutable snapshot; installable in any environment
* - `deprecated` — published but superseded; existing installs continue to work
* but new installs are blocked (unless `allowDeprecated = true`)
*/
import { lazySchema } from '../shared/lazy-schema';
export const PackageVersionStatusSchema = lazySchema(() => z
.enum(['draft', 'published', 'deprecated'])
.describe('Package version lifecycle status'));
export type PackageVersionStatus = z.infer<typeof PackageVersionStatusSchema>;
// ---------------------------------------------------------------------------
// Manifest content schemas (embedded in packageVersion.manifestJson)
// ---------------------------------------------------------------------------
/**
* A single dependency declared in a package manifest.
* Follows npm-style `"package_id": "version_range"` semantics.
*
* This is the DECLARATION-side shape — what an author writes into
* `manifestJson.dependencies[]`. The resolver-side shape (a constraint plus
* the `resolvedVersion` it resolved to) is a different concept and lives in
* `@objectstack/spec/kernel` as `ResolvedPackageDependency`; before #4741 it
* also went by `PackageDependency`, so which type a consumer got depended on
* nothing but the import path (#4411). The bare name is now this one alone.
*/
export const PackageDependencySchema = lazySchema(() => z.object({
/** Manifest ID of the required package (e.g. `com.objectstack.core`). */
packageId: z.string().describe('Manifest ID of the dependency'),
/** Required version range (semver, e.g. "^1.0.0"). */
versionRange: z.string().describe('Semver version range (e.g. ^1.0.0)'),
/** Whether this dependency is optional (missing dep is a warning, not an error). */
optional: z.boolean().default(false).describe('Whether this dependency is optional'),
}).describe('Package dependency declaration'));
export type PackageDependency = z.infer<typeof PackageDependencySchema>;
/**
* Lightweight manifest embedded in `sys_package_version.manifest_json`.
*
* This is the frozen snapshot of what the package declares at publish time.
* The runtime uses this to know what metadata records to load and what
* minimum platform version is required.
*/
export const PackageManifestSchema = lazySchema(() => z.object({
/** Manifest ID (must match the parent `sys_package.manifest_id`). */
id: z.string().describe('Package manifest ID (reverse-domain)'),
/** Semantic version of this release. */
version: z.string().describe('Semver version string (e.g. 1.2.3)'),
/** Human-readable display name. */
name: z.string().describe('Display name'),
/** Short description. */
description: z.string().optional().describe('Short description'),
/**
* Package scope — determines where it runs.
* - `platform` — provided by the runtime; cannot be installed per-env
* - `environment` — installed into a specific environment
*/
scope: z.enum(['platform', 'environment']).default('environment').describe('Package scope'),
/** Minimum ObjectStack platform version required. */
minPlatformVersion: z.string().optional().describe('Minimum required platform version (semver)'),
/** List of packages this version depends on. */
dependencies: z.array(PackageDependencySchema).default([]).describe('Package dependencies'),
/**
* Names of metadata types included in this package version.
* Used by the installer to know which `sys_metadata` rows belong to it.
* Example: `["object", "view", "flow", "translation"]`
*/
metadataTypes: z.array(z.string()).default([]).describe('Metadata types provided by this package'),
/**
* List of migration script identifiers included in this version,
* ordered by execution sequence. Applied to the environment DB on install/upgrade.
*/
migrations: z.array(z.string()).default([]).describe('Migration script identifiers (ordered)'),
/** Free-form configuration schema (JSON Schema) for per-install settings. */
configurationSchema: z.record(z.string(), z.unknown()).optional()
.describe('JSON Schema for per-installation configuration properties'),
/** Free-form extension metadata. */
metadata: z.record(z.string(), z.unknown()).optional().describe('Extension metadata'),
}).describe('Package manifest snapshot embedded in a package version'));
export type PackageManifest = z.infer<typeof PackageManifestSchema>;
// ---------------------------------------------------------------------------
// sys_package_version — Immutable release snapshot
// ---------------------------------------------------------------------------
/**
* One row in `sys_package_version` — a sealed, versioned release of a package.
*
* The triple `(packageId, version)` is UNIQUE.
* Once `status = 'published'`, `manifestJson` and `checksum` MUST NOT change.
*/
export const PackageVersionSchema = lazySchema(() => z.object({
/** UUID of the version row (stable, never reused). */
id: z.string().uuid().describe('UUID of the package version (stable, never reused)'),
/** Parent package this version belongs to. */
packageId: z.string().uuid().describe('UUID of the parent sys_package row'),
/** Semantic version string (e.g. `1.2.3`, `2.0.0-beta.1`). */
version: z
.string()
.regex(/^\d+\.\d+\.\d+(-[a-z0-9.-]+)?(\+[a-z0-9.-]+)?$/)
.describe('Semantic version string'),
/** Lifecycle status. Immutable fields freeze on transition to "published". */
status: PackageVersionStatusSchema.default('draft'),
/**
* Full package manifest frozen at publish time.
* Stored as a JSON-serialized string for portability across database drivers.
*/
manifestJson: z.string().describe('JSON-serialized package manifest (frozen on publish)'),
/**
* SHA-256 hex digest of `manifestJson`.
* Verified by the installer to detect corrupt or tampered snapshots.
*/
checksum: z.string().regex(/^[a-f0-9]{64}$/).optional()
.describe('SHA-256 hex digest of manifestJson'),
/** Human-readable release notes (markdown). */
releaseNotes: z.string().optional().describe('Release notes for this version (markdown)'),
/** Minimum ObjectStack platform version required (denormalized from manifest for fast queries). */
minPlatformVersion: z.string().optional()
.describe('Minimum required platform version (denormalized from manifest)'),
/** Whether this version is a pre-release (beta, rc, alpha). */
isPreRelease: z.boolean().default(false).describe('Whether this is a pre-release version'),
/** Timestamp when the version was published (null while draft). */
publishedAt: z.string().datetime().optional().describe('Publish timestamp (ISO-8601)'),
/** User ID who published this version. */
publishedBy: z.string().optional().describe('User ID who published this version'),
/** Creation timestamp (ISO-8601). */
createdAt: z.string().datetime().describe('Creation timestamp (ISO-8601)'),
/** Last update timestamp (ISO-8601). Only mutable while status is "draft". */
updatedAt: z.string().datetime().describe('Last update timestamp (ISO-8601)'),
/** User ID that created this version row. */
createdBy: z.string().describe('User ID that created this version'),
}));
export type PackageVersion = z.infer<typeof PackageVersionSchema>;
// ---------------------------------------------------------------------------
// Request / Response
// ---------------------------------------------------------------------------
/**
* Request to create a new draft package version.
*/
export const CreatePackageVersionRequestSchema = lazySchema(() => z.object({
packageId: z.string().uuid().describe('Parent package UUID'),
version: PackageVersionSchema.shape.version,
manifestJson: z.string().describe('Initial manifest JSON (can be updated while draft)'),
releaseNotes: z.string().optional(),
isPreRelease: z.boolean().optional(),
createdBy: z.string().describe('User ID creating this version'),
}).describe('Create a new draft package version'));
export type CreatePackageVersionRequest = z.infer<typeof CreatePackageVersionRequestSchema>;
/**
* Request to update a draft version's manifest before publishing.
* Only allowed while `status = 'draft'`.
*/
export const UpdatePackageVersionRequestSchema = lazySchema(() => z.object({
manifestJson: z.string().optional().describe('Updated manifest JSON'),
releaseNotes: z.string().optional(),
isPreRelease: z.boolean().optional(),
}).describe('Update a draft package version (only while status is draft)'));
export type UpdatePackageVersionRequest = z.infer<typeof UpdatePackageVersionRequestSchema>;
/**
* Request to publish a draft version (seals manifestJson and checksum).
*/
export const PublishPackageVersionRequestSchema = lazySchema(() => z.object({
publishedBy: z.string().describe('User ID publishing this version'),
}).describe('Publish a draft version — seals manifestJson and checksum'));
export type PublishPackageVersionRequest = z.infer<typeof PublishPackageVersionRequestSchema>;