@scalar/workspace-store 0.49.3 → 0.51.1

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 (152) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/dist/client.d.ts +2 -3
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +58 -25
  5. package/dist/entities/auth/schema.d.ts +25 -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/helpers/merge-object.d.ts +1 -1
  32. package/dist/helpers/merge-object.d.ts.map +1 -1
  33. package/dist/mutators/auth.d.ts +22 -3
  34. package/dist/mutators/auth.d.ts.map +1 -1
  35. package/dist/mutators/auth.js +213 -37
  36. package/dist/mutators/cookie.d.ts.map +1 -1
  37. package/dist/mutators/cookie.js +3 -2
  38. package/dist/mutators/document.d.ts.map +1 -1
  39. package/dist/mutators/document.js +5 -4
  40. package/dist/mutators/environment.d.ts.map +1 -1
  41. package/dist/mutators/environment.js +12 -5
  42. package/dist/mutators/index.d.ts +4 -0
  43. package/dist/mutators/index.d.ts.map +1 -1
  44. package/dist/mutators/operation/body.d.ts.map +1 -1
  45. package/dist/mutators/operation/body.js +6 -2
  46. package/dist/mutators/operation/extensions.d.ts.map +1 -1
  47. package/dist/mutators/operation/extensions.js +5 -1
  48. package/dist/mutators/operation/history.d.ts.map +1 -1
  49. package/dist/mutators/operation/history.js +7 -3
  50. package/dist/mutators/operation/operation.d.ts.map +1 -1
  51. package/dist/mutators/operation/operation.js +15 -10
  52. package/dist/mutators/operation/parameters.d.ts.map +1 -1
  53. package/dist/mutators/operation/parameters.js +12 -5
  54. package/dist/mutators/server.d.ts.map +1 -1
  55. package/dist/mutators/server.js +2 -1
  56. package/dist/mutators/tag.d.ts.map +1 -1
  57. package/dist/mutators/tag.js +9 -4
  58. package/dist/navigation/get-navigation-options.d.ts +1 -1
  59. package/dist/navigation/get-navigation-options.d.ts.map +1 -1
  60. package/dist/navigation/get-navigation-options.js +1 -0
  61. package/dist/navigation/helpers/get-openapi-object.d.ts.map +1 -1
  62. package/dist/navigation/helpers/get-openapi-object.js +5 -0
  63. package/dist/navigation/helpers/traverse-document.d.ts.map +1 -1
  64. package/dist/navigation/helpers/traverse-document.js +8 -2
  65. package/dist/navigation/helpers/traverse-paths.d.ts +4 -2
  66. package/dist/navigation/helpers/traverse-paths.d.ts.map +1 -1
  67. package/dist/navigation/helpers/traverse-paths.js +9 -6
  68. package/dist/navigation/helpers/traverse-schemas.d.ts.map +1 -1
  69. package/dist/navigation/helpers/traverse-schemas.js +9 -4
  70. package/dist/navigation/helpers/traverse-tags.d.ts.map +1 -1
  71. package/dist/navigation/helpers/traverse-tags.js +2 -1
  72. package/dist/navigation/helpers/traverse-webhooks.d.ts.map +1 -1
  73. package/dist/navigation/helpers/traverse-webhooks.js +4 -3
  74. package/dist/navigation/helpers/update-order-ids.d.ts.map +1 -1
  75. package/dist/navigation/helpers/update-order-ids.js +4 -1
  76. package/dist/navigation/types.d.ts +4 -0
  77. package/dist/navigation/types.d.ts.map +1 -1
  78. package/dist/persistence/index.d.ts +123 -80
  79. package/dist/persistence/index.d.ts.map +1 -1
  80. package/dist/persistence/index.js +233 -167
  81. package/dist/persistence/migrations/v2-team-to-local.d.ts +22 -5
  82. package/dist/persistence/migrations/v2-team-to-local.d.ts.map +1 -1
  83. package/dist/persistence/migrations/v2-team-to-local.js +195 -137
  84. package/dist/request-example/builder/body/build-request-body.d.ts.map +1 -1
  85. package/dist/request-example/builder/body/build-request-body.js +66 -2
  86. package/dist/request-example/builder/build-request.d.ts +24 -3
  87. package/dist/request-example/builder/build-request.d.ts.map +1 -1
  88. package/dist/request-example/builder/build-request.js +89 -18
  89. package/dist/request-example/builder/index.d.ts +2 -1
  90. package/dist/request-example/builder/index.d.ts.map +1 -1
  91. package/dist/request-example/builder/index.js +2 -1
  92. package/dist/request-example/builder/request-factory.d.ts.map +1 -1
  93. package/dist/request-example/builder/request-factory.js +5 -8
  94. package/dist/request-example/builder/resolve-request-factory-url.d.ts +18 -1
  95. package/dist/request-example/builder/resolve-request-factory-url.d.ts.map +1 -1
  96. package/dist/request-example/builder/resolve-request-factory-url.js +29 -4
  97. package/dist/request-example/context/environment.d.ts.map +1 -1
  98. package/dist/request-example/context/environment.js +2 -1
  99. package/dist/request-example/context/get-request-example-context.d.ts.map +1 -1
  100. package/dist/request-example/context/get-request-example-context.js +7 -0
  101. package/dist/request-example/context/headers.d.ts +28 -13
  102. package/dist/request-example/context/headers.d.ts.map +1 -1
  103. package/dist/request-example/context/headers.js +84 -19
  104. package/dist/request-example/context/index.d.ts +1 -0
  105. package/dist/request-example/context/index.d.ts.map +1 -1
  106. package/dist/request-example/context/index.js +1 -0
  107. package/dist/request-example/context/security/get-selected-security.d.ts.map +1 -1
  108. package/dist/request-example/context/security/get-selected-security.js +3 -6
  109. package/dist/request-example/context/servers.d.ts.map +1 -1
  110. package/dist/request-example/context/servers.js +3 -3
  111. package/dist/request-example/index.d.ts +3 -3
  112. package/dist/request-example/index.d.ts.map +1 -1
  113. package/dist/request-example/index.js +2 -2
  114. package/dist/resolve.d.ts.map +1 -1
  115. package/dist/resolve.js +1 -8
  116. package/dist/schemas/extensions/document/x-scalar-environments.d.ts +6 -6
  117. package/dist/schemas/extensions/document/x-scalar-original-source-url.d.ts +15 -0
  118. package/dist/schemas/extensions/document/x-scalar-original-source-url.d.ts.map +1 -0
  119. package/dist/schemas/extensions/document/x-scalar-original-source-url.js +16 -0
  120. package/dist/schemas/extensions/operation/x-badge.d.ts +2 -2
  121. package/dist/schemas/extensions/operation/x-scalar-stability.d.ts +1 -1
  122. package/dist/schemas/extensions/schema/x-enum-descriptions.d.ts +2 -2
  123. package/dist/schemas/extensions/security/x-scalar-credentials-location.d.ts +1 -1
  124. package/dist/schemas/extensions/security/x-use-pkce.d.ts +1 -1
  125. package/dist/schemas/extensions/workspace/x-scalar-active-proxy.d.ts +1 -1
  126. package/dist/schemas/inmemory-workspace.d.ts +3 -4631
  127. package/dist/schemas/inmemory-workspace.d.ts.map +1 -1
  128. package/dist/schemas/inmemory-workspace.js +1 -15
  129. package/dist/schemas/reference-config/index.d.ts +6 -2
  130. package/dist/schemas/reference-config/index.d.ts.map +1 -1
  131. package/dist/schemas/reference-config/settings.d.ts +5 -1
  132. package/dist/schemas/reference-config/settings.d.ts.map +1 -1
  133. package/dist/schemas/type-guards.d.ts +24 -0
  134. package/dist/schemas/type-guards.d.ts.map +1 -0
  135. package/dist/schemas/type-guards.js +30 -0
  136. package/dist/schemas/v3.1/openapi/index.d.ts +4 -3
  137. package/dist/schemas/v3.1/openapi/index.d.ts.map +1 -1
  138. package/dist/schemas/v3.1/openapi/index.js +2 -3
  139. package/dist/schemas/v3.1/strict/encoding.d.ts +12 -0
  140. package/dist/schemas/v3.1/strict/encoding.d.ts.map +1 -1
  141. package/dist/schemas/v3.1/strict/encoding.js +11 -0
  142. package/dist/schemas/v3.1/strict/openapi-document.d.ts +179 -39
  143. package/dist/schemas/v3.1/strict/openapi-document.d.ts.map +1 -1
  144. package/dist/schemas/v3.1/strict/openapi-document.js +2 -2
  145. package/dist/schemas/workspace-specification/index.d.ts +1 -1
  146. package/dist/schemas/workspace.d.ts +4 -4377
  147. package/dist/schemas/workspace.d.ts.map +1 -1
  148. package/dist/schemas/workspace.js +0 -8
  149. package/dist/schemas.d.ts +2 -1
  150. package/dist/schemas.d.ts.map +1 -1
  151. package/dist/schemas.js +1 -1
  152. package/package.json +8 -7
