Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .changeset/23972-installed-package-source.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
'@objectstack/spec': minor
'@objectstack/objectql': minor
'@objectstack/metadata-protocol': patch
'@objectstack/runtime': patch
'@objectstack/service-package': patch
'@objectstack/cloud-connection': patch
'@objectstack/client': patch
---

feat(spec)!: every installed-package row declares where it came from, a closed `source: 'code' | 'marketplace' | 'authored'`, and the `GET /packages` rows declare the `writable` verdict they already served (#23972)

Clause-②: yes (narrowing — the served row gains two declared keys, and the record schema, the served-row union and the registry's installPackage signature each refuse what they accepted before)

**BREAKING** — three published accept sets narrow. `InstalledPackageSchema` refuses a row without `source`, or with a value outside `code` / `marketplace` / `authored`. The served-row union `InstalledPackageAtEitherStageSchema` refuses a row without a boolean `writable`. `SchemaRegistry.installPackage` and `EngineSchemaRegistryView.installPackage` take `source` as their second argument and refuse a call without it. The migration is below. None of it is mechanically convertible, because only the door that registers a package knows where it came from.

**What is new on the wire.** `GET /api/v1/packages` and `GET /api/v1/packages/:id` serve two declared keys on every row:

- `source` names the package's origin. `code` means the deployment's own code registered it (a `defineStack()` host, a plugin's bundled manifest, a platform package). `marketplace` means the marketplace install-local plugin installed it, and that holds after a restart too, when the plugin rehydrates it from its ledger. `authored` means it is a database-authored package (a Studio base, a duplicate, an HTTP install), including after the restart that rehydrates it from `sys_packages`. The door that registers the package stamps the value. No reader derives it.
- `writable` is the server's own writability verdict (ADR-0070 D2), served on these rows since #14375 and now declared. It is computed per read and never stored, so it is declared on the served row (`InstalledPackageAtEitherStageSchema`) and not on the registry record.

FROM → TO for a reader of these rows:

| you did | do instead |
| --- | --- |
| treated `writable: true` as authored, and looked up each non-writable row in the marketplace's installed list (the control plane's, else install-local's) to tell marketplace from code | read `row.source` |
| read the undeclared `writable` | read the declared `writable` (unchanged value) |

The two keys are separate statements: `source` says where the package came from, and `writable` says what this server lets an org do to it.

**`InstalledPackageSchema` requires `source`.** It is `InstalledPackageSourceSchema`, exported from `@objectstack/spec/kernel`. A row without it, or with a value outside the closed set, is refused at the `source` key.

**API change for code that registers packages (`@objectstack/objectql`).** The registry records the origin and never infers it:

| you wrote | write instead |
| --- | --- |
| `registry.installPackage(manifest)` | `registry.installPackage(manifest, 'code')` (or `'marketplace'` / `'authored'`, whichever is where the package came from) |
| `registry.installPackage(manifest, settings, scope)` | `registry.installPackage(manifest, source, settings, scope)` |
| `ctx.getService('manifest').register(artifact)` for a marketplace install | `ctx.getService('manifest').register(artifact, { source: 'marketplace' })` |

`SchemaRegistry.installPackage` throws a `TypeError` naming the closed set when `source` is missing or unknown, before anything is recorded. `ObjectQL.registerApp(body, scope?, source?)` and the `manifest` service record `'code'` when no source is given. They accept `'code'` or `'marketplace'` and refuse `'authored'`. The `IObjectQLEngine` registry contract (`@objectstack/spec/contracts`) declares the same `installPackage(manifest, source, settings?)`.

**The one-line fix:** pass the package's origin as `installPackage`'s second argument.

Readers: objectui's Setup → Packages Source column and its `?source=` filter (it switches from the two-read join to `source` in its own follow-up), and the client SDK's `packages.list()` / `packages.get()` row types.

<!-- adr-0087: registered installed-package-row-source-required, registry-install-package-source-argument -->
21 changes: 11 additions & 10 deletions content/docs/references/api/package-api-assembled.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Installed package row whose manifest is the assembled package body
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | The ASSEMBLED package body this row carries, at the stage the registry records it |
| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) |
| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error |
| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled |
| **installedAt** | `string` | optional | Installation timestamp |
Expand Down Expand Up @@ -166,7 +167,7 @@ Get installed package response
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>; declaredCode?: string; message: string; userMessage?: string; … }` | optional | Error details if success is false |
| **meta** | `{ timestamp: string; duration?: integer; requestId?: string; traceId?: string }` | optional | Response metadata |
| **data** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … } \| … +1 more` | ✅ | Installed package details |
| **data** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … } \| … +1 more` | ✅ | Installed package details |

### Nested Shape: `GetInstalledPackageResponse.error`

Expand All @@ -193,11 +194,10 @@ Get installed package response

### Nested Shape: `GetInstalledPackageResponse.data[option 1]`

Installed package with runtime lifecycle state

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` |
| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) |
| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error |
| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled |
| **installedAt** | `string` | optional | Installation timestamp |
Expand All @@ -209,14 +209,14 @@ Installed package with runtime lifecycle state
| **settings** | `Record<string, any>` | optional | User-provided configuration settings |
| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history |
| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package |
| **writable** | `boolean` | ✅ | Whether this server lets an org author into and manage this package (a writable base); false for a read-only package. Computed per read by the server's own predicate, never stored |

