Skip to content

Commit c866c5a

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21489-job-bodies
2 parents a8c1292 + 0bddffd commit c866c5a

26 files changed

Lines changed: 1995 additions & 127 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/cloud-connection': patch
3+
'@objectstack/metadata-protocol': minor
4+
---
5+
6+
fix(cloud-connection): an install-local uninstall runs the protocol's registered uninstall cleanups, so the package's permission sets and their grants go with it
7+
8+
Clause-②: yes
9+
10+
`DELETE /api/v1/marketplace/install-local/:manifestId` removed the package's ledger entry and nothing else. After a restart the package's objects were gone, but its `managed_by: package` rows in `sys_permission_set`, and every grant of them, survived the uninstall. That broke ADR-0090's "No ghost grants" promise on this door.
11+
12+
The door now runs the uninstall cleanups that domain plugins register with the protocol (`registerUninstallCleanup`) once the ledger entry is gone. It uses the same registry and the same runner as the protocol's own uninstall, so `plugin-security`'s `security.package-permissions` cleanup removes the package's sets with their position and user bindings, and any cleanup registered later fires here too. The cleanups run with the package's manifest id and no organization, because an install-local package is installed for the whole runtime.
13+
14+
The response carries each outcome as `data.cleanups`, the way the protocol's uninstall reports them. A failed cleanup is reported there and named in the operator log with its remedy (install the package again, then uninstall it again). When the protocol cannot run the cleanups, the response says so as one failed `protocol.runUninstallCleanups` outcome. An uninstall that does not happen (a refused caller, an id this door never installed, a ledger write that fails) revokes nothing.
15+
16+
`@objectstack/metadata-protocol`: `ObjectStackProtocolImplementation` gains `runUninstallCleanups({ packageId, organizationId?, actor? })`, the one runner of the uninstall-cleanup registry. It runs every registered cleanup for the package and answers one `UninstallCleanupOutcome` per cleanup. It never throws: a failed cleanup is an outcome, and a thrown fault's driver text goes to the operator log, not into the outcome. `deletePackage` now calls it as its last step in place of its own loop, and its `cleanups` are unchanged. The only visible difference there is the log tag of a failed cleanup's warning, now `[protocol.runUninstallCleanups]` instead of `[protocol.deletePackage]`.
17+
18+
`@objectstack/cloud-connection` now declares its dependency on `@objectstack/metadata-protocol`, which it already received through `@objectstack/runtime`, for the cleanup outcome types.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
`os migrate account-issuer`, `os migrate audit-metadata-bodies`, `os migrate meta --stored`, `os secret orphans`, `os secret rewrap` and `os storage orphans` answer a project whose database does not exist yet with empty work and exit 0, instead of exiting 1 on a refused read (#21552)
6+
7+
Clause-②: no
8+
9+
Each of these commands boots read-only by default: the schema sync is held back, and a missing SQLite file is opened as an empty in-memory stand-in. That boot already measures which tables the database lacks, because the held-back sync lists each one as a table to create. Each command then read the very tables it had just found missing, and the database refused the read. On a never-booted database (or a `--database-url` that points at one) every default run exited 1:
10+
11+
- `os migrate account-issuer` refused, naming `sys_account`;
12+
- `os migrate audit-metadata-bodies` counted `failures: 3` for `sys_audit_log`, `sys_activity` and `sys_metadata_audit`;
13+
- `os migrate meta --stored` refused, naming `sys_metadata`;
14+
- `os secret orphans` and `os secret rewrap` answered `"error": "scan_failed"`, naming `sys_secret`;
15+
- `os storage orphans` refused, naming `sys_file`.
16+
17+
Each command now reads only the tables its boot found present. A table that does not exist holds nothing, so:
18+
19+
- `os migrate account-issuer` reports no account and no collision (`ok: true`), exit 0;
20+
- `os migrate audit-metadata-bodies` reports nothing to rewrite, with `failures: 0`, exit 0;
21+
- `os migrate meta --stored` reports no stored metadata to examine (`scanned: 0`, `clean: true`), exit 0;
22+
- `os secret orphans` and `os secret rewrap` report no secret to act on, with every holder family enumerated rather than a gap, exit 0;
23+
- `os storage orphans` reports no stranded file, exit 0.
24+
25+
Each names the tables it did not read: on stdout in human mode, on stderr under `--json`, where stdout stays one document. `os migrate account-issuer` is the one that recognises the refusal instead of asking the boot: its boot composes no auth plugin, so `sys_account` is never listed as a table to create. It recognises only the missing-table refusal for `sys_account`, with the shared `isMissingTableError` predicate.
26+
27+
A table that exists but lacks a column, and any other read that is refused, is still read and still refuses with exit 1. The write modes (`--apply`, `--delete`) are unchanged: they boot with the schema sync, so their tables exist before they read.
28+
29+
There is nothing to migrate.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
Nine migration-step rationale passages state their decisions in words instead of tracker numbers, and two registry comments cite the commit that decided them
6+
7+
Clause-②: no
8+
9+
The protocol 17 and protocol 18 step rationales are what `os migrate meta` shows per hop
10+
and what the protocol upgrade guide prints. Nine of their passages named GitHub issues
11+
that no longer exist, so an upgrading author met a number with nothing behind it. Each of
12+
those passages now carries no number at all and says what was decided: why `mongo` and
13+
`mongodb` are both accepted, why the form-view option `default` and `connector.errorMapping`
14+
were retired, which earlier cleanup the import mapping `lookup` params finish, what the
15+
memory driver's placeholder refusal extends, how the plugin manifest's `contributes`
16+
members and `routes` were retired, and why the stack `themes` carrier and the
17+
component-translation `submitLabel` key went. Two source comments of the migration
18+
registry now cite the commit behind them. Text only: no migration step, entry, retired key
19+
or def, conversion, schema, export or runtime behaviour changes.

‎content/docs/deployment/cli.mdx‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -532,8 +532,10 @@ It is never run for you; nothing on any boot or upgrade path invokes it.
532532
533533
The report boots your app read-only: no schema change, no seed rows, and a SQLite file
534534
that does not exist is not created. Pointed at a database that lacks `sys_secret` (one
535-
that was never booted, or the wrong `--database-url`), it refuses and exits 1 (under
536-
`--json`: `"error": "scan_failed"`) instead of creating the table and reporting nothing.
535+
that was never booted, or the wrong `--database-url`), it does not read the table, since
536+
a table that does not exist holds no row: it reports nothing to act on, names the
537+
tables it did not read, and exits 0. Any other read it cannot make still refuses and
538+
exits 1 (under `--json`: `"error": "scan_failed"`).
537539
538540
```bash
539541
os secret orphans # report (writes nothing)
@@ -1055,12 +1057,16 @@ creates missing tables and columns so the migration has somewhere to write, but
10551057
still loads no seed data: the only rows that change are the migration's own.
10561058
One edge follows from the read-only boot: it finds out which tables the database lacks
10571059
(a never-booted database, or the wrong `--database-url`) instead of creating them.
1058-
`os migrate value-shapes` does not read a table it found missing, since that table
1059-
holds nothing: the scan is clean over zero records, exits 0, and names the objects it
1060-
did not read. `os migrate recorded-by` and `os migrate resume` answer the same way
1061-
(nothing to convert, no interrupted runs). Another dry run that reads a missing table
1062-
can still fail and exit 1, naming the table. Point `--database-url` at the
1063-
deployment's database, or boot the deployment once first.
1060+
A read-only data command does not read a table it found missing, since that table
1061+
holds nothing, and answers with empty work and exit 0. `os migrate value-shapes` is
1062+
clean over zero records and names the objects it did not read. `os migrate
1063+
recorded-by` has nothing to convert and `os migrate resume` no interrupted runs.
1064+
`os migrate meta --stored` has no stored metadata to examine, `os migrate
1065+
audit-metadata-bodies` no audit copy to rewrite, and `os migrate account-issuer` no
1066+
account to collide. `os secret orphans`, `os secret rewrap` and `os storage orphans`
1067+
report no secret and no file. A read the command cannot avoid and that fails for any
1068+
other reason still refuses and exits 1. Point `--database-url` at the deployment's
1069+
database, or boot the deployment once first, to see what it holds.
10641070
10651071
```bash
10661072
os migrate files-to-references # Dry run: full report, writes nothing