@@ -3,18 +3,25 @@ import { createIndexDbConnection } from '../persistence/indexdb.js';
3
3
  import { v1InitialMigration } from '../persistence/migrations/v1-initial.js';
4
4
  import { v2TeamToLocalMigration } from '../persistence/migrations/v2-team-to-local.js';
5
5
  /**
6
- * Generates a workspace ID from team slug and workspace slug. Used as the key
7
- * in the per-workspace chunk tables (meta, documents, ...).
6
+ * Generates a fresh `workspaceUid` for new workspaces.
7
+ *
8
+ * Wraps `crypto.randomUUID` so every caller produces UIDs in a consistent
9
+ * shape and we can swap the implementation later without rippling through
10
+ * the codebase.
8
11
  */
9
- export const getWorkspaceId = (teamSlug, slug) => `${teamSlug}/${slug}`;
12
+ export const generateWorkspaceUid = () => crypto.randomUUID();
10
13
  /**
11
14
  * Creates the persistence layer for the workspace store using IndexedDB.
12
- * This sets up all the required tables for storing workspace chunk information,
13
- * such as workspace meta, documents, original documents, intermediate documents, overrides, etc.
14
- * Each logical group (meta, documents, etc) gets its own table keyed appropriately for efficient sub-document access.
15
- * Returns an object containing `meta`, `documents`, `originalDocuments`, `intermediateDocuments`, `overrides`,
16
- * `documentMeta`, `documentConfigs`, and `workspace` sections, each exposing a `setItem` method
17
- * for upsetting records, and in the case of `workspace`, also `getItem` and `deleteItem`.
15
+ *
16
+ * Storage model:
17
+ * - `workspace` is the catalog. Its primary key is `workspaceUid`, a
18
+ * stable UUID that does not change when the user (or the server)
19
+ * renames the team or workspace slug. Two indexes back the runtime:
20
+ * - `teamSlug_slug` (unique) for slug-based URL lookups.
21
+ * - `teamUid` for team-scoped queries.
22
+ * - Every chunk table (`meta`, `documents`, ...) is keyed by
23
+ * `workspaceUid` so chunks survive slug renames without ever being
24
+ * re-keyed.
18
25
  */
