@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.
Files changed (135) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/dist/client.d.ts +2 -3
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +38 -15
  5. package/dist/entities/auth/schema.d.ts +10 -5
  6. package/dist/entities/auth/schema.d.ts.map +1 -1
  7. package/dist/events/bus.d.ts +70 -0
  8. package/dist/events/bus.d.ts.map +1 -1
  9. package/dist/events/bus.js +48 -11
  10. package/dist/events/definitions/analytics.d.ts +0 -12
  11. package/dist/events/definitions/analytics.d.ts.map +1 -1
  12. package/dist/events/definitions/auth.d.ts +44 -6
  13. package/dist/events/definitions/auth.d.ts.map +1 -1
  14. package/dist/events/definitions/index.d.ts +3 -2
  15. package/dist/events/definitions/index.d.ts.map +1 -1
  16. package/dist/events/definitions/log.d.ts +18 -0
  17. package/dist/events/definitions/log.d.ts.map +1 -0
  18. package/dist/events/definitions/log.js +1 -0
  19. package/dist/events/definitions/operation.d.ts +1 -1
  20. package/dist/events/definitions/operation.d.ts.map +1 -1
  21. package/dist/events/definitions/ui.d.ts +18 -1
  22. package/dist/events/definitions/ui.d.ts.map +1 -1
  23. package/dist/events/index.d.ts +1 -1
  24. package/dist/events/index.d.ts.map +1 -1
  25. package/dist/helpers/get-resolved-ref.d.ts +6 -0
  26. package/dist/helpers/get-resolved-ref.d.ts.map +1 -1
  27. package/dist/helpers/get-resolved-ref.js +9 -0
  28. package/dist/helpers/is-hidden.d.ts +8 -0
  29. package/dist/helpers/is-hidden.d.ts.map +1 -0
  30. package/dist/helpers/is-hidden.js +5 -0
  31. package/dist/mutators/auth.d.ts +22 -3
  32. package/dist/mutators/auth.d.ts.map +1 -1
  33. package/dist/mutators/auth.js +213 -37
  34. package/dist/mutators/cookie.d.ts.map +1 -1
  35. package/dist/mutators/cookie.js +3 -2
  36. package/dist/mutators/document.d.ts.map +1 -1
  37. package/dist/mutators/document.js +5 -4
  38. package/dist/mutators/environment.d.ts.map +1 -1
  39. package/dist/mutators/environment.js +12 -5
  40. package/dist/mutators/index.d.ts +4 -0
  41. package/dist/mutators/index.d.ts.map +1 -1
  42. package/dist/mutators/operation/body.d.ts.map +1 -1
  43. package/dist/mutators/operation/body.js +6 -2
  44. package/dist/mutators/operation/extensions.d.ts.map +1 -1
  45. package/dist/mutators/operation/extensions.js +5 -1
  46. package/dist/mutators/operation/history.d.ts.map +1 -1
  47. package/dist/mutators/operation/history.js +7 -3
  48. package/dist/mutators/operation/operation.d.ts.map +1 -1
  49. package/dist/mutators/operation/operation.js +15 -10
  50. package/dist/mutators/operation/parameters.d.ts.map +1 -1
  51. package/dist/mutators/operation/parameters.js +12 -5
  52. package/dist/mutators/server.d.ts.map +1 -1
  53. package/dist/mutators/server.js +2 -1
  54. package/dist/mutators/tag.d.ts.map +1 -1
  55. package/dist/mutators/tag.js +9 -4
  56. package/dist/navigation/helpers/get-openapi-object.d.ts.map +1 -1
  57. package/dist/navigation/helpers/get-openapi-object.js +5 -0
  58. package/dist/navigation/helpers/traverse-paths.d.ts.map +1 -1
  59. package/dist/navigation/helpers/traverse-paths.js +4 -3
  60. package/dist/navigation/helpers/traverse-schemas.d.ts.map +1 -1
  61. package/dist/navigation/helpers/traverse-schemas.js +9 -4
  62. package/dist/navigation/helpers/traverse-tags.d.ts.map +1 -1
  63. package/dist/navigation/helpers/traverse-tags.js +2 -1
  64. package/dist/navigation/helpers/traverse-webhooks.d.ts.map +1 -1
  65. package/dist/navigation/helpers/traverse-webhooks.js +4 -3
  66. package/dist/navigation/helpers/update-order-ids.d.ts.map +1 -1
  67. package/dist/navigation/helpers/update-order-ids.js +4 -1
  68. package/dist/persistence/index.d.ts +123 -80
  69. package/dist/persistence/index.d.ts.map +1 -1
  70. package/dist/persistence/index.js +233 -167
  71. package/dist/persistence/migrations/v2-team-to-local.d.ts +22 -5
  72. package/dist/persistence/migrations/v2-team-to-local.d.ts.map +1 -1
  73. package/dist/persistence/migrations/v2-team-to-local.js +195 -137
  74. package/dist/request-example/builder/body/build-request-body.d.ts.map +1 -1
  75. package/dist/request-example/builder/body/build-request-body.js +48 -5
  76. package/dist/request-example/builder/build-request.d.ts +24 -3
  77. package/dist/request-example/builder/build-request.d.ts.map +1 -1
  78. package/dist/request-example/builder/build-request.js +89 -18
  79. package/dist/request-example/builder/index.d.ts +2 -1
  80. package/dist/request-example/builder/index.d.ts.map +1 -1
  81. package/dist/request-example/builder/index.js +2 -1
  82. package/dist/request-example/builder/request-factory.d.ts.map +1 -1
  83. package/dist/request-example/builder/request-factory.js +5 -8
  84. package/dist/request-example/builder/resolve-request-factory-url.d.ts +18 -1
  85. package/dist/request-example/builder/resolve-request-factory-url.d.ts.map +1 -1
  86. package/dist/request-example/builder/resolve-request-factory-url.js +29 -4
  87. package/dist/request-example/context/environment.d.ts.map +1 -1
  88. package/dist/request-example/context/environment.js +2 -1
  89. package/dist/request-example/context/get-request-example-context.d.ts.map +1 -1
  90. package/dist/request-example/context/get-request-example-context.js +7 -0
  91. package/dist/request-example/context/headers.d.ts +28 -13
  92. package/dist/request-example/context/headers.d.ts.map +1 -1
  93. package/dist/request-example/context/headers.js +84 -19
  94. package/dist/request-example/context/index.d.ts +1 -0
  95. package/dist/request-example/context/index.d.ts.map +1 -1
  96. package/dist/request-example/context/index.js +1 -0
  97. package/dist/request-example/context/security/get-selected-security.d.ts.map +1 -1
  98. package/dist/request-example/context/security/get-selected-security.js +3 -6
  99. package/dist/request-example/context/servers.d.ts.map +1 -1
  100. package/dist/request-example/context/servers.js +3 -3
  101. package/dist/request-example/index.d.ts +3 -3
  102. package/dist/request-example/index.d.ts.map +1 -1
  103. package/dist/request-example/index.js +2 -2
  104. package/dist/resolve.d.ts.map +1 -1
  105. package/dist/resolve.js +1 -8
  106. package/dist/schemas/asyncapi/asyncapi-document.d.ts +79 -0
  107. package/dist/schemas/asyncapi/asyncapi-document.d.ts.map +1 -0
  108. package/dist/schemas/asyncapi/asyncapi-document.js +58 -0
  109. package/dist/schemas/extensions/document/workspace-managed-extensions.d.ts +25 -0
  110. package/dist/schemas/extensions/document/workspace-managed-extensions.d.ts.map +1 -0
  111. package/dist/schemas/extensions/document/workspace-managed-extensions.js +26 -0
  112. package/dist/schemas/inmemory-workspace.d.ts +3 -4631
  113. package/dist/schemas/inmemory-workspace.d.ts.map +1 -1
  114. package/dist/schemas/inmemory-workspace.js +1 -15
  115. package/dist/schemas/reference-config/index.d.ts +3 -2
  116. package/dist/schemas/reference-config/index.d.ts.map +1 -1
  117. package/dist/schemas/reference-config/settings.d.ts +2 -1
  118. package/dist/schemas/reference-config/settings.d.ts.map +1 -1
  119. package/dist/schemas/type-guards.d.ts +24 -0
  120. package/dist/schemas/type-guards.d.ts.map +1 -0
  121. package/dist/schemas/type-guards.js +35 -0
  122. package/dist/schemas/v3.1/openapi/index.d.ts +2 -1
  123. package/dist/schemas/v3.1/openapi/index.d.ts.map +1 -1
  124. package/dist/schemas/v3.1/openapi/index.js +3 -3
  125. package/dist/schemas/v3.1/strict/openapi-document.d.ts +74 -39
  126. package/dist/schemas/v3.1/strict/openapi-document.d.ts.map +1 -1
  127. package/dist/schemas/v3.1/strict/openapi-document.js +6 -2
  128. package/dist/schemas/workspace-specification/index.d.ts +1 -1
  129. package/dist/schemas/workspace.d.ts +15 -4377
  130. package/dist/schemas/workspace.d.ts.map +1 -1
  131. package/dist/schemas/workspace.js +13 -8
  132. package/dist/schemas.d.ts +3 -1
  133. package/dist/schemas.d.ts.map +1 -1
  134. package/dist/schemas.js +3 -1
  135. package/package.json +6 -6
