@scalar/workspace-store 0.47.1 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +23 -4
- package/dist/entities/auth/schema.d.ts +20 -0
- package/dist/entities/auth/schema.d.ts.map +1 -1
- package/dist/events/bus.d.ts +14 -0
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/events/bus.js +8 -0
- package/dist/events/definitions/ui.d.ts +2 -2
- package/dist/events/definitions/ui.d.ts.map +1 -1
- package/dist/persistence/index.d.ts +23 -21
- package/dist/persistence/index.d.ts.map +1 -1
- package/dist/persistence/index.js +37 -35
- package/dist/persistence/indexdb.d.ts +69 -49
- package/dist/persistence/indexdb.d.ts.map +1 -1
- package/dist/persistence/indexdb.js +100 -123
- package/dist/persistence/migrations/v1-initial.d.ts +19 -0
- package/dist/persistence/migrations/v1-initial.d.ts.map +1 -0
- package/dist/persistence/migrations/v1-initial.js +50 -0
- package/dist/persistence/migrations/v2-team-to-local.d.ts +33 -0
- package/dist/persistence/migrations/v2-team-to-local.d.ts.map +1 -0
- package/dist/persistence/migrations/v2-team-to-local.js +213 -0
- package/dist/schemas/extensions/document/x-scalar-registry-meta.d.ts +48 -0
- package/dist/schemas/extensions/document/x-scalar-registry-meta.d.ts.map +1 -1
- package/dist/schemas/extensions/document/x-scalar-registry-meta.js +33 -1
- package/dist/schemas/inmemory-workspace.d.ts +12 -0
- package/dist/schemas/inmemory-workspace.d.ts.map +1 -1
- package/dist/schemas/reference-config/index.d.ts +4 -0
- package/dist/schemas/reference-config/index.d.ts.map +1 -1
- package/dist/schemas/reference-config/settings.d.ts +4 -0
- package/dist/schemas/reference-config/settings.d.ts.map +1 -1
- package/dist/schemas/v3.1/openapi/index.d.ts +4 -0
- package/dist/schemas/v3.1/openapi/index.d.ts.map +1 -1
- package/dist/schemas/v3.1/strict/openapi-document.d.ts +144 -0
- package/dist/schemas/v3.1/strict/openapi-document.d.ts.map +1 -1
- package/dist/schemas/workspace.d.ts +12 -0
- package/dist/schemas/workspace.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -1,70 +1,90 @@
|
|
|
1
1
|
import type { Static, TObject } from '@scalar/typebox';
|
|
2
|
+
/**
|
|
3
|
+
* Declarative shape for a table at its CURRENT (latest) version.
|
|
4
|
+
*
|
|
5
|
+
* This is used purely for TypeScript typing and runtime key serialization in
|
|
6
|
+
* the wrapper API returned by `get(name)`. IndexedDB schema (object stores,
|
|
7
|
+
* keyPaths, indexes) is NOT derived from this config — every schema change is
|
|
8
|
+
* expressed in a migration. That keeps fresh installs and upgraded installs on
|
|
9
|
+
* exactly the same code path and makes the TypeScript types safe to evolve
|
|
10
|
+
* without accidentally reshaping the underlying database.
|
|
11
|
+
*/
|
|
2
12
|
type TableEntry<S extends TObject, K extends readonly (keyof Static<S>)[]> = {
|
|
3
13
|
schema: S;
|
|
4
14
|
keyPath: K;
|
|
5
|
-
indexes?: Record<string, readonly (keyof Static<S>)[]>;
|
|
6
15
|
};
|
|
7
16
|
/**
|
|
8
|
-
*
|
|
17
|
+
* Context passed to every migration. The upgrade `transaction` lives for as
|
|
18
|
+
* long as any IDB request on it is pending, so migrations may schedule async
|
|
19
|
+
* cursor / getAll work and still mutate the same transaction afterwards.
|
|
20
|
+
*/
|
|
21
|
+
export type MigrationContext = {
|
|
22
|
+
db: IDBDatabase;
|
|
23
|
+
transaction: IDBTransaction;
|
|
24
|
+
oldVersion: number;
|
|
25
|
+
newVersion: number;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* A single, atomic schema (and/or data) change.
|
|
9
29
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* @returns An object with the following methods:
|
|
15
|
-
* - `get(tableName)` — Get a wrapper to interact with the object store for the given table name.
|
|
16
|
-
* - `closeDatabase()` — Closes the database connection.
|
|
30
|
+
* Every structural change to the database — creating an object store, adding
|
|
31
|
+
* or removing an index, renaming a field, re-keying records — lives inside a
|
|
32
|
+
* migration. Fresh installs run the full chain from v1 up; existing installs
|
|
33
|
+
* run only the migrations whose position is past their current version.
|
|
17
34
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
35
|
+
* The version of a migration is its 1-based position in the `migrations`
|
|
36
|
+
* array passed to `createIndexDbConnection` — there is no `version` field to
|
|
37
|
+
* keep in sync. Append to the end to add a new migration; never reorder or
|
|
38
|
+
* insert in the middle (each position represents a real schema state that
|
|
39
|
+
* shipped to users).
|
|
22
40
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
41
|
+
* Migrations may run synchronously, or may return a Promise when they need
|
|
42
|
+
* to read existing data (via `getAll`, cursors, ...) before performing schema
|
|
43
|
+
* changes. The runner awaits each migration before starting the next, so a
|
|
44
|
+
* later migration always observes the fully-applied state of every earlier
|
|
45
|
+
* migration. To keep the upgrade transaction alive across awaits, every async
|
|
46
|
+
* migration must queue at least one IDB request before yielding.
|
|
47
|
+
*/
|
|
48
|
+
export type Migration = {
|
|
49
|
+
/** Short human-readable summary surfaced in errors / logs. */
|
|
50
|
+
description?: string;
|
|
51
|
+
/** Runs inside the upgrade transaction. May be sync or async. */
|
|
52
|
+
up: (context: MigrationContext) => void | Promise<void>;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Initializes and manages an IndexedDB database connection for table-based persistence.
|
|
29
56
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* index: ['id'] as const,
|
|
35
|
-
* },
|
|
36
|
-
* }
|
|
57
|
+
* The database version is derived from `migrations.length`, so callers cannot
|
|
58
|
+
* accidentally drift between the declared version and the migrations that
|
|
59
|
+
* define it. Every structural change — including the initial schema — must
|
|
60
|
+
* be expressed as a migration; append new ones to the end of the array.
|
|
37
61
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
62
|
+
* Example:
|
|
63
|
+
* ```ts
|
|
64
|
+
* const connection = await createIndexDbConnection({
|
|
40
65
|
* name: 'my-app-db',
|
|
41
|
-
* tables:
|
|
42
|
-
*
|
|
66
|
+
* tables: {
|
|
67
|
+
* users: { schema: UserSchema, keyPath: ['id'] as const },
|
|
68
|
+
* },
|
|
69
|
+
* migrations: [
|
|
70
|
+
* {
|
|
71
|
+
* description: 'Initial schema',
|
|
72
|
+
* up: ({ db }) => {
|
|
73
|
+
* if (!db.objectStoreNames.contains('users')) {
|
|
74
|
+
* db.createObjectStore('users', { keyPath: 'id' })
|
|
75
|
+
* }
|
|
76
|
+
* },
|
|
77
|
+
* },
|
|
78
|
+
* ],
|
|
43
79
|
* })
|
|
44
|
-
*
|
|
45
|
-
* // Get a strongly-typed users table API
|
|
46
|
-
* const usersTable = get('users')
|
|
47
|
-
*
|
|
48
|
-
* // Add a user
|
|
49
|
-
* await usersTable.addItem({ id: 'user-1' }, { name: 'Alice', age: 25 })
|
|
50
|
-
*
|
|
51
|
-
* // Retrieve a user by id
|
|
52
|
-
* const user = await usersTable.getItem({ id: 'user-1' })
|
|
53
|
-
*
|
|
54
|
-
* // Don't forget to close the database when done!
|
|
55
|
-
* closeDatabase()
|
|
56
80
|
* ```
|
|
57
81
|
*/
|
|
58
|
-
export declare const createIndexDbConnection: <T extends Record<string, TableEntry<any, readonly (keyof any)[]>>>({ name, tables,
|
|
82
|
+
export declare const createIndexDbConnection: <T extends Record<string, TableEntry<any, readonly (keyof any)[]>>>({ name, tables, migrations, }: {
|
|
59
83
|
name: string;
|
|
60
84
|
tables: T;
|
|
61
|
-
|
|
62
|
-
migrations?: {
|
|
63
|
-
version: number;
|
|
64
|
-
exec: (db: IDBDatabase, event: IDBVersionChangeEvent) => {};
|
|
65
|
-
}[];
|
|
85
|
+
migrations: readonly Migration[];
|
|
66
86
|
}) => Promise<{
|
|
67
|
-
get: <Name extends keyof T>(
|
|
87
|
+
get: <Name extends keyof T>(tableName: Name) => {
|
|
68
88
|
addItem: (key: Record<T[Name]["keyPath"][number], IDBValidKey>, value: Omit<(T[Name]["schema"] & {
|
|
69
89
|
params: [];
|
|
70
90
|
})["static"], T[Name]["keyPath"][number]>) => Promise<(T[Name]["schema"] & {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"indexdb.d.ts","sourceRoot":"","sources":["../../src/persistence/indexdb.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAW,MAAM,iBAAiB,CAAA;AAE/D,KAAK,UAAU,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,SAAS,SAAS,CAAC,MAAM,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI;IAC3E,MAAM,EAAE,CAAC,CAAA;IACT,OAAO,EAAE,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"indexdb.d.ts","sourceRoot":"","sources":["../../src/persistence/indexdb.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAW,MAAM,iBAAiB,CAAA;AAE/D;;;;;;;;;GASG;AACH,KAAK,UAAU,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,SAAS,SAAS,CAAC,MAAM,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI;IAC3E,MAAM,EAAE,CAAC,CAAA;IACT,OAAO,EAAE,CAAC,CAAA;CACX,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,EAAE,WAAW,CAAA;IACf,WAAW,EAAE,cAAc,CAAA;IAC3B,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iEAAiE;IACjE,EAAE,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACxD,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,uBAAuB,GAAU,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,EAAE,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,+BAI9G;IACD,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,CAAC,CAAA;IACT,UAAU,EAAE,SAAS,SAAS,EAAE,CAAA;CACjC;UAuFS,IAAI,SAAS,MAAM,CAAC,aAAa,IAAI;;;;;;;;;+BAiEf,WAAW,EAAE,cAAc,MAAM;;;8EA6BP,OAAO,CAAC,IAAI,CAAC;kCAWpC,WAAW,EAAE,KAAG,OAAO,CAAC,MAAM,CAAC;;;;yBA4BpC,OAAO,CAAC,IAAI,CAAC;;;EAxH1C,CAAA"}
|
|
@@ -1,127 +1,123 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Initializes and manages an IndexedDB database connection for table-based persistence.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* @returns An object with the following methods:
|
|
9
|
-
* - `get(tableName)` — Get a wrapper to interact with the object store for the given table name.
|
|
10
|
-
* - `closeDatabase()` — Closes the database connection.
|
|
4
|
+
* The database version is derived from `migrations.length`, so callers cannot
|
|
5
|
+
* accidentally drift between the declared version and the migrations that
|
|
6
|
+
* define it. Every structural change — including the initial schema — must
|
|
7
|
+
* be expressed as a migration; append new ones to the end of the array.
|
|
11
8
|
*
|
|
12
|
-
* Example
|
|
9
|
+
* Example:
|
|
13
10
|
* ```ts
|
|
14
|
-
*
|
|
15
|
-
* import { createIndexDbConnection } from './indexdb.js'
|
|
16
|
-
*
|
|
17
|
-
* // Define a schema for a user
|
|
18
|
-
* const UserSchema = Type.Object({
|
|
19
|
-
* id: Type.String(),
|
|
20
|
-
* name: Type.String(),
|
|
21
|
-
* age: Type.Number(),
|
|
22
|
-
* })
|
|
23
|
-
*
|
|
24
|
-
* // Define tables in the database
|
|
25
|
-
* const dbConfig = {
|
|
26
|
-
* users: {
|
|
27
|
-
* schema: UserSchema,
|
|
28
|
-
* index: ['id'] as const,
|
|
29
|
-
* },
|
|
30
|
-
* }
|
|
31
|
-
*
|
|
32
|
-
* // Open the database connection and get table API
|
|
33
|
-
* const { get, closeDatabase } = await createIndexDbConnection({
|
|
11
|
+
* const connection = await createIndexDbConnection({
|
|
34
12
|
* name: 'my-app-db',
|
|
35
|
-
* tables:
|
|
36
|
-
*
|
|
13
|
+
* tables: {
|
|
14
|
+
* users: { schema: UserSchema, keyPath: ['id'] as const },
|
|
15
|
+
* },
|
|
16
|
+
* migrations: [
|
|
17
|
+
* {
|
|
18
|
+
* description: 'Initial schema',
|
|
19
|
+
* up: ({ db }) => {
|
|
20
|
+
* if (!db.objectStoreNames.contains('users')) {
|
|
21
|
+
* db.createObjectStore('users', { keyPath: 'id' })
|
|
22
|
+
* }
|
|
23
|
+
* },
|
|
24
|
+
* },
|
|
25
|
+
* ],
|
|
37
26
|
* })
|
|
38
|
-
*
|
|
39
|
-
* // Get a strongly-typed users table API
|
|
40
|
-
* const usersTable = get('users')
|
|
41
|
-
*
|
|
42
|
-
* // Add a user
|
|
43
|
-
* await usersTable.addItem({ id: 'user-1' }, { name: 'Alice', age: 25 })
|
|
44
|
-
*
|
|
45
|
-
* // Retrieve a user by id
|
|
46
|
-
* const user = await usersTable.getItem({ id: 'user-1' })
|
|
47
|
-
*
|
|
48
|
-
* // Don't forget to close the database when done!
|
|
49
|
-
* closeDatabase()
|
|
50
27
|
* ```
|
|
51
28
|
*/
|
|
52
|
-
export const createIndexDbConnection = async ({ name = 'scalar-workspace-store', tables,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
29
|
+
export const createIndexDbConnection = async ({ name = 'scalar-workspace-store', tables, migrations, }) => {
|
|
30
|
+
if (migrations.length === 0) {
|
|
31
|
+
throw new Error(`createIndexDbConnection("${name}"): at least one migration is required. The initial schema must be defined as the first migration.`);
|
|
32
|
+
}
|
|
33
|
+
// The 1-based array position is the schema version. `migrations[0]` is v1,
|
|
34
|
+
// `migrations[1]` is v2, and so on. The latest version is just the length.
|
|
35
|
+
const latestVersion = migrations.length;
|
|
36
|
+
const request = indexedDB.open(name, latestVersion);
|
|
37
|
+
// Captured here so the descriptive error from a failing migration can be
|
|
38
|
+
// surfaced through the `open` promise instead of the generic IDB
|
|
39
|
+
// `AbortError` that follows `transaction.abort()`.
|
|
40
|
+
let migrationError;
|
|
41
|
+
request.onupgradeneeded = (event) => {
|
|
42
|
+
const transaction = request.transaction;
|
|
43
|
+
if (!transaction) {
|
|
44
|
+
// IDB always provides the upgrade transaction here; this is a guard for
|
|
45
|
+
// exotic environments and keeps types honest.
|
|
46
|
+
return;
|
|
70
47
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
48
|
+
const context = {
|
|
49
|
+
db: request.result,
|
|
50
|
+
transaction,
|
|
51
|
+
oldVersion: event.oldVersion,
|
|
52
|
+
newVersion: event.newVersion ?? latestVersion,
|
|
53
|
+
};
|
|
54
|
+
// Run pending migrations sequentially, awaiting any async work before
|
|
55
|
+
// starting the next one. This is important when a migration reads
|
|
56
|
+
// existing data via IDB requests (e.g. `getAll`) and only performs the
|
|
57
|
+
// real schema changes inside the request callback — the next migration
|
|
58
|
+
// would otherwise execute against the pre-migration state.
|
|
59
|
+
//
|
|
60
|
+
// The upgrade transaction stays alive across awaits because every async
|
|
61
|
+
// migration in the codebase queues at least one IDB request before
|
|
62
|
+
// yielding, and microtasks complete before IDB checks for transaction
|
|
63
|
+
// commit at the next task boundary.
|
|
64
|
+
const runMigrations = async () => {
|
|
65
|
+
for (const [index, migration] of migrations.entries()) {
|
|
66
|
+
const version = index + 1;
|
|
67
|
+
if (version <= event.oldVersion) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
await migration.up(context);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
const label = migration.description ? `v${version} (${migration.description})` : `v${version}`;
|
|
75
|
+
throw new Error(`Migration ${label} failed: ${error?.message ?? error}`, { cause: error });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
runMigrations().catch((error) => {
|
|
80
|
+
migrationError = error;
|
|
81
|
+
// Abort the upgrade transaction so we do not leave the DB in a half-
|
|
82
|
+
// migrated state. Aborting fires `request.onerror`; the captured
|
|
83
|
+
// `migrationError` takes precedence over the resulting `AbortError`.
|
|
84
|
+
try {
|
|
85
|
+
transaction.abort();
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// The transaction may already be in a finished state (e.g. when the
|
|
89
|
+
// failing migration itself triggered an abort). Nothing to do.
|
|
75
90
|
}
|
|
76
91
|
});
|
|
77
92
|
};
|
|
78
93
|
await new Promise((resolve, reject) => {
|
|
79
|
-
|
|
80
|
-
|
|
94
|
+
request.onsuccess = () => resolve(true);
|
|
95
|
+
request.onerror = () => reject(migrationError ?? request.error);
|
|
96
|
+
// If another tab holds an older-version connection open we would otherwise
|
|
97
|
+
// hang forever waiting for the upgrade. Surface it as a clear rejection so
|
|
98
|
+
// the app can react (reload, notify the user, ...) instead of freezing.
|
|
99
|
+
request.onblocked = () => reject(new Error(`IndexedDB upgrade for "${name}" is blocked by another open connection. Close other tabs and try again.`));
|
|
81
100
|
});
|
|
82
101
|
return {
|
|
83
|
-
get: (
|
|
84
|
-
|
|
102
|
+
get: (tableName) => {
|
|
103
|
+
// Surface a helpful error if a caller asks for a table that is not in
|
|
104
|
+
// the typed config — the underlying IDB call would otherwise throw a
|
|
105
|
+
// generic `NotFoundError` from a lazy `transaction()`.
|
|
106
|
+
if (!Object.hasOwn(tables, tableName)) {
|
|
107
|
+
throw new Error(`Unknown table "${String(tableName)}". Add it to the \`tables\` config of "${name}".`);
|
|
108
|
+
}
|
|
109
|
+
return createTableWrapper(tableName, request.result);
|
|
85
110
|
},
|
|
86
111
|
closeDatabase: () => {
|
|
87
|
-
|
|
112
|
+
request.result.close();
|
|
88
113
|
},
|
|
89
114
|
};
|
|
90
115
|
};
|
|
91
116
|
/**
|
|
92
117
|
* Utility wrapper for interacting with an IndexedDB object store, typed by the schema.
|
|
93
118
|
*
|
|
94
|
-
* Usage example:
|
|
95
|
-
* ```
|
|
96
|
-
* // Define a TypeBox schema for users
|
|
97
|
-
* const UserSchema = Type.Object({
|
|
98
|
-
* id: Type.String(),
|
|
99
|
-
* name: Type.String(),
|
|
100
|
-
* age: Type.Number(),
|
|
101
|
-
* })
|
|
102
|
-
*
|
|
103
|
-
* // Open or create the users table
|
|
104
|
-
* const usersTable = createTableWrapper<typeof UserSchema, 'id'>('users', openDatabase)
|
|
105
|
-
*
|
|
106
|
-
* // Add a user
|
|
107
|
-
await usersTable.addItem({ id: 'user-1' }, { name: 'Alice', age: 24 })
|
|
108
|
-
*
|
|
109
|
-
* // Get a user by id
|
|
110
|
-
* const alic = await usersTable.getItem({ id: 'user-1' })
|
|
111
|
-
*
|
|
112
|
-
* // Get users with a partial key (use [] if no composite key)
|
|
113
|
-
* const users = await usersTable.getRange(['user-1'])
|
|
114
|
-
*
|
|
115
|
-
* // Get all users
|
|
116
|
-
* const allUsers = await usersTable.getAll()
|
|
117
|
-
* ```
|
|
118
|
-
*
|
|
119
119
|
* @template T TypeBox schema type for objects in the store
|
|
120
120
|
* @template K Key property names that compose the primary key
|
|
121
|
-
*
|
|
122
|
-
* @param name - Object store name
|
|
123
|
-
* @param getDb - Function returning a Promise for the IDBDatabase
|
|
124
|
-
* @returns Methods to interact with the object store
|
|
125
121
|
*/
|
|
126
122
|
function createTableWrapper(name, db) {
|
|
127
123
|
/**
|
|
@@ -159,12 +155,6 @@ function createTableWrapper(name, db) {
|
|
|
159
155
|
* Returns all records matching a partial (prefix) key. Use for composite keys.
|
|
160
156
|
* For non-compound keys, pass single-element array: getRange(['some-id'])
|
|
161
157
|
* For prefix search, pass subset of key parts.
|
|
162
|
-
* @param partialKey - Array of partial key values
|
|
163
|
-
* @returns Matching objects
|
|
164
|
-
*
|
|
165
|
-
* Example (composite [a,b]):
|
|
166
|
-
* getRange(['foo']) // All with a === 'foo'
|
|
167
|
-
* getRange(['foo', 'bar']) // All with a === 'foo' and b === 'bar'
|
|
168
158
|
*/
|
|
169
159
|
function getRange(partialKey, indexName) {
|
|
170
160
|
const store = getStore('readonly');
|
|
@@ -175,9 +165,9 @@ function createTableWrapper(name, db) {
|
|
|
175
165
|
upperBound.push([]); // ensures upper bound includes all keys with this prefix
|
|
176
166
|
const range = IDBKeyRange.bound(partialKey, upperBound, false, true);
|
|
177
167
|
return new Promise((resolve, reject) => {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
168
|
+
const req = objectStoreOrIndex.openCursor(range);
|
|
169
|
+
req.onerror = () => reject(req.error);
|
|
170
|
+
req.onsuccess = (event) => {
|
|
181
171
|
const cursor = event.target.result;
|
|
182
172
|
if (cursor) {
|
|
183
173
|
results.push(cursor.value);
|
|
@@ -191,8 +181,6 @@ function createTableWrapper(name, db) {
|
|
|
191
181
|
}
|
|
192
182
|
/**
|
|
193
183
|
* Deletes an item from the store by its composite key.
|
|
194
|
-
* @param key - Key values. For a single key: { id: '...' }
|
|
195
|
-
* @returns void
|
|
196
184
|
*/
|
|
197
185
|
async function deleteItem(key) {
|
|
198
186
|
const store = getStore('readwrite');
|
|
@@ -203,14 +191,6 @@ function createTableWrapper(name, db) {
|
|
|
203
191
|
}
|
|
204
192
|
/**
|
|
205
193
|
* Deletes all records matching a partial (prefix) key. Use for composite keys.
|
|
206
|
-
* For non-compound keys, pass single-element array: deleteRange(['some-id'])
|
|
207
|
-
* For prefix deletion, pass subset of key parts.
|
|
208
|
-
* @param partialKey - Array of partial key values
|
|
209
|
-
* @returns Number of deleted items
|
|
210
|
-
*
|
|
211
|
-
* Example (composite [a,b]):
|
|
212
|
-
* deleteRange(['foo']) // Delete all with a === 'foo'
|
|
213
|
-
* deleteRange(['foo', 'bar']) // Delete all with a === 'foo' and b === 'bar'
|
|
214
194
|
*/
|
|
215
195
|
function deleteRange(partialKey) {
|
|
216
196
|
const store = getStore('readwrite');
|
|
@@ -220,9 +200,9 @@ function createTableWrapper(name, db) {
|
|
|
220
200
|
upperBound.push([]); // ensures upper bound includes all keys with this prefix
|
|
221
201
|
const range = IDBKeyRange.bound(partialKey, upperBound, false, true);
|
|
222
202
|
return new Promise((resolve, reject) => {
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
203
|
+
const req = store.openCursor(range);
|
|
204
|
+
req.onerror = () => reject(req.error);
|
|
205
|
+
req.onsuccess = (event) => {
|
|
226
206
|
const cursor = event.target.result;
|
|
227
207
|
if (cursor) {
|
|
228
208
|
cursor.delete();
|
|
@@ -237,7 +217,6 @@ function createTableWrapper(name, db) {
|
|
|
237
217
|
}
|
|
238
218
|
/**
|
|
239
219
|
* Deletes all items from the table.
|
|
240
|
-
* @returns void
|
|
241
220
|
*/
|
|
242
221
|
async function deleteAll() {
|
|
243
222
|
const store = getStore('readwrite');
|
|
@@ -245,7 +224,6 @@ function createTableWrapper(name, db) {
|
|
|
245
224
|
}
|
|
246
225
|
/**
|
|
247
226
|
* Retrieves all items from the table.
|
|
248
|
-
* @returns Array of all objects in the store
|
|
249
227
|
*/
|
|
250
228
|
function getAll() {
|
|
251
229
|
const store = getStore('readonly');
|
|
@@ -261,7 +239,6 @@ function createTableWrapper(name, db) {
|
|
|
261
239
|
deleteAll,
|
|
262
240
|
};
|
|
263
241
|
}
|
|
264
|
-
// ---- Utility ----
|
|
265
242
|
function requestAsPromise(req) {
|
|
266
243
|
return new Promise((resolve, reject) => {
|
|
267
244
|
req.onsuccess = () => resolve(req.result);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Migration } from '../../persistence/indexdb.js';
|
|
2
|
+
/**
|
|
3
|
+
* v1 — initial schema for the workspace store.
|
|
4
|
+
*
|
|
5
|
+
* This migration defines the database as it first shipped: a `workspace`
|
|
6
|
+
* object store keyed by `[namespace, slug]` with a `teamUid` index, a `meta`
|
|
7
|
+
* store keyed by `workspaceId`, and six per-document chunk stores keyed by
|
|
8
|
+
* `[workspaceId, documentName]`.
|
|
9
|
+
*
|
|
10
|
+
* Every installation — fresh or upgraded — runs this migration. Fresh installs
|
|
11
|
+
* execute the full chain starting here, then each subsequent migration
|
|
12
|
+
* transforms the schema into the latest shape. Upgraded installs are already
|
|
13
|
+
* past v1 and skip it.
|
|
14
|
+
*
|
|
15
|
+
* Intentionally uses the ORIGINAL field names (including `teamUid`); later
|
|
16
|
+
* migrations are free to rename, drop, or reshape them.
|
|
17
|
+
*/
|
|
18
|
+
export declare const v1InitialMigration: Migration;
|
|
19
|
+
//# sourceMappingURL=v1-initial.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v1-initial.d.ts","sourceRoot":"","sources":["../../../src/persistence/migrations/v1-initial.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AAmBtD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAoBhC,CAAA"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tables that store per-workspace chunks keyed by `workspaceId` (single key).
|
|
3
|
+
*/
|
|
4
|
+
const SINGLE_KEY_CHUNK_TABLES = ['meta'];
|
|
5
|
+
/**
|
|
6
|
+
* Tables that store per-document chunks keyed by `[workspaceId, documentName]`.
|
|
7
|
+
*/
|
|
8
|
+
const COMPOSITE_KEY_CHUNK_TABLES = [
|
|
9
|
+
'documents',
|
|
10
|
+
'originalDocuments',
|
|
11
|
+
'intermediateDocuments',
|
|
12
|
+
'overrides',
|
|
13
|
+
'history',
|
|
14
|
+
'auth',
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* v1 — initial schema for the workspace store.
|
|
18
|
+
*
|
|
19
|
+
* This migration defines the database as it first shipped: a `workspace`
|
|
20
|
+
* object store keyed by `[namespace, slug]` with a `teamUid` index, a `meta`
|
|
21
|
+
* store keyed by `workspaceId`, and six per-document chunk stores keyed by
|
|
22
|
+
* `[workspaceId, documentName]`.
|
|
23
|
+
*
|
|
24
|
+
* Every installation — fresh or upgraded — runs this migration. Fresh installs
|
|
25
|
+
* execute the full chain starting here, then each subsequent migration
|
|
26
|
+
* transforms the schema into the latest shape. Upgraded installs are already
|
|
27
|
+
* past v1 and skip it.
|
|
28
|
+
*
|
|
29
|
+
* Intentionally uses the ORIGINAL field names (including `teamUid`); later
|
|
30
|
+
* migrations are free to rename, drop, or reshape them.
|
|
31
|
+
*/
|
|
32
|
+
export const v1InitialMigration = {
|
|
33
|
+
description: 'Initial schema: workspace + chunk tables',
|
|
34
|
+
up: ({ db }) => {
|
|
35
|
+
if (!db.objectStoreNames.contains('workspace')) {
|
|
36
|
+
const workspace = db.createObjectStore('workspace', { keyPath: ['namespace', 'slug'] });
|
|
37
|
+
workspace.createIndex('teamUid', ['teamUid']);
|
|
38
|
+
}
|
|
39
|
+
for (const tableName of SINGLE_KEY_CHUNK_TABLES) {
|
|
40
|
+
if (!db.objectStoreNames.contains(tableName)) {
|
|
41
|
+
db.createObjectStore(tableName, { keyPath: 'workspaceId' });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const tableName of COMPOSITE_KEY_CHUNK_TABLES) {
|
|
45
|
+
if (!db.objectStoreNames.contains(tableName)) {
|
|
46
|
+
db.createObjectStore(tableName, { keyPath: ['workspaceId', 'documentName'] });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Migration } from '../../persistence/indexdb.js';
|
|
2
|
+
type WorkspaceRecordV1 = {
|
|
3
|
+
name: string;
|
|
4
|
+
/** Old field — dropped entirely in v2. */
|
|
5
|
+
teamUid?: string;
|
|
6
|
+
namespace: string;
|
|
7
|
+
slug: string;
|
|
8
|
+
};
|
|
9
|
+
type WorkspaceRecordV2 = {
|
|
10
|
+
name: string;
|
|
11
|
+
teamSlug: string;
|
|
12
|
+
slug: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Picks a slug that does not collide with anything in `taken`.
|
|
16
|
+
* Falls back to `<slug>-2`, `<slug>-3`, ... when the desired slug is already used.
|
|
17
|
+
*/
|
|
18
|
+
export declare const pickUniqueSlug: (desired: string, taken: ReadonlySet<string>) => string;
|
|
19
|
+
/**
|
|
20
|
+
* Computes the new shape for every workspace, preserving local entries under
|
|
21
|
+
* their existing slug and relocating team entries into the local team with a
|
|
22
|
+
* unique slug when needed.
|
|
23
|
+
*/
|
|
24
|
+
export declare const planWorkspaceMigration: (workspaces: readonly WorkspaceRecordV1[]) => Array<{
|
|
25
|
+
before: {
|
|
26
|
+
namespace: string;
|
|
27
|
+
slug: string;
|
|
28
|
+
};
|
|
29
|
+
after: WorkspaceRecordV2;
|
|
30
|
+
}>;
|
|
31
|
+
export declare const v2TeamToLocalMigration: Migration;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=v2-team-to-local.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v2-team-to-local.d.ts","sourceRoot":"","sources":["../../../src/persistence/migrations/v2-team-to-local.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AA2CtD,KAAK,iBAAiB,GAAG;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,KAAK,iBAAiB,GAAG;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,MAAM,EAAE,OAAO,WAAW,CAAC,MAAM,CAAC,KAAG,MAU5E,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,GACjC,YAAY,SAAS,iBAAiB,EAAE,KACvC,KAAK,CAAC;IAAE,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,KAAK,EAAE,iBAAiB,CAAA;CAAE,CAqCjF,CAAA;AAqHD,eAAO,MAAM,sBAAsB,EAAE,SAqCpC,CAAA"}
|