19
26
  export const createWorkspaceStorePersistence = async () => {
20
27
  // The `tables` config below only describes the CURRENT shape for TypeScript
@@ -27,46 +34,49 @@ export const createWorkspaceStorePersistence = async () => {
27
34
  tables: {
28
35
  workspace: {
29
36
  schema: Type.Object({
30
- /** Visual name for a given workspace */
31
- name: Type.String(),
32
- /** Slug of the team this workspace belongs to. Use 'local' for personal workspaces. */
37
+ /** Stable UUID for the workspace; never changes after creation. */
38
+ workspaceUid: Type.String(),
39
+ /** UID of the team this workspace belongs to. Use 'local' for personal workspaces. */
40
+ teamUid: Type.String({ default: 'local' }),
41
+ /** Current team slug. Mutable metadata used to build URLs. */
33
42
  teamSlug: Type.String({ default: 'local' }),
34
- /** Slug of the workspace, unique within the team. */
43
+ /** Current workspace slug. Mutable metadata used to build URLs. */
35
44
  slug: Type.String({ default: 'local' }),
45
+ /** Visual name for a given workspace. */
46
+ name: Type.String(),
36
47
  }),
37
- keyPath: ['teamSlug', 'slug'],
48
+ keyPath: ['workspaceUid'],
38
49
  },
39
50
  meta: {
40
- schema: Type.Object({ workspaceId: Type.String(), data: Type.Any() }),
41
- keyPath: ['workspaceId'],
51
+ schema: Type.Object({ workspaceUid: Type.String(), data: Type.Any() }),
52
+ keyPath: ['workspaceUid'],
42
53
  },
43
54
  documents: {
44
- schema: Type.Object({ workspaceId: Type.String(), documentName: Type.String(), data: Type.Any() }),
45
- keyPath: ['workspaceId', 'documentName'],
55
+ schema: Type.Object({ workspaceUid: Type.String(), documentName: Type.String(), data: Type.Any() }),
56
+ keyPath: ['workspaceUid', 'documentName'],
46
57
  },
47
58
  originalDocuments: {
48
- schema: Type.Object({ workspaceId: Type.String(), documentName: Type.String(), data: Type.Any() }),
49
- keyPath: ['workspaceId', 'documentName'],
59
+ schema: Type.Object({ workspaceUid: Type.String(), documentName: Type.String(), data: Type.Any() }),
60
+ keyPath: ['workspaceUid', 'documentName'],
50
61
  },
51
62
  intermediateDocuments: {
52
- schema: Type.Object({ workspaceId: Type.String(), documentName: Type.String(), data: Type.Any() }),
53
- keyPath: ['workspaceId', 'documentName'],
63
+ schema: Type.Object({ workspaceUid: Type.String(), documentName: Type.String(), data: Type.Any() }),
64
+ keyPath: ['workspaceUid', 'documentName'],
54
65
  },
55
66
  overrides: {
56
- schema: Type.Object({ workspaceId: Type.String(), documentName: Type.String(), data: Type.Any() }),
57
- keyPath: ['workspaceId', 'documentName'],
67
+ schema: Type.Object({ workspaceUid: Type.String(), documentName: Type.String(), data: Type.Any() }),
68
+ keyPath: ['workspaceUid', 'documentName'],
58
69
  },
59
70
  history: {
60
- schema: Type.Object({ workspaceId: Type.String(), documentName: Type.String(), data: Type.Any() }),
61
- keyPath: ['workspaceId', 'documentName'],
71
+ schema: Type.Object({ workspaceUid: Type.String(), documentName: Type.String(), data: Type.Any() }),
72
+ keyPath: ['workspaceUid', 'documentName'],
62
73
  },
63
74
  auth: {
64
- schema: Type.Object({ workspaceId: Type.String(), documentName: Type.String(), data: Type.Any() }),
65
- keyPath: ['workspaceId', 'documentName'],
75
+ schema: Type.Object({ workspaceUid: Type.String(), documentName: Type.String(), data: Type.Any() }),
76
+ keyPath: ['workspaceUid', 'documentName'],
66
77
  },
67
78
  },
68
79
  });