@@ -1,33 +1,58 @@
1
1
  /**
2
- * v2 — collapse every workspace into the local team and drop the namespace concept.
2
+ * v2 — move to UID-based identity and collapse every workspace into the
3
+ * local team.
3
4
  *
4
- * Before this migration, workspaces were keyed by `[namespace, slug]` where the
5
- * namespace doubled as the team identifier (for example `acme-corp/api-workspace`)
6
- * and a separate `teamUid` field stored the team's UID.
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 a new composite key
10
- * `[teamSlug, slug]`. The old `teamUid` index is removed; no separate
11
- * `teamSlug` index is needed because it is now part of the primary key.
12
- * - Every workspace is placed under `teamSlug = 'local'`. Team association is
13
- * intentionally dropped every workspace becomes a personal/local one.
14
- * - The `namespace` field is removed entirely from the record.
15
- * - When moving a team workspace into the local team would collide with an
16
- * existing local slug, a unique suffix (`-2`, `-3`, ...) is appended.
17
- * - All chunk records (meta, documents, originalDocuments, intermediateDocuments,
18
- * overrides, history, auth) are re-keyed to the new `local/<slug>` workspaceId.
19
- * - Saved tabs and the active tab index are cleared from every workspace's meta
20
- * chunk. Tab paths are URLs that embed the old `@<namespace>/<slug>` prefix
21
- * and slugs may have been rewritten to resolve collisions, so keeping them
22
- * would cause the client to route to stale paths on next load.
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 every
25
- * IDB request it queues so the database is fully migrated before `up` resolves
26
- * — that guarantee is what lets later migrations safely build on this state.
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 `workspaceId` (single key). */
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 `[workspaceId, documentName]`. */
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
- * Computes the new shape for every workspace, preserving local entries under
55
- * their existing slug and relocating team entries into the local team with a
56
- * unique slug when needed.
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
- // Local slugs that already exist take priority and keep their slug as-is.
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
- const plan = [];
62
- for (const workspace of workspaces) {
63
- if (workspace.namespace === 'local') {
64
- plan.push({
65
- before: { namespace: workspace.namespace, slug: workspace.slug },
66
- after: {
67
- name: workspace.name,
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
- const newSlug = pickUniqueSlug(workspace.slug, reservedSlugs);
75
- reservedSlugs.add(newSlug);
76
- plan.push({
121
+ return {
77
122
  before: { namespace: workspace.namespace, slug: workspace.slug },
78
123
  after: {
79
- name: workspace.name,
80
- // Team association is dropped on purpose every workspace becomes
81
- // local. Slug uniqueness has already been handled above.
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: newSlug,
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 so
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 every
98
- * other key untouched so color mode, theme, active document, etc. survive.
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
- * Re-keys all chunk records belonging to `oldWorkspaceId` so they live under
117
- * `newWorkspaceId` instead. Always runs so we can also strip stale tab state
118
- * from the meta chunk, even when the workspace keeps its old id.
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 remapChunkTables = async (transaction, oldWorkspaceId, newWorkspaceId) => {
125
- const idChanged = oldWorkspaceId !== newWorkspaceId;
126
- const tasks = [];
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
- await Promise.all(tasks);
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: 'Re-key workspace store to [teamSlug, slug]; collapse all workspaces into the local team',
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 is
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 from the old workspace store before we delete it.
191
- // The upgrade transaction stays alive while the `getAll` request is
192
- // pending, so the schema mutations below still run in versionchange
193
- // mode — which is required for deleteObjectStore / createObjectStore.
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
- // The workspace store's keyPath cannot be changed in place, so drop it
198
- // and recreate it with the new composite key. No separate `teamSlug`
199
- // index is needed because the team slug is the first part of the key.
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: ['teamSlug', 'slug'] });
202
- // Re-key all chunk tables in parallel so the entire migration finishes in
203
- // a single round-trip. Awaiting before `up` resolves is what guarantees
204
- // any future migration appended after this one observes the post-v2
205
- // state without this await, IDB callbacks would still be in flight.
206
- await Promise.all(plan.map(async ({ before, after }) => {
207
- const oldWorkspaceId = buildWorkspaceId(before.namespace, before.slug);
208
- const newWorkspaceId = buildWorkspaceId(after.teamSlug, after.slug);
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
- await remapChunkTables(transaction, oldWorkspaceId, newWorkspaceId);
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":"AAEA,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,IAqJhB,CAAA"}
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 === 'string' ? value : String(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 BuildRequestResponse = {
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
- }) => BuildRequestResponse;
23
- export {};
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":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2CAA2C,CAAA;AAK/E;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;AAElD;;;;;KAKK;AACL,KAAK,oBAAoB,GAAG;IAC1B,kGAAkG;IAClG,cAAc,EAAE,cAAc,CAAA;IAC9B,2BAA2B;IAC3B,UAAU,EAAE,eAAe,CAAA;IAC3B,0DAA0D;IAC1D,YAAY,EAAE,OAAO,CAAA;CACtB,CAAA;AAED,eAAO,MAAM,YAAY,GACvB,SAAS,cAAc,EACvB,SAAS;IACP,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACrC,KACA,oBA2JF,CAAA"}
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"}