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
14 changes: 14 additions & 0 deletions .changeset/24710-ownership-refusal-text-fresh-installs-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@objectstack/metadata-core': patch
'@objectstack/metadata-protocol': patch
---

fix(metadata-core, metadata-protocol): two refusals say what 18.0 supports for a v17 database, not `os migrate organization-ownership`, which 18.0 refuses on every database

Clause-②: no (text only: no refusal code, outcome, accept set or served shape moves)

In 18.0, `os migrate organization-ownership` refuses plan and apply on every database (`NOT_IMPLEMENTED`): 18.0 supports fresh installs only. Two refusals still told the operator to run it, so following them led to a second refusal.

- **`LEGACY_ORGANIZATION_ROWS_UNREADABLE`** (`LegacyOrganizationRowsUnreadable`, `@objectstack/metadata-core`). It used to close with "Run `os migrate organization-ownership` on this database first." This read runs on every SQL database, a fresh 18.0 one included, and several of its refusals are the driver's (a dialect it cannot name, no raw-SQL seam, a column list that is not a list or comes back empty). For those, the command was never the remedy. The message now closes: "A fresh 18.0 install holds no legacy organization rows: there, the cause is the one named above, and the fix is in the SQL driver or the caller that met it. A database that does hold them is a v17 one. 18.0 supports fresh installs only and does not upgrade a v17 database: keep a v17 database on a 17.x release until the 18.x release that carries the upgrade path."
- **The seed refusal for a natural key held by a row with no organization** (`SeedLoaderService`, `@objectstack/metadata-protocol`). It used to close with "Attribute the unowned rows first (`os migrate organization-ownership`, then the same command with --apply), then re-run this seed". Even before 18.0, that bare `--apply` was refused, since the command requires `--plan-file`. The message now closes: "A row with no organization predates organization stamping, so it is a v17 database's. 18.0 supports fresh installs only and does not upgrade a v17 database: keep a v17 database on a 17.x release until the 18.x release that carries the upgrade path."
- **Unchanged:** both refusal codes, "Nothing was listed or written.", the seed refusal's opening sentences (what was refused and why), its `errored` count, and the dropped record.
22 changes: 22 additions & 0 deletions packages/metadata-core/src/legacy-organization-row.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,28 @@ describe('readLegacyOrganizationRows — fails closed', () => {
await expect(readLegacyOrganizationRows({ dialectName: 'sqlite' })).rejects.toThrow(/no raw-SQL seam/);
});

// What 18.0 supports, said to both readers: a driver-shaped refusal, which a
// fresh install meets too, and one only a database still carrying the
// column reaches. The ceremony 18.0 refuses is never the remedy.
it.each([
['driver-shaped: no raw seam (any database, a fresh one included)', { dialectName: 'sqlite' }],
['database-shaped: the column is on disk, the legacy select answered nothing', sqlDriver({ dialectName: 'sqlite', columns: V17_COLUMNS, rows: null })],
])('the refusal says what 18.0 supports, never the ceremony (%s)', async (_label, driver) => {
const error = await readLegacyOrganizationRows(driver).then(() => null, (e: unknown) => e);
expect(error).toBeInstanceOf(LegacyOrganizationRowsUnreadable);
const refusal = error as LegacyOrganizationRowsUnreadable;
expect(refusal.code).toBe('LEGACY_ORGANIZATION_ROWS_UNREADABLE');
expect(refusal.message).toContain('Nothing was listed or written.');
expect(refusal.message).toContain(
'A fresh 18.0 install holds no legacy organization rows: there, the cause is the one named above, ' +
'and the fix is in the SQL driver or the caller that met it.',
);
expect(refusal.message).toMatch(
/A database that does hold them is a v17 one\. 18\.0 supports fresh installs only and does not upgrade a v17 database: keep a v17 database on a 17\.x release until the 18\.x release that carries the upgrade path\.$/,
);
expect(refusal.message).not.toContain('os migrate organization-ownership');
});

