Skip to content

Commit c0c3046

Browse files
committed
Introduce package registry objects (ADR-0003)
Add first-class control-plane package model: create sys_package and sys_package_version object schemas, and evolve sys_package_installation to point at immutable package_version_id (with denormalized package_id) and richer metadata (status, settings, timestamps, descriptions, indexes). Add package_version_id to sys_metadata and its index. Export and register the new objects from the service-tenant index and tenant plugin, and add tests verifying names, uniqueness indexes, system flags, and field descriptions. These changes implement ADR-0003: treat packages and versions as control-plane first-class entities and enable atomic upgrades/rollbacks by swapping package_version_id.
1 parent c949fef commit c0c3046

7 files changed

Lines changed: 459 additions & 36 deletions

File tree

packages/metadata/src/objects/sys-metadata.object.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,24 @@ export const SysMetadataObject = ObjectSchema.create({
5555
maxLength: 100,
5656
}),
5757

58-
/** Package that owns/delivered this metadata */
58+
/** Package that owns/delivered this metadata (legacy string identifier, kept for compat) */
5959
package_id: Field.text({
6060
label: 'Package ID',
6161
required: false,
6262
maxLength: 255,
63+
description: 'Legacy package manifest ID string. Use package_version_id for new records.',
64+
}),
65+
66+
/**
67+
* FK → sys_package_version (UUID). Set for metadata that belongs to a specific
68+
* package release snapshot. NULL = platform-built-in or environment override.
69+
*/
70+
package_version_id: Field.text({
71+
label: 'Package Version ID',
72+
required: false,
73+
maxLength: 255,
74+
description:
75+
'Foreign key to sys_package_version (UUID). Null = platform-built-in or env-level override.',
6376
}),
6477