### Nested Shape: `GetInstalledPackageResponse.data[option 2]`

Installed package row whose manifest is the assembled package body

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | The ASSEMBLED package body this row carries, at the stage the registry records it |
| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) |
| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error |
| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled |
| **installedAt** | `string` | optional | Installation timestamp |
Expand All @@ -228,6 +228,7 @@ Installed package row whose manifest is the assembled package body
| **settings** | `Record<string, any>` | optional | User-provided configuration settings |
| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history |
| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package |
| **writable** | `boolean` | ✅ | Whether this server lets an org author into and manage this package (a writable base); false for a read-only package. Computed per read by the server's own predicate, never stored |


---
Expand All @@ -242,13 +243,12 @@ This schema accepts one of the following structures:

#### Option 1

Installed package with runtime lifecycle state

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` |
| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) |
| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error |
| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled |
| **installedAt** | `string` | optional | Installation timestamp |
Expand All @@ -260,6 +260,7 @@ Installed package with runtime lifecycle state
| **settings** | `Record<string, any>` | optional | User-provided configuration settings |
| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history |
| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package |
| **writable** | `boolean` | ✅ | Whether this server lets an org author into and manage this package (a writable base); false for a read-only package. Computed per read by the server's own predicate, never stored |

### Nested Shape: `InstalledPackageAtEitherStage[option 1].manifest`

Expand Down Expand Up @@ -306,13 +307,12 @@ Installed package with runtime lifecycle state

#### Option 2

Installed package row whose manifest is the assembled package body

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | The ASSEMBLED package body this row carries, at the stage the registry records it |
| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) |
| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error |
| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled |
| **installedAt** | `string` | optional | Installation timestamp |
Expand All @@ -324,6 +324,7 @@ Installed package row whose manifest is the assembled package body
| **settings** | `Record<string, any>` | optional | User-provided configuration settings |
| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history |
| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package |
| **writable** | `boolean` | ✅ | Whether this server lets an org author into and manage this package (a writable base); false for a read-only package. Computed per read by the server's own predicate, never stored |

### Nested Shape: `InstalledPackageAtEitherStage[option 2].manifest`

Expand Down Expand Up @@ -443,7 +444,7 @@ List installed packages response

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **packages** | `({ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … } \| … +1 more)[]` | ✅ | Installed packages |
| **packages** | `({ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … } \| … +1 more)[]` | ✅ | Installed packages |
| **total** | `integer` | optional | Total matching packages |
| **nextCursor** | `string` | optional | Cursor for the next page |
| **hasMore** | `boolean` | ✅ | Whether more packages are available — this door serves one page, so always `false` |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/api/package-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ Install package response

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Installed package details |
| **package** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }` | ✅ | Installed package details |
| **dependencyResolution** | `{ dependencies: object[]; canProceed: boolean; requiredActions: object[]; installOrder: string[]; … }` | optional | Dependency resolution result |
| **namespaceConflicts** | `{ type: 'namespace_conflict'; requestedNamespace: string; conflictingPackageId: string; conflictingPackageName: string; … }[]` | optional | Namespace conflicts detected |
| **message** | `string` | optional | Installation status message |
Expand Down
Loading
Loading