it('a table outside the family is refused before anything runs', async () => {
const driver = sqlDriver({ dialectName: 'sqlite', columns: V17_COLUMNS, rows: [] });
await expect(readLegacyOrganizationRows(driver, 'sys_user')).rejects.toThrow(/not a sys_metadata-family table/);
Expand Down
5 changes: 4 additions & 1 deletion packages/metadata-core/src/legacy-organization-row.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ export class LegacyOrganizationRowsUnreadable extends Error {
constructor(readonly table: string, detail: string) {
super(
`Could not tell ${table}'s legacy organization rows from the environment's: ${detail}. ` +
'Nothing was listed or written. Run `os migrate organization-ownership` on this database first.',
'Nothing was listed or written. A fresh 18.0 install holds no legacy organization rows: there, the ' +
'cause is the one named above, and the fix is in the SQL driver or the caller that met it. A database ' +
'that does hold them is a v17 one. 18.0 supports fresh installs only and does not upgrade a v17 ' +
'database: keep a v17 database on a 17.x release until the 18.x release that carries the upgrade path.',
);
this.name = 'LegacyOrganizationRowsUnreadable';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,15 @@ describe('[#24301] a scoped load over a NULL-organization holder of a natural ke
const [refusal] = refusalsOf(result);
expect(refusal).toMatchObject({ sourceObject: 'tw_widget', targetField: 'sku', attemptedValue: 'W-HELD', recordIndex: 0 });
expect(refusal.message).toContain("'w_unowned'");
// What 18.0 supports, never the ceremony it refuses; the first sentences
// (what was refused, and why) stay.
expect(refusal.message).toContain('which has no organization (organization_id is NULL)');
expect(refusal.message).toContain('decided by the organization-ownership ceremony, not by a seed load.');
expect(refusal.message).toMatch(
/A row with no organization predates organization stamping, so it is a v17 database's\. 18\.0 supports fresh installs only and does not upgrade a v17 database: keep a v17 database on a 17\.x release until the 18\.x release that carries the upgrade path\.$/,
);
expect(refusal.message).not.toContain('os migrate organization-ownership');
expect(refusal.message).not.toContain('--apply');
});

it("the organization's own row wins: a key held both by it and by an unowned row is matched, not refused", async () => {
Expand Down
7 changes: 4 additions & 3 deletions packages/metadata-protocol/src/seed-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1848,9 +1848,10 @@ export class SeedLoaderService implements ISeedLoaderService {
`which has no organization (organization_id is NULL), and this seed load writes for organization ` +
`${config.organizationId}. Inserting the record would put a twin beside that row, and updating the ` +
`row would hand it to this organization: which organization an unowned row belongs to is decided by ` +
`the organization-ownership ceremony, not by a seed load. Attribute the unowned rows first ` +
`(\`os migrate organization-ownership\`, then the same command with --apply), then re-run this seed: ` +
`the row is this organization's own by then, and the upsert matches it.`,
`the organization-ownership ceremony, not by a seed load. A row with no organization predates ` +
`organization stamping, so it is a v17 database's. 18.0 supports fresh installs only and does not ` +
`upgrade a v17 database: keep a v17 database on a 17.x release until the 18.x release that carries ` +
`the upgrade path.`,
};
errors.push(error);
allErrors.push(error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,12 @@ describe('scoped seed load over NULL-organization holders (sqlite-file)', () =>
expect(kickoff.targetObject).toBe('tw_event');
expect(kickoff.targetField).toBe('subject');
expect(kickoff.attemptedValue).toBe('Kickoff');
// The message names the key, the unowned holder and the remedy's command.
// The message names the key, the unowned holder and what 18.0 supports:
// never the ceremony 18.0 refuses.
expect(kickoff.message).toContain('subject=Kickoff');
expect(kickoff.message).toContain("'ev_kickoff'");
expect(kickoff.message).toContain('os migrate organization-ownership');
expect(kickoff.message).toContain('18.0 supports fresh installs only and does not upgrade a v17 database');
expect(kickoff.message).not.toContain('os migrate organization-ownership');
expect(result.success).toBe(false);
// Loud: said at `error`, once per dataset.
const lines = logger.error.mock.calls.map((c) => String(c[0])).filter((m) => refusals.some((e) => m.includes(e.message)));
Expand Down
Loading