6578
/** Who manages this record: package, platform, or user */
@@ -184,6 +197,7 @@ export const SysMetadataObject = ObjectSchema.create({
184197
{ fields: ['type', 'scope'] },
185198
{ fields: ['organization_id'] },
186199
{ fields: ['env_id'] },
200+
{ fields: ['package_version_id'] },
187201
{ fields: ['state'] },
188202
{ fields: ['namespace'] },
189203
],

packages/services/service-tenant/src/objects/environment-objects.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import {
55
SysEnvironment,
66
SysDatabaseCredential,
77
SysEnvironmentMember,
8+
SysPackage,
9+
SysPackageVersion,
10+
SysPackageInstallation,
811
} from './index';
912

1013
describe('control-plane environment objects', () => {
@@ -51,3 +54,68 @@ describe('control-plane environment objects', () => {
5154
expect(SysEnvironmentMember.isSystem).toBe(true);
5255
});
5356
});
57+
58+
describe('control-plane package objects (ADR-0003)', () => {
59+
it('registers sys_package and sys_package_version with correct namespaced names', () => {
60+
expect(`${SysPackage.namespace}_${SysPackage.name}`).toBe('sys_package');
61+
expect(`${SysPackageVersion.namespace}_${SysPackageVersion.name}`).toBe('sys_package_version');
62+
expect(`${SysPackageInstallation.namespace}_${SysPackageInstallation.name}`).toBe('sys_package_installation');
63+
});
64+
65+
it('marks all package objects as system objects', () => {
66+
expect(SysPackage.isSystem).toBe(true);
67+
expect(SysPackageVersion.isSystem).toBe(true);
68+
expect(SysPackageInstallation.isSystem).toBe(true);
69+
});
70+
71+
it('sys_package has UNIQUE manifest_id index', () => {
72+
const idx = SysPackage.indexes ?? [];
73+
expect(
74+
idx.some((i: any) => i.unique && i.fields.join(',') === 'manifest_id'),
75+
).toBe(true);
76+
});
77+
78+
it('sys_package_version has UNIQUE (package_id, version) index', () => {
79+
const idx = SysPackageVersion.indexes ?? [];
80+
expect(
81+
idx.some((i: any) => i.unique && i.fields.join(',') === 'package_id,version'),
82+
).toBe(true);
83+
});
84+
85+
it('sys_package_installation has UNIQUE (environment_id, package_id) index', () => {
86+
const idx = SysPackageInstallation.indexes ?? [];
87+
expect(
88+
idx.some((i: any) => i.unique && i.fields.join(',') === 'environment_id,package_id'),
89+
).toBe(true);
90+
});
91+
92+
it('sys_package_installation has package_version_id field (not a version string)', () => {
93+
expect(SysPackageInstallation.fields).toHaveProperty('package_version_id');
94+
expect(SysPackageInstallation.fields).not.toHaveProperty('upgrade_history');
95+
});
96+
97+
it('sys_package_installation has package_version_id index', () => {
98+
const idx = SysPackageInstallation.indexes ?? [];
99+
expect(
100+
idx.some((i: any) => i.fields.join(',') === 'package_version_id'),
101+
).toBe(true);
102+
});
103+
104+
it('gives every field on sys_package a .description', () => {
105+
for (const [name, field] of Object.entries(SysPackage.fields)) {
106+
expect((field as any).description, `sys_package.${name} missing description`).toBeTruthy();
107+
}
108+
});
109+
110+
it('gives every field on sys_package_version a .description', () => {
111+
for (const [name, field] of Object.entries(SysPackageVersion.fields)) {
112+
expect((field as any).description, `sys_package_version.${name} missing description`).toBeTruthy();
113+
}
114+
});
115+
116+
it('gives every field on sys_package_installation a .description', () => {
117+
for (const [name, field] of Object.entries(SysPackageInstallation.fields)) {
118+
expect((field as any).description, `sys_package_installation.${name} missing description`).toBeTruthy();
119+
}
120+
});
121+
});

packages/services/service-tenant/src/objects/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export * from './sys-environment-member.object';
99
// docs/adr/0002-environment-database-isolation.md for the migration path.
1010
export * from './sys-tenant-database.object';
1111

12-
// Package installation registry (lives in control plane for now; will move
13-
// into each environment's data plane in v5.0 per ADR-0002).
12+
// Package registry (Control Plane, permanent — see ADR-0003).
13+
export * from './sys-package.object';
14+
export * from './sys-package-version.object';
1415
export * from './sys-package-installation.object';

packages/services/service-tenant/src/objects/sys-package-installation.object.ts

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,20 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
55
/**
66
* sys_package_installation — Per-environment package installation record.
77
*
8-
* Tracks which packages (business solutions) are installed in each environment.
9-
* Stored in the **Control Plane** (sys namespace → turso driver) so that all
10-
* environment installations are visible from a single query. Environment DBs
11-
* contain only business data rows — zero system tables.
8+
* Models the pairing between an environment and a specific, immutable package
9+
* version snapshot (`sys_package_version`). Only one version of a given package
10+
* may be active per environment at a time (enforced by UNIQUE on
11+
* `(environment_id, package_id)`).
12+
*
13+
* **Upgrade** = atomic UPDATE of `package_version_id` to a newer version UUID.
14+
* **Rollback** = atomic UPDATE of `package_version_id` to an older version UUID.
15+
* Version history is tracked via the sequence of `package_version_id` changes
16+
* on this row (and an optional sys_package_installation_history audit table).
17+
*
18+
* **Stored in the Control Plane DB (not in environment DBs).**
19+
* Environment DBs contain only business data rows — zero system tables.
20+
*
21+
* See `docs/adr/0003-package-as-first-class-citizen.md` for the full rationale.
1222
*
1323
* @namespace sys
1424
*/
@@ -19,106 +29,111 @@ export const SysPackageInstallation = ObjectSchema.create({
1929
pluralLabel: 'Package Installations',
2030
icon: 'package',
2131
isSystem: true,
22-
description: 'Per-environment package installation registry (sys_package_installation)',
32+
description: 'Per-environment package installation registry (sys_package_installation).',
2333
titleFormat: '{package_id} @ {environment_id}',
24-
compactLayout: ['package_id', 'environment_id', 'version', 'status'],
34+
compactLayout: ['package_version_id', 'environment_id', 'status', 'installed_at'],
2535

2636
fields: {
2737
id: Field.text({
2838
label: 'Installation ID',
2939
required: true,
3040
readonly: true,
31-
description: 'UUID-based installation identifier',
41+
description: 'UUID of this installation record (stable, never reused).',
3242
}),
3343

3444
created_at: Field.datetime({
3545
label: 'Created At',
3646
defaultValue: 'NOW()',
3747
readonly: true,
48+
description: 'Creation timestamp (ISO-8601).',
3849
}),
3950

4051
updated_at: Field.datetime({
4152
label: 'Updated At',
4253
defaultValue: 'NOW()',
4354
readonly: true,
55+
description: 'Last update timestamp — changes on upgrade, rollback, enable/disable (ISO-8601).',
4456
}),
4557

4658
environment_id: Field.text({
4759
label: 'Environment ID',
4860
required: true,
49-
description: 'Foreign key to sys__environment',
61+
description: 'Foreign key to sys_environment (UUID). The environment that owns this installation.',
5062
}),
5163

52-
package_id: Field.text({
53-
label: 'Package ID',
64+
package_version_id: Field.text({
65+
label: 'Package Version ID',
5466
required: true,
55-
maxLength: 255,
56-
description: 'Manifest ID of the installed package (reverse-domain, e.g. com.example.crm)',
67+
description:
68+
'Foreign key to sys_package_version (UUID). The specific, immutable release snapshot ' +
69+
'currently installed in this environment. Upgrading = swapping this field to a newer ' +
70+
'version UUID. Rollback = swapping to an older version UUID.',
5771
}),
5872

59-
version: Field.text({
60-
label: 'Version',
73+
package_id: Field.text({
74+
label: 'Package ID',
6175
required: true,
62-
maxLength: 50,
63-
description: 'Installed package version (semver)',
76+
description:
77+
'Foreign key to sys_package (UUID). Denormalized from the linked package_version row ' +
78+
'at install time to enforce the UNIQUE (environment_id, package_id) constraint without a JOIN.',
6479
}),
6580

6681
status: Field.select({
6782
label: 'Status',
6883
required: true,
84+
defaultValue: 'installed',
85+
description: 'Current lifecycle status of this installation within the environment.',
6986
options: [
7087
{ value: 'installed', label: 'Installed' },
7188
{ value: 'installing', label: 'Installing' },
7289
{ value: 'upgrading', label: 'Upgrading' },
7390
{ value: 'disabled', label: 'Disabled' },
7491
{ value: 'error', label: 'Error' },
7592
],
76-
defaultValue: 'installed',
7793
}),
7894

7995
enabled: Field.boolean({
8096
label: 'Enabled',
8197
required: true,
8298
defaultValue: true,
83-
description: 'Whether the package is currently active in this environment',
99+
description:
100+
'Whether the package metadata is actively loaded into this environment. ' +
101+
'Disabled packages are installed but their schema is not visible to the runtime.',
102+
}),
103+
104+
settings: Field.textarea({
105+
label: 'Settings',
106+
required: false,
107+
description:
108+
'JSON-serialized per-installation configuration overrides. ' +
109+
'Keys mirror the package manifest configurationSchema.properties.',
84110
}),
85111

86112
installed_at: Field.datetime({
87113
label: 'Installed At',
88114
required: true,
89115
defaultValue: 'NOW()',
116+
description: 'Timestamp when this installation was first created (ISO-8601).',
90117
}),
91118

92119
installed_by: Field.text({
93120
label: 'Installed By',
94121
required: false,
95-
description: 'User ID who installed the package',
122+
description: 'User ID who performed the initial install. Null for system-automated installs.',
96123
}),
97124

98125
error_message: Field.textarea({
99126
label: 'Error Message',
100127
required: false,
101-
description: 'Error details when status is error',
102-
}),
103-
104-
settings: Field.textarea({
105-
label: 'Settings',
106-
required: false,
107-
description: 'JSON-serialized per-installation configuration',
108-
}),
109-
110-
upgrade_history: Field.textarea({
111-
label: 'Upgrade History',
112-
required: false,
113-
defaultValue: '[]',
114-
description: 'JSON array of version upgrade records',
128+
description: 'Error details when status is error. Cleared on next successful install/upgrade.',
115129
}),
116130
},
117131

118132
indexes: [
119133
{ fields: ['environment_id', 'package_id'], unique: true },
120134
{ fields: ['environment_id'] },
121135
{ fields: ['package_id'] },
136+
{ fields: ['package_version_id'] },
122137
{ fields: ['status'] },
123138
],
124139

0 commit comments

Comments
 (0)