Skip to content
Open
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
24 changes: 24 additions & 0 deletions .changeset/migrate-project-id-guard-doubled-sentence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"@objectstack/metadata": patch
---

fix(metadata): `migrateProjectIdToEnvironmentId`'s raw-driver guard stated its instruction sentence twice (#13219)

An operator who called `migrateProjectIdToEnvironmentId` with a driver that has
no `raw()` was refused correctly, but read the same remedy twice in one message:

```
migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. SqlDriver (better-sqlite3/knex) supports this; cloud-side TursoDriver also conforms.
```

The sentence was concatenated twice, a copy-paste artifact — the sibling
`migrateEnvIdToProjectId` carries the correct single-sentence form of the
identical guard. Cosmetic and operator-facing only: the guard fires on exactly
the same condition, the remedy it names is unchanged, and nothing parses the
message. The duplicate line is deleted; the surviving sentence keeps the
trailing space that separates it from the one naming the conforming drivers.

The refusal case in the package's tests now pins the properties — the
instruction appears exactly once, no sentence runs into the next, and the
supporting sentence is still present — rather than substring-matching the
message, which could not see a second copy and so passed either way.
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,37 @@ describe('migrateProjectIdToEnvironmentId — behaviour against a physically-sta
expect(statements.filter((s) => s.startsWith('ALTER TABLE'))).toEqual([]);
});

it('still refuses a driver without .raw()', async () => {
await expect(migrateProjectIdToEnvironmentId({} as any)).rejects.toThrow(
/must expose a \.raw\(sql, bindings\?\) method/,
it('still refuses a driver without .raw(), stating the remedy exactly once', async () => {
// #13219 — the guard concatenated its instruction sentence TWICE, so an
// operator with a raw-less driver read the same remedy twice in one
// message. The assertion that used to stand here
// (`rejects.toThrow(/must expose a \.raw\(sql, bindings\?\) method/)`)
// passed either way: a substring match cannot see a second copy. So
// these pin the PROPERTIES of the assembled message, not a full-string
// copy of today's wording.
const outcome: unknown = await migrateProjectIdToEnvironmentId({} as any).then(
(value) => value,
(error: unknown) => error,
);
expect(outcome, 'a driver with no .raw() must be refused').toBeInstanceOf(Error);
const message = (outcome as Error).message;

// 1. The remedy is stated exactly ONCE. Counted, not compared, so a
// later rewording of the sentence still leaves this asserting.
const instruction = /driver must expose a \.raw\(sql, bindings\?\) method\./g;
expect(message.match(instruction) ?? []).toHaveLength(1);

// 2. ...and the sentences stay SEPARATED. Deleting the duplicate by
// trimming the surviving line's trailing space would satisfy (1)
// while gluing `method.SqlDriver` — this defect inverted, so it is
// pinned in the same case. A run-together sentence boundary is a
// lowercase letter, a period, then a capital; the `.raw(` in the
// text is lowercase-after-period and so is correctly not one.
expect(message).not.toMatch(/[a-z]\.[A-Z]/);

// 3. Non-vacuity: deleting the SUPPORTING sentence instead would also
// satisfy (1) and (2). It names the drivers that do conform, which
// is the half of the message an operator acts on.
expect(message).toMatch(/SqlDriver/);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ export async function migrateProjectIdToEnvironmentId(

if (typeof driverAny.raw !== 'function') {
throw new Error(
'migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. ' +
'migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. ' +
'SqlDriver (better-sqlite3/knex) supports this; cloud-side TursoDriver also conforms.'
);
Expand Down
Loading