‎docs/protocol-upgrade-guide.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ Closing the same audit on the data side, `datasource.readReplicas` is removed (#
6565

6666
The datasource close-out also graduates the four legacy `datasource.config` spellings the shared driver factory still tolerated via undeclared read-side `??` fallbacks (#4456, the #4410 follow-up): sqlite `file`/`database` (use `filename`), postgres/mysql `connectionString` (use `url`) and `user` (use `username`), and mongo `uri` (use `url`) and `user` (use `username`). #4410 made the authoring gate reject each with a rename hint, but a runtime datasource persisted in `sys_metadata` before the gate kept working only because the factory read leniently — and deleting that tolerance without a conversion would have silently moved data (a stored sqlite `file:` row falls back to `:memory:`). The `datasource-config-driver-key-aliases` conversion rewrites the stored shape to the canonical keys at every rehydration seam, the factory now reads exactly one spelling per key, and the four `??` chains are deleted. Driver-aware by construction: `database` renames only under sqlite, where it aliased the file path — for every other driver it is a canonical key and is untouched. Retired from the load path not for lying but because the authoring gate already rejects the spellings loudly; the chain and the stored-row replay are the seams that accept them.
6767

68-
Finishing the same datasource surface, the canonical driver id `mongo` is renamed to `mongodb` (#6345). The two spellings have both been accepted since #4410 and both still are, so no boot breaks and no data moves — what changed is which one is CANONICAL, and that string is published as `DRIVER_CATALOG.id` and is what the Studio connection form writes into `datasource.driver`. Every row written before the rename therefore carries `mongo` while the form now emits `mongodb`, leaving one deployment with two spellings of one driver and any reader that matches a stored driver against the published catalog id silently missing the older rows. The `datasource-driver-mongo-to-mongodb` conversion converges the stored value at every rehydration seam; it stays on the LIVE load path (unlike the config-key aliases beside it) precisely because `mongo` is still legal — there is no loud rejection for it to pre-empt, and nothing to lose by converging early. The rename is what let the driver-selection id and the config-contract id become one string: `packages/spec`'s driver vocabulary is now a single table both boot hosts read, which closed the last fork where `OS_DATABASE_DRIVER=pg` booted under `os start` and was refused by `os migrate`. `turso`/libSQL joins the same table with a real config contract, so a libSQL `config` is validated instead of waved through.
68+
Finishing the same datasource surface, the canonical driver id `mongo` is renamed to `mongodb`. The two spellings have both been accepted since `datasource.config` was first parsed against its driver's own contract, and both still are, so no boot breaks and no data moves — what changed is which one is CANONICAL, and that string is published as `DRIVER_CATALOG.id` and is what the Studio connection form writes into `datasource.driver`. Every row written before the rename therefore carries `mongo` while the form now emits `mongodb`, leaving one deployment with two spellings of one driver and any reader that matches a stored driver against the published catalog id silently missing the older rows. The `datasource-driver-mongo-to-mongodb` conversion converges the stored value at every rehydration seam; it stays on the LIVE load path (unlike the config-key aliases beside it) precisely because `mongo` is still legal — there is no loud rejection for it to pre-empt, and nothing to lose by converging early. The rename is what let the driver-selection id and the config-contract id become one string: `packages/spec`'s driver vocabulary is now a single table both boot hosts read, which closed the last fork where `OS_DATABASE_DRIVER=pg` booted under `os start` and was refused by `os migrate`. `turso`/libSQL joins the same table with a real config contract, so a libSQL `config` is validated instead of waved through.
6969

7070
The `script` flow node converges on its one real path (#4343). It had four ways to name what it ran and only one of them ran anything: `config.actionType: 'email' | 'slack'` were logger-backed stubs that wrote a line, reported success and delivered nothing under any configuration — with `config.template` / `.recipients` / `.variables` feeding a message no channel ever sent; inline `config.script` was recognized and never executed (the built-in runtime has no server-side JS sandbox), so the node warned and no-op'd; and every other `actionType` value was shorthand for a registered-function name, a second spelling of `config.function`. All five keys are retired and `function` becomes required, which is also what finally made the contract PARSEABLE: while the legal key set depended on `actionType`, a flat parse would either reject valid shapes or wave everything through, so `script` (with `subflow`) now runs through the same execute-time contract parse #4277 gave the flat builtins. A shorthand `actionType` CONVERTS into `function` — that is what it meant — unless `function` is already set, in which case it was dead metadata the executor never reached. The other four are dropped outright: nothing read them, so there is no value to preserve, and rebuilding the intent is an authoring decision the tombstones prescribe per branch (a `notify` node for mail — it delivers through the messaging service, the in-app inbox by default and real email once `@objectstack/plugin-email` is installed; a `connector_action` with the Slack connector, or an `http` node posting to a webhook, for Slack; a registered function for an inline body). Retired from the load path for the same reason as the rest: absorbing `actionType: 'email'` silently would let an author keep believing the flow sends mail.
7171

‎packages/cli/src/commands/migrate/account-issuer.ts‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { Command, Flags } from '@oclif/core';
44
import chalk from 'chalk';
5+
import { isMissingTableError } from '@objectstack/types';
56
import {
67
printHeader,
78
printSuccess,
@@ -128,17 +129,44 @@ export default class MigrateAccountIssuer extends Command {
128129
);
129130

130131
if (!flags.json) printStep('Scanning sys_account…');
131-
const report = await probeAccountIdentityCollisions(engine as never, {
132+
133+
// [#21552] A database with no `sys_account` table holds no account, so no
134+
// two rows collide: the probe reads it as no rows. The read is not
135+
// avoided, measured: this boot composes no `AuthPlugin`, so `sys_account`
136+
// is not a registered object and the held-back sync never lists it, and
137+
// `stack.tableAbsent('sys_account')` answers false on every database.
138+
// The refusal is therefore recognised, with the shared predicate and for
139+
// this command's own table only. ⛔ No other refused read is softened: it
140+
// still throws the probe's refusal below and is never read as clean.
141+
let noAccountTable = false;
142+
const readEngine = engine as Parameters<typeof probeAccountIdentityCollisions>[0];
143+
const readView: typeof readEngine = {
144+
find: async (object, query, options) => {
145+
try {
146+
return await readEngine.find(object, query, options);
147+
} catch (error) {
148+
if (object !== 'sys_account' || !isMissingTableError(error, object)) throw error;
149+
noAccountTable = true;
150+
return [];
151+
}
152+
},
153+
};
154+
const report = await probeAccountIdentityCollisions(readView, {
132155
...(flags['max-records'] != null ? { max: flags['max-records'] } : {}),
133156
});
157+
const noAccountTableLine = noAccountTable
158+
? 'sys_account has no table in this database yet, so no account is stored in it and it was read as no rows.'
159+
: null;
134160

135161
if (flags.json) {
162+
if (noAccountTableLine) console.error(noAccountTableLine);
136163
await emitJson({ database: stack.dbLabel, ...report, duration: timer.elapsed() });
137164
if (!report.ok) this.exit(1);
138165
return;
139166
}
140167

141168
printInfo(`Database: ${chalk.white(stack.dbLabel)}`);
169+
if (noAccountTableLine) printInfo(noAccountTableLine);
142170
console.log('');
143171
console.log(formatAccountIdentityPreflightReport(report));
144172
console.log('');

0 commit comments

Comments
 (0)