69
- // Tables wrappers for each logical section.
70
80
  const workspaceTable = connection.get('workspace');
71
81
  const metaTable = connection.get('meta');
72
82
  const documentsTable = connection.get('documents');
@@ -75,208 +85,264 @@ export const createWorkspaceStorePersistence = async () => {
75
85
  const overridesTable = connection.get('overrides');
76
86
  const historyTable = connection.get('history');
77
87
  const authTable = connection.get('auth');
78
- // The returned persistence API with logical sections for each table and mapping.
88
+ /**
89
+ * Resolves a workspace record by its `[teamSlug, slug]` pair using the
90
+ * unique secondary index. Returns `undefined` when no workspace matches.
91
+ *
92
+ * The compound index is unique, so at most one record is ever returned;
93
+ * we still go through `getRange` because the wrapper does not expose a
94
+ * single-key index lookup helper.
95
+ */
96
+ const findByTeamSlugAndSlug = async (teamSlug, slug) => {
97
+ const matches = await workspaceTable.getRange([teamSlug, slug], 'teamSlug_slug');
98
+ return matches[0];
99
+ };
100
+ /**
101
+ * Loads every chunk that belongs to a workspace and stitches them back
102
+ * into a single in-memory shape. Shared between `getItem` and
103
+ * `getItemBySlug` so both lookups produce identical output.
104
+ */
105
+ const assembleWorkspace = async (workspace) => {
106
+ const { workspaceUid } = workspace;
107
+ const [workspaceDocuments, workspaceOriginalDocuments, workspaceIntermediateDocuments, workspaceOverrides, workspaceMeta, workspaceHistory, workspaceAuth,] = await Promise.all([
108
+ documentsTable.getRange([workspaceUid]),
109
+ originalDocumentTable.getRange([workspaceUid]),
110
+ intermediateDocumentTable.getRange([workspaceUid]),
111
+ overridesTable.getRange([workspaceUid]),
112
+ metaTable.getItem({ workspaceUid }),
113
+ historyTable.getRange([workspaceUid]),
114
+ authTable.getRange([workspaceUid]),
115
+ ]);
116
+ return {
117
+ workspaceUid: workspace.workspaceUid,
118
+ teamUid: workspace.teamUid,
119
+ teamSlug: workspace.teamSlug,
120
+ slug: workspace.slug,
121
+ name: workspace.name,
122
+ workspace: {
123
+ documents: Object.fromEntries(workspaceDocuments.map((item) => [item.documentName, item.data])),
124
+ originalDocuments: Object.fromEntries(workspaceOriginalDocuments.map((item) => [item.documentName, item.data])),
125
+ intermediateDocuments: Object.fromEntries(workspaceIntermediateDocuments.map((item) => [item.documentName, item.data])),
126
+ overrides: Object.fromEntries(workspaceOverrides.map((item) => [item.documentName, item.data])),
127
+ meta: workspaceMeta?.data ?? {},
128
+ history: Object.fromEntries(workspaceHistory.map((item) => [item.documentName, item.data])),
129
+ auth: Object.fromEntries(workspaceAuth.map((item) => [item.documentName, item.data])),
130
+ },
131
+ };
132
+ };
79
133
  return {
80
134
  close: () => {
81
135
  connection.closeDatabase();
82
136
  },
83
137
  meta: {
84
138
  /**
85
- * Set meta data for a workspace.
139
+ * Loads persisted workspace meta only (no document or other chunk
140
+ * reads). Returns an empty object when no meta row exists yet.
86
141
  */
87
- setItem: async (workspaceId, data) => {
88
- await metaTable.addItem({ workspaceId }, { data });
142
+ getItem: async (workspaceUid) => {
143
+ const row = await metaTable.getItem({ workspaceUid });
144
+ return (row?.data ?? {});
145
+ },
146
+ /** Set meta data for a workspace. */
147
+ setItem: async (workspaceUid, data) => {
148
+ await metaTable.addItem({ workspaceUid }, { data });
89
149
  },
90
150
  },
91
151
  documents: {
92
- /**
93
- * Set (persist) a workspace document using workspaceId and documentName as composite key.
94
- */
95
- setItem: async (workspaceId, documentName, data) => {
96
- await documentsTable.addItem({ workspaceId, documentName }, { data });
152
+ /** Set (persist) a workspace document using workspaceUid and documentName as composite key. */
153
+ setItem: async (workspaceUid, documentName, data) => {
154
+ await documentsTable.addItem({ workspaceUid, documentName }, { data });
97
155
  },
98
156
  },
99
157
  originalDocuments: {
100
- /**
101
- * Set an original (raw) document for a workspace/document pair.
102
- */
103
- setItem: async (workspaceId, documentName, data) => {
104
- await originalDocumentTable.addItem({ workspaceId, documentName }, { data });
158
+ /** Set an original (raw) document for a workspace/document pair. */
159
+ setItem: async (workspaceUid, documentName, data) => {
160
+ await originalDocumentTable.addItem({ workspaceUid, documentName }, { data });
105
161
  },
106
162
  },
107
163
  intermediateDocuments: {
108
- /**
109
- * Set an intermediate (transformed) document for a workspace/document pair.
110
- */
111
- setItem: async (workspaceId, documentName, data) => {
112
- await intermediateDocumentTable.addItem({ workspaceId, documentName }, { data });
164
+ /** Set an intermediate (transformed) document for a workspace/document pair. */
165
+ setItem: async (workspaceUid, documentName, data) => {
166
+ await intermediateDocumentTable.addItem({ workspaceUid, documentName }, { data });
113
167
  },
114
168
  },
115
169
  overrides: {
116
- /**
117
- * Set document overrides for a workspace/document pair.
118
- */
119
- setItem: async (workspaceId, documentName, data) => {
120
- await overridesTable.addItem({ workspaceId, documentName }, { data });
170
+ /** Set document overrides for a workspace/document pair. */
171
+ setItem: async (workspaceUid, documentName, data) => {
172
+ await overridesTable.addItem({ workspaceUid, documentName }, { data });
121
173
  },
122
174
  },
123
175
  history: {
124
- /**
125
- * Set history for a document.
126
- */
127
- setItem: async (workspaceId, documentName, data) => {
128
- await historyTable.addItem({ workspaceId, documentName }, { data });
176
+ /** Set history for a document. */
177
+ setItem: async (workspaceUid, documentName, data) => {
178
+ await historyTable.addItem({ workspaceUid, documentName }, { data });
129
179
  },
130
180
  },
131
181
  auth: {
132
- /**
133
- * Set auth for a document.
134
- */
135
- setItem: async (workspaceId, documentName, data) => {
136
- await authTable.addItem({ workspaceId, documentName }, { data });
182
+ /** Set auth for a document. */
183
+ setItem: async (workspaceUid, documentName, data) => {
184
+ await authTable.addItem({ workspaceUid, documentName }, { data });
137
185
  },
138
186
  },
139
187
  workspace: {
140
188
  /**
141
- * Retrieves a workspace by its team + workspace slug.
142
- * Returns undefined if the workspace does not exist.
143
- * Gathers all workspace 'chunk' tables and assembles a full workspace shape.
189
+ * Retrieves a workspace by its stable UID, returning the full
190
+ * assembled state (chunks included). Returns `undefined` when no
191
+ * workspace matches.
192
+ *
193
+ * This is the primary lookup path because the UID never changes,
194
+ * making it safe to cache and reference across slug renames.
144
195
  */
145
- getItem: async ({ teamSlug = 'local', slug, }) => {
146
- const workspace = await workspaceTable.getItem({ teamSlug, slug });
196
+ getItem: async (workspaceUid) => {
197
+ const workspace = (await workspaceTable.getItem({ workspaceUid }));
147
198
  if (!workspace) {
148
199
  return undefined;
149
200
  }
150
- // Create a composite key for the workspace chunks.
151
- const id = getWorkspaceId(teamSlug, slug);
152
- // Retrieve all chunk records for this workspace.
153
- const workspaceDocuments = await documentsTable.getRange([id]);
154
- const workspaceOriginalDocuments = await originalDocumentTable.getRange([id]);
155
- const workspaceIntermediateDocuments = await intermediateDocumentTable.getRange([id]);
156
- const workspaceOverrides = await overridesTable.getRange([id]);
157
- const workspaceMeta = await metaTable.getItem({ workspaceId: id });
158
- const workspaceHistory = await historyTable.getRange([id]);
159
- const workspaceAuth = await authTable.getRange([id]);
160
- // Compose the workspace structure from table records.
161
- return {
162
- name: workspace.name,
163
- teamSlug: workspace.teamSlug,
164
- slug: workspace.slug,
165
- workspace: {
166
- documents: Object.fromEntries(workspaceDocuments.map((item) => [item.documentName, item.data])),
167
- originalDocuments: Object.fromEntries(workspaceOriginalDocuments.map((item) => [item.documentName, item.data])),
168
- intermediateDocuments: Object.fromEntries(workspaceIntermediateDocuments.map((item) => [item.documentName, item.data])),
169
- overrides: Object.fromEntries(workspaceOverrides.map((item) => [item.documentName, item.data])),
170
- meta: workspaceMeta?.data,
171
- history: Object.fromEntries(workspaceHistory.map((item) => [item.documentName, item.data])),
172
- auth: Object.fromEntries(workspaceAuth.map((item) => [item.documentName, item.data])),
173
- },
174
- };
201
+ return assembleWorkspace(workspace);
175
202
  },
176
203
  /**
177
- * Retrieves all workspaces from the database.
178
- * Returns only the workspace ID and name for each workspace.
179
- * To get the full workspace data including documents and metadata, use getItem() with a specific ID.
180
- * Returns an empty array if no workspaces exist.
204
+ * Retrieves a workspace by its mutable `[teamSlug, slug]` pair.
205
+ *
206
+ * Use this when the only thing you have is the URL for example
207
+ * when the router needs to resolve `/@<teamSlug>/<workspaceSlug>`
208
+ * back to a workspace. For all other cases, prefer `getItem(uid)`
209
+ * because the slugs can change at any time.
210
+ */
211
+ getItemBySlug: async ({ teamSlug = 'local', slug, }) => {
212
+ const workspace = await findByTeamSlugAndSlug(teamSlug, slug);
213
+ if (!workspace) {
214
+ return undefined;
215
+ }
216
+ return assembleWorkspace(workspace);
217
+ },
218
+ /**
219
+ * Retrieves all workspace catalog records.
220
+ *
221
+ * Only returns the workspace shell (`workspaceUid`, `teamUid`,
222
+ * `teamSlug`, `slug`, `name`). To get the full workspace data
223
+ * including documents and metadata, use `getItem(workspaceUid)`.
181
224
  */
182
225
  getAll: async () => {
183
- return await workspaceTable.getAll();
226
+ return (await workspaceTable.getAll());
227
+ },
228
+ /**
229
+ * Retrieves all workspaces for a given team UID. Uses the `teamUid`
230
+ * index, so this is O(matches) rather than a full scan.
231
+ *
232
+ * Prefer this over `getAllByTeamSlug` because the team UID is the
233
+ * canonical identifier and survives team-slug renames.
234
+ */
235
+ getAllByTeamUid: async (teamUid) => {
236
+ return (await workspaceTable.getRange([teamUid], 'teamUid'));
184
237
  },
185
238
  /**
186
- * Retrieves all workspaces for a given team slug. Uses the primary key
187
- * prefix so no secondary index is required.
239
+ * Retrieves all workspaces for a given team slug. Uses the
240
+ * `teamSlug_slug` compound index as a prefix scan. Useful when the
241
+ * only thing on hand is the URL segment; otherwise prefer
242
+ * `getAllByTeamUid`.
188
243
  */
189
244
  getAllByTeamSlug: async (teamSlug) => {
190
- return await workspaceTable.getRange([teamSlug]);
245
+ return (await workspaceTable.getRange([teamSlug], 'teamSlug_slug'));
191
246
  },
192
247
  /**
193
- * Saves a workspace to the database.
194
- * All chunks (meta, documents, configs, etc.) are upsert in their respective tables.
195
- * If a workspace with the same ID already exists, it will be replaced.
248
+ * Saves a workspace and all of its chunks. The caller is responsible
249
+ * for providing a stable `workspaceUid` (typically `crypto.randomUUID()`
250
+ * for new records, or the existing UID for updates).
251
+ *
252
+ * `teamSlug` and `slug` are validated by the underlying unique index;
253
+ * attempting to persist a duplicate pair will reject the transaction.
196
254
  */
197
- setItem: async ({ teamSlug = 'local', slug }, value) => {
198
- const workspace = await workspaceTable.addItem({ teamSlug, slug }, {
199
- name: value.name,
200
- });
201
- const id = getWorkspaceId(teamSlug, slug);
202
- // Save all meta info for workspace.
203
- await metaTable.addItem({ workspaceId: id }, { data: value.workspace.meta });
204
- // Persist all workspace documents (chunks).
205
- await Promise.all(Object.entries(value.workspace.documents ?? {}).map(([name, data]) => {
206
- return documentsTable.addItem({ workspaceId: id, documentName: name }, { data });
207
- }));
208
- // Persist all original documents.
209
- await Promise.all(Object.entries(value.workspace.originalDocuments ?? {}).map(([name, data]) => {
210
- return originalDocumentTable.addItem({ workspaceId: id, documentName: name }, { data });
211
- }));
212
- // Persist all intermediate documents.
213
- await Promise.all(Object.entries(value.workspace.intermediateDocuments ?? {}).map(([name, data]) => {
214
- return intermediateDocumentTable.addItem({ workspaceId: id, documentName: name }, { data });
215
- }));
216
- // Persist all document overrides.
217
- await Promise.all(Object.entries(value.workspace.overrides ?? {}).map(([name, data]) => {
218
- return overridesTable.addItem({ workspaceId: id, documentName: name }, { data });
219
- }));
220
- // Persist all history.
221
- await Promise.all(Object.entries(value.workspace.history ?? {}).map(([name, data]) => {
222
- return historyTable.addItem({ workspaceId: id, documentName: name }, { data });
223
- }));
224
- // Persist all auth.
225
- await Promise.all(Object.entries(value.workspace.auth ?? {}).map(([name, data]) => {
226
- return authTable.addItem({ workspaceId: id, documentName: name }, { data });
227
- }));
255
+ setItem: async ({ workspaceUid, teamUid = 'local', teamSlug = 'local', slug, }, value) => {
256
+ const workspace = (await workspaceTable.addItem({ workspaceUid }, { teamUid, teamSlug, slug, name: value.name }));
257
+ await metaTable.addItem({ workspaceUid }, { data: value.workspace.meta });
258
+ await Promise.all([
259
+ ...Object.entries(value.workspace.documents ?? {}).map(([name, data]) => documentsTable.addItem({ workspaceUid, documentName: name }, { data })),
260
+ ...Object.entries(value.workspace.originalDocuments ?? {}).map(([name, data]) => originalDocumentTable.addItem({ workspaceUid, documentName: name }, { data })),
261
+ ...Object.entries(value.workspace.intermediateDocuments ?? {}).map(([name, data]) => intermediateDocumentTable.addItem({ workspaceUid, documentName: name }, { data })),
262
+ ...Object.entries(value.workspace.overrides ?? {}).map(([name, data]) => overridesTable.addItem({ workspaceUid, documentName: name }, { data })),
263
+ ...Object.entries(value.workspace.history ?? {}).map(([name, data]) => historyTable.addItem({ workspaceUid, documentName: name }, { data })),
264
+ ...Object.entries(value.workspace.auth ?? {}).map(([name, data]) => authTable.addItem({ workspaceUid, documentName: name }, { data })),
265
+ ]);
228
266
  return workspace;
229
267
  },
230
268
  /**
231
- * Deletes an entire workspace and all associated chunk records from all tables by ID.
269
+ * Deletes an entire workspace and every chunk that belongs to it.
270
+ * Safe to call on a workspace that does not exist — the chunk
271
+ * deletions are range scans that simply find nothing to delete.
232
272
  */
233
- deleteItem: async ({ teamSlug = 'local', slug }) => {
234
- const id = getWorkspaceId(teamSlug, slug);
235
- await workspaceTable.deleteItem({ teamSlug, slug });
236
- // Remove all workspace-related records from all chunk tables.
273
+ deleteItem: async (workspaceUid) => {
274
+ await workspaceTable.deleteItem({ workspaceUid });
237
275
  await Promise.all([
238
- // By id
239
- metaTable.deleteItem({ workspaceId: id }),
240
- // By range (composite-key tables)
241
- documentsTable.deleteRange([id]),
242
- originalDocumentTable.deleteRange([id]),
243
- intermediateDocumentTable.deleteRange([id]),
244
- overridesTable.deleteRange([id]),
245
- historyTable.deleteRange([id]),
246
- authTable.deleteRange([id]),
276
+ metaTable.deleteItem({ workspaceUid }),
277
+ documentsTable.deleteRange([workspaceUid]),
278
+ originalDocumentTable.deleteRange([workspaceUid]),
279
+ intermediateDocumentTable.deleteRange([workspaceUid]),
280
+ overridesTable.deleteRange([workspaceUid]),
281
+ historyTable.deleteRange([workspaceUid]),
282
+ authTable.deleteRange([workspaceUid]),
247
283
  ]);
248
284
  },
249
285
  /**
250
- * Deletes a single document and all related records (overrides, history, auth, etc.)
251
- * for the given workspace and document name from all relevant tables.
286
+ * Deletes a single document and all related chunks (overrides,
287
+ * history, auth, ...) for the given workspace/document pair.
252
288
  */
253
- deleteDocument: async (workspaceId, documentName) => {
289
+ deleteDocument: async (workspaceUid, documentName) => {
254
290
  await Promise.all([
255
- documentsTable.deleteItem({ workspaceId, documentName }),
256
- intermediateDocumentTable.deleteItem({ workspaceId, documentName }),
257
- originalDocumentTable.deleteItem({ workspaceId, documentName }),
258
- overridesTable.deleteItem({ workspaceId, documentName }),
259
- historyTable.deleteItem({ workspaceId, documentName }),
260
- authTable.deleteItem({ workspaceId, documentName }),
291
+ documentsTable.deleteItem({ workspaceUid, documentName }),
292
+ intermediateDocumentTable.deleteItem({ workspaceUid, documentName }),
293
+ originalDocumentTable.deleteItem({ workspaceUid, documentName }),
294
+ overridesTable.deleteItem({ workspaceUid, documentName }),
295
+ historyTable.deleteItem({ workspaceUid, documentName }),
296
+ authTable.deleteItem({ workspaceUid, documentName }),
261
297
  ]);
262
298
  },
263
299
  /**
264
- * Updates the name of an existing workspace.
265
- * Returns the updated workspace object, or undefined if the workspace does not exist.
300
+ * Updates the name of an existing workspace. Returns the updated
301
+ * record, or `undefined` when the workspace does not exist.
266
302
  */
267
- updateName: async ({ teamSlug = 'local', slug }, name) => {
268
- const workspace = await workspaceTable.getItem({ teamSlug, slug });
303
+ updateName: async (workspaceUid, name) => {
304
+ const workspace = (await workspaceTable.getItem({ workspaceUid }));
269
305
  if (!workspace) {
270
306
  return undefined;
271
307
  }
272
- // Update the workspace name
273
- return await workspaceTable.addItem({ teamSlug, slug }, { ...workspace, name });
308
+ return (await workspaceTable.addItem({ workspaceUid }, { ...workspace, name }));
274
309
  },
275
310
  /**
276
- * Checks if a workspace with the given team + workspace slug exists in the store.
311
+ * Updates the mutable slug metadata for an existing workspace.
312
+ * Returns the updated record, or `undefined` when the workspace
313
+ * does not exist, or when another workspace already owns the target
314
+ * `[teamSlug, slug]` pair (the `teamSlug_slug` index is unique).
315
+ *
316
+ * Use this when the server tells us a team slug or workspace slug
317
+ * has changed. The `workspaceUid` stays the same, so all chunk
318
+ * references continue to resolve.
277
319
  */
278
- has: async ({ teamSlug = 'local', slug }) => {
279
- return (await workspaceTable.getItem({ teamSlug, slug })) !== undefined;
320
+ updateSlugs: async (workspaceUid, slugs) => {
321
+ const workspace = (await workspaceTable.getItem({ workspaceUid }));
322
+ if (!workspace) {
323
+ return undefined;
324
+ }
325
+ const nextTeamSlug = slugs.teamSlug ?? workspace.teamSlug;
326
+ const nextSlug = slugs.slug ?? workspace.slug;
327
+ if (nextTeamSlug !== workspace.teamSlug || nextSlug !== workspace.slug) {
328
+ const occupant = await findByTeamSlugAndSlug(nextTeamSlug, nextSlug);
329
+ if (occupant && occupant.workspaceUid !== workspaceUid) {
330
+ return undefined;
331
+ }
332
+ }
333
+ return (await workspaceTable.addItem({ workspaceUid }, {
334
+ ...workspace,
335
+ teamSlug: nextTeamSlug,
336
+ slug: nextSlug,
337
+ }));
338
+ },
339
+ /** Checks if a workspace with the given UID exists. */
340
+ has: async (workspaceUid) => {
341
+ return (await workspaceTable.getItem({ workspaceUid })) !== undefined;
342
+ },
343
+ /** Checks if a workspace with the given `[teamSlug, slug]` pair exists. */
344
+ hasSlug: async ({ teamSlug = 'local', slug }) => {
345
+ return (await findByTeamSlugAndSlug(teamSlug, slug)) !== undefined;
280
346
  },
281
347
  },
282
348
  clear: async () => {
@@ -1,25 +1,42 @@
1
1
  import type { Migration } from '../../persistence/indexdb.js';
2
2
  type WorkspaceRecordV1 = {
3
3
  name: string;
4
- /** Old field dropped entirely in v2. */
4
+ /** Team UID at the time of save. Often missing for personal workspaces. */
5
5
  teamUid?: string;
6
+ /** Team slug at the time of save. Doubled as the team identifier in v1. */
6
7
  namespace: string;
8
+ /** Workspace slug at the time of save. */
7
9
  slug: string;
8
10
  };
9
11
  type WorkspaceRecordV2 = {
10
- name: string;
12
+ workspaceUid: string;
13
+ teamUid: string;
11
14
  teamSlug: string;
12
15
  slug: string;
16
+ name: string;
13
17
  };
14
18
  /**
15
19
  * Picks a slug that does not collide with anything in `taken`.
16
20
  * Falls back to `<slug>-2`, `<slug>-3`, ... when the desired slug is already used.
21
+ *
22
+ * Collapsing every legacy workspace into the local team can produce
23
+ * `[local, <slug>]` collisions whenever a team workspace shared a slug
24
+ * with an existing local workspace (or with another team workspace). The
25
+ * unique `[teamSlug, slug]` index would otherwise reject the upgrade, so
26
+ * this helper resolves collisions deterministically.
17
27
  */
18
28
  export declare const pickUniqueSlug: (desired: string, taken: ReadonlySet<string>) => string;
19
29
  /**
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.
30
+ * Computes the new shape for every workspace.
31
+ *
32
+ * Every record is collapsed into the local team: `teamUid` and `teamSlug`
33
+ * are both forced to `'local'`. Slug uniqueness is enforced by reserving
34
+ * the legacy local-team slugs first (they keep their slug verbatim), then
35
+ * placing every team workspace on top with a `-2`, `-3`, ... suffix when
36
+ * the desired slug is already taken.
37
+ *
38
+ * A fresh `workspaceUid` is generated for every record so the new
39
+ * identifier is stable across future slug renames.
23
40
  */
24
41
  export declare const planWorkspaceMigration: (workspaces: readonly WorkspaceRecordV1[]) => Array<{
25
42
  before: {
@@ -1 +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"}
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;AAoEtD,KAAK,iBAAiB,GAAG;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAA;IACjB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,KAAK,iBAAiB,GAAG;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,MAAM,EAAE,OAAO,WAAW,CAAC,MAAM,CAAC,KAAG,MAU5E,CAAA;AAcD;;;;;;;;;;;GAWG;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,CAkCjF,CAAA;AAiDD,eAAO,MAAM,sBAAsB,EAAE,SA8GpC,CAAA"}