@scalar/workspace-store 0.49.2 → 0.51.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 +114 -0
- package/dist/client.d.ts +2 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +38 -15
- package/dist/entities/auth/schema.d.ts +10 -5
- package/dist/entities/auth/schema.d.ts.map +1 -1
- package/dist/events/bus.d.ts +70 -0
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/events/bus.js +48 -11
- package/dist/events/definitions/analytics.d.ts +0 -12
- package/dist/events/definitions/analytics.d.ts.map +1 -1
- package/dist/events/definitions/auth.d.ts +44 -6
- package/dist/events/definitions/auth.d.ts.map +1 -1
- package/dist/events/definitions/index.d.ts +3 -2
- package/dist/events/definitions/index.d.ts.map +1 -1
- package/dist/events/definitions/log.d.ts +18 -0
- package/dist/events/definitions/log.d.ts.map +1 -0
- package/dist/events/definitions/log.js +1 -0
- package/dist/events/definitions/operation.d.ts +1 -1
- package/dist/events/definitions/operation.d.ts.map +1 -1
- package/dist/events/definitions/ui.d.ts +18 -1
- package/dist/events/definitions/ui.d.ts.map +1 -1
- package/dist/events/index.d.ts +1 -1
- package/dist/events/index.d.ts.map +1 -1
- package/dist/helpers/get-resolved-ref.d.ts +6 -0
- package/dist/helpers/get-resolved-ref.d.ts.map +1 -1
- package/dist/helpers/get-resolved-ref.js +9 -0
- package/dist/helpers/is-hidden.d.ts +8 -0
- package/dist/helpers/is-hidden.d.ts.map +1 -0
- package/dist/helpers/is-hidden.js +5 -0
- package/dist/mutators/auth.d.ts +22 -3
- package/dist/mutators/auth.d.ts.map +1 -1
- package/dist/mutators/auth.js +213 -37
- package/dist/mutators/cookie.d.ts.map +1 -1
- package/dist/mutators/cookie.js +3 -2
- package/dist/mutators/document.d.ts.map +1 -1
- package/dist/mutators/document.js +5 -4
- package/dist/mutators/environment.d.ts.map +1 -1
- package/dist/mutators/environment.js +12 -5
- package/dist/mutators/index.d.ts +4 -0
- package/dist/mutators/index.d.ts.map +1 -1
- package/dist/mutators/operation/body.d.ts.map +1 -1
- package/dist/mutators/operation/body.js +6 -2
- package/dist/mutators/operation/extensions.d.ts.map +1 -1
- package/dist/mutators/operation/extensions.js +5 -1
- package/dist/mutators/operation/history.d.ts.map +1 -1
- package/dist/mutators/operation/history.js +7 -3
- package/dist/mutators/operation/operation.d.ts.map +1 -1
- package/dist/mutators/operation/operation.js +15 -10
- package/dist/mutators/operation/parameters.d.ts.map +1 -1
- package/dist/mutators/operation/parameters.js +12 -5
- package/dist/mutators/server.d.ts.map +1 -1
- package/dist/mutators/server.js +2 -1
- package/dist/mutators/tag.d.ts.map +1 -1
- package/dist/mutators/tag.js +9 -4
- package/dist/navigation/helpers/get-openapi-object.d.ts.map +1 -1
- package/dist/navigation/helpers/get-openapi-object.js +5 -0
- package/dist/navigation/helpers/traverse-paths.d.ts.map +1 -1
- package/dist/navigation/helpers/traverse-paths.js +4 -3
- package/dist/navigation/helpers/traverse-schemas.d.ts.map +1 -1
- package/dist/navigation/helpers/traverse-schemas.js +9 -4
- package/dist/navigation/helpers/traverse-tags.d.ts.map +1 -1
- package/dist/navigation/helpers/traverse-tags.js +2 -1
- package/dist/navigation/helpers/traverse-webhooks.d.ts.map +1 -1
- package/dist/navigation/helpers/traverse-webhooks.js +4 -3
- package/dist/navigation/helpers/update-order-ids.d.ts.map +1 -1
- package/dist/navigation/helpers/update-order-ids.js +4 -1
- package/dist/persistence/index.d.ts +123 -80
- package/dist/persistence/index.d.ts.map +1 -1
- package/dist/persistence/index.js +233 -167
- package/dist/persistence/migrations/v2-team-to-local.d.ts +22 -5
- package/dist/persistence/migrations/v2-team-to-local.d.ts.map +1 -1
- package/dist/persistence/migrations/v2-team-to-local.js +195 -137
- package/dist/request-example/builder/body/build-request-body.d.ts.map +1 -1
- package/dist/request-example/builder/body/build-request-body.js +48 -5
- package/dist/request-example/builder/build-request.d.ts +24 -3
- package/dist/request-example/builder/build-request.d.ts.map +1 -1
- package/dist/request-example/builder/build-request.js +89 -18
- package/dist/request-example/builder/index.d.ts +2 -1
- package/dist/request-example/builder/index.d.ts.map +1 -1
- package/dist/request-example/builder/index.js +2 -1
- package/dist/request-example/builder/request-factory.d.ts.map +1 -1
- package/dist/request-example/builder/request-factory.js +5 -8
- package/dist/request-example/builder/resolve-request-factory-url.d.ts +18 -1
- package/dist/request-example/builder/resolve-request-factory-url.d.ts.map +1 -1
- package/dist/request-example/builder/resolve-request-factory-url.js +29 -4
- package/dist/request-example/context/environment.d.ts.map +1 -1
- package/dist/request-example/context/environment.js +2 -1
- package/dist/request-example/context/get-request-example-context.d.ts.map +1 -1
- package/dist/request-example/context/get-request-example-context.js +7 -0
- package/dist/request-example/context/headers.d.ts +28 -13
- package/dist/request-example/context/headers.d.ts.map +1 -1
- package/dist/request-example/context/headers.js +84 -19
- package/dist/request-example/context/index.d.ts +1 -0
- package/dist/request-example/context/index.d.ts.map +1 -1
- package/dist/request-example/context/index.js +1 -0
- package/dist/request-example/context/security/get-selected-security.d.ts.map +1 -1
- package/dist/request-example/context/security/get-selected-security.js +3 -6
- package/dist/request-example/context/servers.d.ts.map +1 -1
- package/dist/request-example/context/servers.js +3 -3
- package/dist/request-example/index.d.ts +3 -3
- package/dist/request-example/index.d.ts.map +1 -1
- package/dist/request-example/index.js +2 -2
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +1 -8
- package/dist/schemas/asyncapi/asyncapi-document.d.ts +79 -0
- package/dist/schemas/asyncapi/asyncapi-document.d.ts.map +1 -0
- package/dist/schemas/asyncapi/asyncapi-document.js +58 -0
- package/dist/schemas/extensions/document/workspace-managed-extensions.d.ts +25 -0
- package/dist/schemas/extensions/document/workspace-managed-extensions.d.ts.map +1 -0
- package/dist/schemas/extensions/document/workspace-managed-extensions.js +26 -0
- package/dist/schemas/inmemory-workspace.d.ts +3 -4631
- package/dist/schemas/inmemory-workspace.d.ts.map +1 -1
- package/dist/schemas/inmemory-workspace.js +1 -15
- package/dist/schemas/reference-config/index.d.ts +3 -2
- package/dist/schemas/reference-config/index.d.ts.map +1 -1
- package/dist/schemas/reference-config/settings.d.ts +2 -1
- package/dist/schemas/reference-config/settings.d.ts.map +1 -1
- package/dist/schemas/type-guards.d.ts +24 -0
- package/dist/schemas/type-guards.d.ts.map +1 -0
- package/dist/schemas/type-guards.js +35 -0
- package/dist/schemas/v3.1/openapi/index.d.ts +2 -1
- package/dist/schemas/v3.1/openapi/index.d.ts.map +1 -1
- package/dist/schemas/v3.1/openapi/index.js +3 -3
- package/dist/schemas/v3.1/strict/openapi-document.d.ts +74 -39
- package/dist/schemas/v3.1/strict/openapi-document.d.ts.map +1 -1
- package/dist/schemas/v3.1/strict/openapi-document.js +6 -2
- package/dist/schemas/workspace-specification/index.d.ts +1 -1
- package/dist/schemas/workspace.d.ts +15 -4377
- package/dist/schemas/workspace.d.ts.map +1 -1
- package/dist/schemas/workspace.js +13 -8
- package/dist/schemas.d.ts +3 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +3 -1
- package/package.json +6 -6
|
@@ -1,33 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* v2 —
|
|
2
|
+
* v2 — move to UID-based identity and collapse every workspace into the
|
|
3
|
+
* local team.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Server-side slugs (both team and workspace) can change at any time and
|
|
6
|
+
* there is no reliable way for the client to map a stale slug back to its
|
|
7
|
+
* canonical record. Slugs are still meaningful — they drive the URL — but
|
|
8
|
+
* the source of truth for "which workspace is this" must be a stable
|
|
9
|
+
* identifier that lives independently of any human-editable name.
|
|
10
|
+
*
|
|
11
|
+
* Before this migration, the workspace store was keyed by `[namespace, slug]`
|
|
12
|
+
* (with a separate `teamUid` index that was never used for lookups). All
|
|
13
|
+
* chunk records (meta, documents, ...) were keyed by `${namespace}/${slug}`.
|
|
7
14
|
*
|
|
8
15
|
* After this migration:
|
|
9
|
-
* - The workspace object store is re-created with
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* - Every workspace is
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
16
|
+
* - The workspace object store is re-created with `workspaceUid` as its
|
|
17
|
+
* primary key. A fresh UUID is generated for every existing record so
|
|
18
|
+
* the new identifier is guaranteed to be stable across slug renames.
|
|
19
|
+
* - Every workspace is relocated to the local team. Both `teamUid` and
|
|
20
|
+
* `teamSlug` are set to `'local'`, regardless of where the workspace
|
|
21
|
+
* came from. The client only supports a single team workspace going
|
|
22
|
+
* forward, so any pre-existing team association is intentionally
|
|
23
|
+
* dropped — the app will rebuild the user's team workspace from the
|
|
24
|
+
* server on first sign-in.
|
|
25
|
+
* - Workspace slugs are preserved when possible. If two legacy records
|
|
26
|
+
* would collide on the same `[local, <slug>]` pair after collapse, the
|
|
27
|
+
* later record gets a unique suffix (`-2`, `-3`, ...). Records that
|
|
28
|
+
* were already under the local team keep their slug as-is; team
|
|
29
|
+
* workspaces yield first because their slug came from a namespace the
|
|
30
|
+
* user no longer controls.
|
|
31
|
+
* - Two indexes are added to the workspace store:
|
|
32
|
+
* - `teamSlug_slug` on `['teamSlug', 'slug']` with `unique: true`,
|
|
33
|
+
* so the URL `/@<teamSlug>/<workspaceSlug>` resolves to a single
|
|
34
|
+
* workspace and the app can rely on slug-pair uniqueness.
|
|
35
|
+
* - `teamUid` on `['teamUid']`, so we can fetch every workspace for a
|
|
36
|
+
* team without scanning the store.
|
|
37
|
+
* - Every chunk store (meta, documents, originalDocuments,
|
|
38
|
+
* intermediateDocuments, overrides, history, auth) is recreated with
|
|
39
|
+
* `workspaceUid` as its key path (replacing the old `workspaceId`
|
|
40
|
+
* field). All chunk records are re-keyed from the legacy
|
|
41
|
+
* `${namespace}/${slug}` value to the new `workspaceUid`.
|
|
42
|
+
* - Saved tabs (`x-scalar-tabs`) and the active tab index
|
|
43
|
+
* (`x-scalar-active-tab`) are stripped from every workspace's meta
|
|
44
|
+
* chunk. Tab paths embed the old `@<namespace>/<slug>` URL and slugs
|
|
45
|
+
* may have been rewritten to resolve collisions, so keeping them would
|
|
46
|
+
* route the client to stale paths on next load.
|
|
23
47
|
*
|
|
24
|
-
* All work happens inside the upgrade transaction. The migration awaits
|
|
25
|
-
* IDB request it queues so the database is fully migrated before
|
|
26
|
-
* — that guarantee is what lets later migrations safely
|
|
48
|
+
* All work happens inside the upgrade transaction. The migration awaits
|
|
49
|
+
* every IDB request it queues so the database is fully migrated before
|
|
50
|
+
* `up` resolves — that guarantee is what lets later migrations safely
|
|
51
|
+
* build on this state.
|
|
27
52
|
*/
|
|
28
|
-
/** Tables that store per-workspace chunks keyed by `
|
|
53
|
+
/** Tables that store per-workspace chunks keyed by `workspaceUid` (single key). */
|
|
29
54
|
const SINGLE_KEY_CHUNK_TABLES = ['meta'];
|
|
30
|
-
/** Tables that store per-document chunks keyed by `[
|
|
55
|
+
/** Tables that store per-document chunks keyed by `[workspaceUid, documentName]`. */
|
|
31
56
|
const COMPOSITE_KEY_CHUNK_TABLES = [
|
|
32
57
|
'documents',
|
|
33
58
|
'originalDocuments',
|
|
@@ -39,6 +64,12 @@ const COMPOSITE_KEY_CHUNK_TABLES = [
|
|
|
39
64
|
/**
|
|
40
65
|
* Picks a slug that does not collide with anything in `taken`.
|
|
41
66
|
* Falls back to `<slug>-2`, `<slug>-3`, ... when the desired slug is already used.
|
|
67
|
+
*
|
|
68
|
+
* Collapsing every legacy workspace into the local team can produce
|
|
69
|
+
* `[local, <slug>]` collisions whenever a team workspace shared a slug
|
|
70
|
+
* with an existing local workspace (or with another team workspace). The
|
|
71
|
+
* unique `[teamSlug, slug]` index would otherwise reject the upgrade, so
|
|
72
|
+
* this helper resolves collisions deterministically.
|
|
42
73
|
*/
|
|
43
74
|
export const pickUniqueSlug = (desired, taken) => {
|
|
44
75
|
if (!taken.has(desired)) {
|
|
@@ -51,51 +82,67 @@ export const pickUniqueSlug = (desired, taken) => {
|
|
|
51
82
|
return `${desired}-${counter}`;
|
|
52
83
|
};
|
|
53
84
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
85
|
+
* Generates a UUID. Wrapped in a function so tests can stub it and so the
|
|
86
|
+
* migration does not silently break in environments where `crypto.randomUUID`
|
|
87
|
+
* is unavailable.
|
|
88
|
+
*/
|
|
89
|
+
const generateUid = () => {
|
|
90
|
+
if (typeof globalThis.crypto?.randomUUID === 'function') {
|
|
91
|
+
return globalThis.crypto.randomUUID();
|
|
92
|
+
}
|
|
93
|
+
throw new Error('crypto.randomUUID is not available in this environment; cannot run v2 migration');
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Computes the new shape for every workspace.
|
|
97
|
+
*
|
|
98
|
+
* Every record is collapsed into the local team: `teamUid` and `teamSlug`
|
|
99
|
+
* are both forced to `'local'`. Slug uniqueness is enforced by reserving
|
|
100
|
+
* the legacy local-team slugs first (they keep their slug verbatim), then
|
|
101
|
+
* placing every team workspace on top with a `-2`, `-3`, ... suffix when
|
|
102
|
+
* the desired slug is already taken.
|
|
103
|
+
*
|
|
104
|
+
* A fresh `workspaceUid` is generated for every record so the new
|
|
105
|
+
* identifier is stable across future slug renames.
|
|
57
106
|
*/
|
|
58
107
|
export const planWorkspaceMigration = (workspaces) => {
|
|
59
|
-
//
|
|
108
|
+
// Reserve every legacy local slug up front so genuine local workspaces
|
|
109
|
+
// never lose their slug to a colliding team workspace. Without this the
|
|
110
|
+
// suffixing would depend on iteration order and could rename the user's
|
|
111
|
+
// local workspace just because a team workspace happened to come first.
|
|
60
112
|
const reservedSlugs = new Set(workspaces.filter((workspace) => workspace.namespace === 'local').map((workspace) => workspace.slug));
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
teamSlug: 'local',
|
|
69
|
-
slug: workspace.slug,
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
continue;
|
|
113
|
+
return workspaces.map((workspace) => {
|
|
114
|
+
const isLocal = workspace.namespace === 'local';
|
|
115
|
+
// Local workspaces keep their slug because it was already reserved
|
|
116
|
+
// above. Team workspaces pick a unique slug, suffixing on collision.
|
|
117
|
+
const slug = isLocal ? workspace.slug : pickUniqueSlug(workspace.slug, reservedSlugs);
|
|
118
|
+
if (!isLocal) {
|
|
119
|
+
reservedSlugs.add(slug);
|
|
73
120
|
}
|
|
74
|
-
|
|
75
|
-
reservedSlugs.add(newSlug);
|
|
76
|
-
plan.push({
|
|
121
|
+
return {
|
|
77
122
|
before: { namespace: workspace.namespace, slug: workspace.slug },
|
|
78
123
|
after: {
|
|
79
|
-
|
|
80
|
-
// Team
|
|
81
|
-
//
|
|
124
|
+
workspaceUid: generateUid(),
|
|
125
|
+
// Team membership is intentionally dropped: the client now ships
|
|
126
|
+
// with a "single team workspace" UX, so any pre-existing team
|
|
127
|
+
// association is rebuilt from the server on next sign-in.
|
|
128
|
+
teamUid: 'local',
|
|
82
129
|
teamSlug: 'local',
|
|
83
|
-
slug
|
|
130
|
+
slug,
|
|
131
|
+
name: workspace.name,
|
|
84
132
|
},
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return plan;
|
|
133
|
+
};
|
|
134
|
+
});
|
|
88
135
|
};
|
|
89
|
-
const buildWorkspaceId = (prefix, slug) => `${prefix}/${slug}`;
|
|
90
136
|
/**
|
|
91
137
|
* Keys on the workspace meta record that embed URL paths tied to the old
|
|
92
|
-
* `@<namespace>/<slug>` routing scheme. We strip them during the migration
|
|
93
|
-
* the client can rebuild them from the current route on next load.
|
|
138
|
+
* `@<namespace>/<slug>` routing scheme. We strip them during the migration
|
|
139
|
+
* so the client can rebuild them from the current route on next load.
|
|
94
140
|
*/
|
|
95
141
|
const STALE_META_KEYS = ['x-scalar-tabs', 'x-scalar-active-tab'];
|
|
96
142
|
/**
|
|
97
|
-
* Returns a new meta object with stale, URL-bound fields removed. Leaves
|
|
98
|
-
* other key untouched so color mode, theme, active document, etc.
|
|
143
|
+
* Returns a new meta object with stale, URL-bound fields removed. Leaves
|
|
144
|
+
* every other key untouched so color mode, theme, active document, etc.
|
|
145
|
+
* survive.
|
|
99
146
|
*/
|
|
100
147
|
const stripStaleMetaFields = (meta) => {
|
|
101
148
|
if (!meta || typeof meta !== 'object') {
|
|
@@ -113,101 +160,112 @@ const requestAsPromise = (req) => new Promise((resolve, reject) => {
|
|
|
113
160
|
req.onerror = () => reject(req.error);
|
|
114
161
|
});
|
|
115
162
|
/**
|
|
116
|
-
*
|
|
117
|
-
* `
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* Returns a Promise that resolves once every queued read/write has completed
|
|
121
|
-
* — this is what lets the migration runner guarantee subsequent migrations
|
|
122
|
-
* see the fully re-keyed state.
|
|
163
|
+
* Reads every record from a chunk store, returning them tagged with the
|
|
164
|
+
* legacy `workspaceId` so the caller can remap them to the new
|
|
165
|
+
* `workspaceUid` once the store has been recreated.
|
|
123
166
|
*/
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
for (const tableName of SINGLE_KEY_CHUNK_TABLES) {
|
|
128
|
-
if (!transaction.db.objectStoreNames.contains(tableName)) {
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
const store = transaction.objectStore(tableName);
|
|
132
|
-
tasks.push(new Promise((resolve, reject) => {
|
|
133
|
-
const getRequest = store.get(oldWorkspaceId);
|
|
134
|
-
getRequest.onerror = () => reject(getRequest.error);
|
|
135
|
-
getRequest.onsuccess = () => {
|
|
136
|
-
const record = getRequest.result;
|
|
137
|
-
if (!record) {
|
|
138
|
-
resolve();
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
// The meta chunk holds x-scalar-tabs with full URL paths built from the
|
|
142
|
-
// pre-migration namespace/slug. Those paths are no longer routable after
|
|
143
|
-
// this migration (namespace is dropped and slugs may have been renamed),
|
|
144
|
-
// so drop them here and let the client rebuild tabs from the live route.
|
|
145
|
-
const nextData = tableName === 'meta' ? stripStaleMetaFields(record.data) : record.data;
|
|
146
|
-
if (idChanged) {
|
|
147
|
-
store.delete(oldWorkspaceId);
|
|
148
|
-
}
|
|
149
|
-
const putRequest = store.put({ ...record, workspaceId: newWorkspaceId, data: nextData });
|
|
150
|
-
putRequest.onerror = () => reject(putRequest.error);
|
|
151
|
-
putRequest.onsuccess = () => resolve();
|
|
152
|
-
};
|
|
153
|
-
}));
|
|
154
|
-
}
|
|
155
|
-
if (idChanged) {
|
|
156
|
-
for (const tableName of COMPOSITE_KEY_CHUNK_TABLES) {
|
|
157
|
-
if (!transaction.db.objectStoreNames.contains(tableName)) {
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
const store = transaction.objectStore(tableName);
|
|
161
|
-
// Range covering every `[oldWorkspaceId, *]` key.
|
|
162
|
-
const range = IDBKeyRange.bound([oldWorkspaceId], [oldWorkspaceId, []], false, true);
|
|
163
|
-
tasks.push(new Promise((resolve, reject) => {
|
|
164
|
-
const cursorRequest = store.openCursor(range);
|
|
165
|
-
cursorRequest.onerror = () => reject(cursorRequest.error);
|
|
166
|
-
cursorRequest.onsuccess = (event) => {
|
|
167
|
-
const cursor = event.target.result;
|
|
168
|
-
if (!cursor) {
|
|
169
|
-
resolve();
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
const value = cursor.value;
|
|
173
|
-
cursor.delete();
|
|
174
|
-
store.put({ ...value, workspaceId: newWorkspaceId });
|
|
175
|
-
cursor.continue();
|
|
176
|
-
};
|
|
177
|
-
}));
|
|
178
|
-
}
|
|
167
|
+
const readLegacyChunkRecords = async (transaction, tableName) => {
|
|
168
|
+
if (!transaction.db.objectStoreNames.contains(tableName)) {
|
|
169
|
+
return [];
|
|
179
170
|
}
|
|
180
|
-
|
|
171
|
+
const store = transaction.objectStore(tableName);
|
|
172
|
+
const records = (await requestAsPromise(store.getAll())) ?? [];
|
|
173
|
+
return records;
|
|
181
174
|
};
|
|
182
175
|
export const v2TeamToLocalMigration = {
|
|
183
|
-
description: '
|
|
176
|
+
description: 'Switch to UID-based identity: workspaceUid primary key, collapse every workspace into the local team',
|
|
184
177
|
up: async ({ db, transaction }) => {
|
|
185
178
|
if (!db.objectStoreNames.contains('workspace')) {
|
|
186
|
-
// The workspace store must exist after v1; if it does not, something
|
|
187
|
-
// very wrong and we should not silently create a new one here.
|
|
179
|
+
// The workspace store must exist after v1; if it does not, something
|
|
180
|
+
// is very wrong and we should not silently create a new one here.
|
|
188
181
|
return;
|
|
189
182
|
}
|
|
190
|
-
// Read every record
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
183
|
+
// Read every legacy record before we drop the old stores. The upgrade
|
|
184
|
+
// transaction stays alive while these requests are pending, so the
|
|
185
|
+
// schema mutations below still run in versionchange mode — which is
|
|
186
|
+
// required for deleteObjectStore / createObjectStore.
|
|
194
187
|
const oldWorkspaceStore = transaction.objectStore('workspace');
|
|
195
188
|
const workspaces = ((await requestAsPromise(oldWorkspaceStore.getAll())) ?? []);
|
|
189
|
+
// Snapshot every chunk record up front so we can recreate the stores
|
|
190
|
+
// with the new key paths and then rewrite the records below.
|
|
191
|
+
const legacySingleKeyChunks = {};
|
|
192
|
+
for (const tableName of SINGLE_KEY_CHUNK_TABLES) {
|
|
193
|
+
legacySingleKeyChunks[tableName] = await readLegacyChunkRecords(transaction, tableName);
|
|
194
|
+
}
|
|
195
|
+
const legacyCompositeKeyChunks = {};
|
|
196
|
+
for (const tableName of COMPOSITE_KEY_CHUNK_TABLES) {
|
|
197
|
+
const records = await readLegacyChunkRecords(transaction, tableName);
|
|
198
|
+
legacyCompositeKeyChunks[tableName] = records;
|
|
199
|
+
}
|
|
196
200
|
const plan = planWorkspaceMigration(workspaces);
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
201
|
+
// Map legacy `${namespace}/${slug}` to the new workspaceUid so chunk
|
|
202
|
+
// records can be re-keyed in a single pass below.
|
|
203
|
+
const legacyIdToWorkspaceUid = new Map();
|
|
204
|
+
for (const { before, after } of plan) {
|
|
205
|
+
legacyIdToWorkspaceUid.set(`${before.namespace}/${before.slug}`, after.workspaceUid);
|
|
206
|
+
}
|
|
207
|
+
// Recreate the workspace store with workspaceUid as the primary key,
|
|
208
|
+
// plus the indexes the runtime relies on for slug-based lookups and
|
|
209
|
+
// team-scoped queries. The `[teamSlug, slug]` index is unique so we
|
|
210
|
+
// never end up with two workspaces racing for the same URL.
|
|
200
211
|
db.deleteObjectStore('workspace');
|
|
201
|
-
const newWorkspaceStore = db.createObjectStore('workspace', { keyPath:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
212
|
+
const newWorkspaceStore = db.createObjectStore('workspace', { keyPath: 'workspaceUid' });
|
|
213
|
+
newWorkspaceStore.createIndex('teamSlug_slug', ['teamSlug', 'slug'], { unique: true });
|
|
214
|
+
newWorkspaceStore.createIndex('teamUid', ['teamUid']);
|
|
215
|
+
// Recreate every chunk store with the new `workspaceUid` key path. We
|
|
216
|
+
// drop and recreate (rather than rewriting records in place) because
|
|
217
|
+
// changing an object store's key path is not supported by IndexedDB.
|
|
218
|
+
for (const tableName of SINGLE_KEY_CHUNK_TABLES) {
|
|
219
|
+
if (transaction.db.objectStoreNames.contains(tableName)) {
|
|
220
|
+
db.deleteObjectStore(tableName);
|
|
221
|
+
}
|
|
222
|
+
db.createObjectStore(tableName, { keyPath: 'workspaceUid' });
|
|
223
|
+
}
|
|
224
|
+
for (const tableName of COMPOSITE_KEY_CHUNK_TABLES) {
|
|
225
|
+
if (transaction.db.objectStoreNames.contains(tableName)) {
|
|
226
|
+
db.deleteObjectStore(tableName);
|
|
227
|
+
}
|
|
228
|
+
db.createObjectStore(tableName, { keyPath: ['workspaceUid', 'documentName'] });
|
|
229
|
+
}
|
|
230
|
+
// Write the migrated workspace records. We do this after creating all
|
|
231
|
+
// stores so the upgrade transaction commits a fully-formed schema even
|
|
232
|
+
// if there are zero legacy records to migrate.
|
|
233
|
+
for (const { after } of plan) {
|
|
209
234
|
newWorkspaceStore.put(after);
|
|
210
|
-
|
|
211
|
-
}
|
|
235
|
+
}
|
|
236
|
+
// Re-key every chunk record from the legacy `${namespace}/${slug}` to
|
|
237
|
+
// the new workspaceUid. Records belonging to a workspace that no
|
|
238
|
+
// longer exists (orphans) are dropped — they have no meaning without
|
|
239
|
+
// their parent workspace.
|
|
240
|
+
for (const tableName of SINGLE_KEY_CHUNK_TABLES) {
|
|
241
|
+
const store = transaction.objectStore(tableName);
|
|
242
|
+
for (const record of legacySingleKeyChunks[tableName] ?? []) {
|
|
243
|
+
const workspaceUid = legacyIdToWorkspaceUid.get(record.workspaceId);
|
|
244
|
+
if (!workspaceUid) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const { workspaceId: _legacyId, ...rest } = record;
|
|
248
|
+
// The meta chunk holds x-scalar-tabs with full URL paths built
|
|
249
|
+
// from the pre-migration namespace/slug. Those paths are no
|
|
250
|
+
// longer routable after collapsing into the local team (and slugs
|
|
251
|
+
// may have been suffixed on collision), so drop them here and
|
|
252
|
+
// let the client rebuild tabs from the live route.
|
|
253
|
+
const nextData = tableName === 'meta'
|
|
254
|
+
? stripStaleMetaFields(rest.data)
|
|
255
|
+
: rest.data;
|
|
256
|
+
store.put({ ...rest, data: nextData, workspaceUid });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
for (const tableName of COMPOSITE_KEY_CHUNK_TABLES) {
|
|
260
|
+
const store = transaction.objectStore(tableName);
|
|
261
|
+
for (const record of legacyCompositeKeyChunks[tableName] ?? []) {
|
|
262
|
+
const workspaceUid = legacyIdToWorkspaceUid.get(record.workspaceId);
|
|
263
|
+
if (!workspaceUid) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const { workspaceId: _legacyId, ...rest } = record;
|
|
267
|
+
store.put({ ...rest, workspaceUid });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
212
270
|
},
|
|
213
271
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-request-body.d.ts","sourceRoot":"","sources":["../../../../src/request-example/builder/body/build-request-body.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"build-request-body.d.ts","sourceRoot":"","sources":["../../../../src/request-example/builder/body/build-request-body.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0DAA0D,CAAA;AAKjG,KAAK,QAAQ,GAAG;IACd,IAAI,EAAE,UAAU,CAAA;IAChB,KAAK,EAAE,CACH;QACE,IAAI,EAAE,MAAM,CAAA;QACZ,GAAG,EAAE,MAAM,CAAA;QACX,KAAK,EAAE,MAAM,CAAA;KACd,GACD;QACE,IAAI,EAAE,MAAM,CAAA;QACZ,GAAG,EAAE,MAAM,CAAA;QACX,KAAK,EAAE,IAAI,CAAA;QACX,WAAW,CAAC,EAAE,MAAM,CAAA;KACrB,GACD;QACE,IAAI,EAAE,MAAM,CAAA;QACZ,GAAG,EAAE,MAAM,CAAA;QACX,KAAK,EAAE,IAAI,CAAA;QACX,WAAW,CAAC,EAAE,MAAM,CAAA;KACrB,CACJ,EAAE,CAAA;CACJ,CAAA;AAED,KAAK,UAAU,GAAG;IAChB,IAAI,EAAE,YAAY,CAAA;IAClB,KAAK,EAAE;QACL,GAAG,EAAE,MAAM,CAAA;QACX,KAAK,EAAE,MAAM,CAAA;KACd,EAAE,CAAA;CACJ,CAAA;AAED,KAAK,GAAG,GAAG;IACT,IAAI,EAAE,KAAK,CAAA;IACX,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,UAAU,GAAG,GAAG,CAAA;AAKrD;;GAEG;AACH,eAAO,MAAM,gBAAgB,GAC3B,aAAa,iBAAiB,GAAG,SAAS;AAC1C,qCAAqC;AACrC,oBAAuB;AACvB,sEAAsE;AACtE,kCAAkC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KACvD,WAAW,GAAG,IAyMhB,CAAA"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// import { replaceEnvVariables } from '@scalar/helpers/regex/replace-variables'
|
|
2
|
+
import { isObject } from '@scalar/helpers/object/is-object';
|
|
2
3
|
import { unpackProxyObject } from '@scalar/workspace-store/helpers/unpack-proxy';
|
|
3
4
|
import { getExampleFromBody } from './get-request-body-example.js';
|
|
4
5
|
import { getSelectedBodyContentType } from './get-selected-body-content-type.js';
|
|
@@ -88,10 +89,7 @@ requestBodyCompositionSelection) => {
|
|
|
88
89
|
// Form data - object format (from schema examples)
|
|
89
90
|
// When the example value is a plain object and content type is form-urlencoded,
|
|
90
91
|
// convert to URLSearchParams instead of JSON stringifying
|
|
91
|
-
if (bodyContentType === 'application/x-www-form-urlencoded' &&
|
|
92
|
-
example.value !== null &&
|
|
93
|
-
typeof example.value === 'object' &&
|
|
94
|
-
!Array.isArray(example.value)) {
|
|
92
|
+
if (bodyContentType === 'application/x-www-form-urlencoded' && isObject(example.value)) {
|
|
95
93
|
const result = {
|
|
96
94
|
mode: 'urlencoded',
|
|
97
95
|
value: [],
|
|
@@ -99,7 +97,7 @@ requestBodyCompositionSelection) => {
|
|
|
99
97
|
// Convert object properties to form fields
|
|
100
98
|
for (const [key, value] of Object.entries(example.value)) {
|
|
101
99
|
if (key && value !== undefined && value !== null) {
|
|
102
|
-
const stringValue = typeof value === '
|
|
100
|
+
const stringValue = typeof value === 'object' && value !== null ? JSON.stringify(unpackProxyObject(value)) : String(value);
|
|
103
101
|
result.value.push({
|
|
104
102
|
key,
|
|
105
103
|
value: stringValue,
|
|
@@ -108,6 +106,51 @@ requestBodyCompositionSelection) => {
|
|
|
108
106
|
}
|
|
109
107
|
return result;
|
|
110
108
|
}
|
|
109
|
+
// Form data - object format (from schema examples)
|
|
110
|
+
if (bodyContentType === 'multipart/form-data' && isObject(example.value)) {
|
|
111
|
+
const result = {
|
|
112
|
+
mode: 'formdata',
|
|
113
|
+
value: [],
|
|
114
|
+
};
|
|
115
|
+
for (const [key, value] of Object.entries(example.value)) {
|
|
116
|
+
if (!key || value === undefined || value === null) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const partContentType = getMultipartEncodingContentType(requestBody, bodyContentType, key);
|
|
120
|
+
if (value instanceof File) {
|
|
121
|
+
const unwrappedValue = unpackProxyObject(value);
|
|
122
|
+
const encodedValue = partContentType && partContentType !== unwrappedValue.type
|
|
123
|
+
? new File([unwrappedValue], unwrappedValue.name, {
|
|
124
|
+
type: partContentType,
|
|
125
|
+
lastModified: unwrappedValue.lastModified,
|
|
126
|
+
})
|
|
127
|
+
: unwrappedValue;
|
|
128
|
+
result.value.push({
|
|
129
|
+
type: 'file',
|
|
130
|
+
key,
|
|
131
|
+
value: encodedValue,
|
|
132
|
+
contentType: partContentType,
|
|
133
|
+
});
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const serializedValue = typeof value === 'object' && value !== null ? JSON.stringify(unpackProxyObject(value)) : String(value);
|
|
137
|
+
if (partContentType) {
|
|
138
|
+
result.value.push({
|
|
139
|
+
type: 'blob',
|
|
140
|
+
key,
|
|
141
|
+
value: new Blob([serializedValue], { type: partContentType }),
|
|
142
|
+
contentType: partContentType,
|
|
143
|
+
});
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
result.value.push({
|
|
147
|
+
type: 'text',
|
|
148
|
+
key,
|
|
149
|
+
value: serializedValue,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
111
154
|
// Any other type
|
|
112
155
|
const exampleValue = example.value !== null && typeof example.value === 'object' ? unpackProxyObject(example.value) : example.value;
|
|
113
156
|
// File type
|
|
@@ -1,15 +1,28 @@
|
|
|
1
|
+
import { type Result } from '@scalar/helpers/types/result';
|
|
1
2
|
import type { RequestFactory } from '../../request-example/builder/request-factory.js';
|
|
3
|
+
import { type ResolveRequestFactoryUrlError } from '../../request-example/builder/resolve-request-factory-url.js';
|
|
2
4
|
/**
|
|
3
5
|
* The payload to build a request, useful when bypassing limitations of the browser Request object
|
|
4
6
|
*/
|
|
5
7
|
export type RequestPayload = [string, RequestInit];
|
|
8
|
+
/**
|
|
9
|
+
* Resolved request URL string (path vars, operation query, **security query**
|
|
10
|
+
* params, env substitution, reserved-query rules) without proxy rewriting —
|
|
11
|
+
* aligned with {@link buildRequest} before `redirectToProxy`.
|
|
12
|
+
*
|
|
13
|
+
* By default allows incomplete merged URLs (same as permissive copy / preview); pass
|
|
14
|
+
* `allowMissingRequestServerBase: false` to enforce a complete absolute URL.
|
|
15
|
+
*/
|
|
16
|
+
export declare const resolveExecutableRequestUrl: (request: RequestFactory, envVariables: Record<string, string>, resolveOptions?: {
|
|
17
|
+
allowMissingRequestServerBase?: boolean;
|
|
18
|
+
}) => string;
|
|
6
19
|
/**
|
|
7
20
|
* Built request response
|
|
8
21
|
*
|
|
9
22
|
* We no longer return a Request object, but a tuple of [url, init] that maps directly to the fetch() argument list so
|
|
10
23
|
* we can do things that the browser doesn't allow like GET + body
|
|
11
24
|
* */
|
|
12
|
-
type
|
|
25
|
+
export type BuildRequestData = {
|
|
13
26
|
/** Create a new request payload object with the replaced values ready to be sent to the server */
|
|
14
27
|
requestPayload: RequestPayload;
|
|
15
28
|
/** The abort controller */
|
|
@@ -17,8 +30,16 @@ type BuildRequestResponse = {
|
|
|
17
30
|
/** The flag indicating if the request is being proxied */
|
|
18
31
|
isUsingProxy: boolean;
|
|
19
32
|
};
|
|
33
|
+
/** Catch-all code when an unexpected synchronous error escapes a helper during request construction. */
|
|
34
|
+
export declare const BUILD_REQUEST_FAILED: "BUILD_REQUEST_FAILED";
|
|
35
|
+
export type BuildRequestFailureCode = ResolveRequestFactoryUrlError | typeof BUILD_REQUEST_FAILED;
|
|
36
|
+
export type BuildRequestResult = Result<BuildRequestData, BuildRequestFailureCode>;
|
|
20
37
|
export declare const buildRequest: (request: RequestFactory, options: {
|
|
21
38
|
envVariables: Record<string, string>;
|
|
22
|
-
|
|
23
|
-
|
|
39
|
+
/**
|
|
40
|
+
* When true, allows an empty resolved server base URL (embedded modal, API reference callbacks, tests).
|
|
41
|
+
* @default false
|
|
42
|
+
*/
|
|
43
|
+
allowMissingRequestServerBase?: boolean;
|
|
44
|
+
}) => BuildRequestResult;
|
|
24
45
|
//# sourceMappingURL=build-request.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-request.d.ts","sourceRoot":"","sources":["../../../src/request-example/builder/build-request.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"build-request.d.ts","sourceRoot":"","sources":["../../../src/request-example/builder/build-request.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,MAAM,EAAW,MAAM,8BAA8B,CAAA;AAOnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2CAA2C,CAAA;AAC/E,OAAO,EACL,KAAK,6BAA6B,EAEnC,MAAM,uDAAuD,CAAA;AAK9D;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;AAuClD;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,GACtC,SAAS,cAAc,EACvB,cAAc,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,iBAAiB;IAAE,6BAA6B,CAAC,EAAE,OAAO,CAAA;CAAE,KAC3D,MAyBF,CAAA;AAED;;;;;KAKK;AACL,MAAM,MAAM,gBAAgB,GAAG;IAC7B,kGAAkG;IAClG,cAAc,EAAE,cAAc,CAAA;IAC9B,2BAA2B;IAC3B,UAAU,EAAE,eAAe,CAAA;IAC3B,0DAA0D;IAC1D,YAAY,EAAE,OAAO,CAAA;CACtB,CAAA;AAED,wGAAwG;AACxG,eAAO,MAAM,oBAAoB,EAAG,sBAA+B,CAAA;AAEnE,MAAM,MAAM,uBAAuB,GAAG,6BAA6B,GAAG,OAAO,oBAAoB,CAAA;AAEjG,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,gBAAgB,EAAE,uBAAuB,CAAC,CAAA;AAElF,eAAO,MAAM,YAAY,GACvB,SAAS,cAAc,EACvB,SAAS;IACP,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACpC;;;OAGG;IACH,6BAA6B,CAAC,EAAE,OAAO,CAAA;CACxC,KACA,kBAMF,CAAA"}
|