@company-semantics/contracts 47.1.0 → 47.2.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/package.json +3 -3
- package/src/__tests__/resource-keys.test.ts +104 -0
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +219 -0
- package/src/generated/openapi-routes.ts +3 -0
- package/src/resource-key-types.ts +157 -0
- package/src/resource-keys.ts +26 -128
- package/src/resource-registry.ts +69 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@company-semantics/contracts",
|
|
3
|
-
"version": "47.
|
|
3
|
+
"version": "47.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -148,10 +148,10 @@
|
|
|
148
148
|
"markdownlint-cli2": "^0.23.2",
|
|
149
149
|
"openapi-typescript": "^7.13.0",
|
|
150
150
|
"prettier": "^3.9.6",
|
|
151
|
-
"tsx": "^4.23.
|
|
151
|
+
"tsx": "^4.23.11",
|
|
152
152
|
"typescript": "^5.8.3",
|
|
153
153
|
"typescript-eslint": "^8.66.0",
|
|
154
|
-
"vite": "^8.2.
|
|
154
|
+
"vite": "^8.2.1",
|
|
155
155
|
"vitest": "^4.1.10",
|
|
156
156
|
"yaml": "^2.9.0"
|
|
157
157
|
},
|
|
@@ -234,6 +234,110 @@ describe("resource-keys: companyMdAccessRequests (per-doc identity)", () => {
|
|
|
234
234
|
});
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
+
describe("resource-keys: companyMdDocVersions (per-doc version list)", () => {
|
|
238
|
+
const DOC_ID = "44444444-4444-4444-8444-444444444444";
|
|
239
|
+
const key: ResourceKey = {
|
|
240
|
+
type: "companyMdDocVersions",
|
|
241
|
+
orgId: ORG_ID,
|
|
242
|
+
docId: DOC_ID,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
it("serialises to [type, orgId, docId]", () => {
|
|
246
|
+
expect(toQueryKey(key)).toEqual(["companyMdDocVersions", ORG_ID, DOC_ID]);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("round-trips through fromQueryKey", () => {
|
|
250
|
+
expect(fromQueryKey(toQueryKey(key))).toEqual(key);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("discriminates two keys differing only in docId", () => {
|
|
254
|
+
const other: ResourceKey = {
|
|
255
|
+
...key,
|
|
256
|
+
docId: "55555555-5555-4555-8555-555555555555",
|
|
257
|
+
};
|
|
258
|
+
expect(matchesResourceKey(toQueryKey(key), key)).toBe(true);
|
|
259
|
+
expect(matchesResourceKey(toQueryKey(other), key)).toBe(false);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("does NOT collide with companyMdAccessRequests, which shares its shape", () => {
|
|
263
|
+
// Same segments, same field name, same document — only the type tag tells
|
|
264
|
+
// the two apart. An owner's access-request inbox and a version list must
|
|
265
|
+
// never invalidate each other.
|
|
266
|
+
const requestsKey: ResourceKey = {
|
|
267
|
+
type: "companyMdAccessRequests",
|
|
268
|
+
orgId: ORG_ID,
|
|
269
|
+
docId: DOC_ID,
|
|
270
|
+
};
|
|
271
|
+
expect(matchesResourceKey(toQueryKey(requestsKey), key)).toBe(false);
|
|
272
|
+
expect(matchesResourceKey(toQueryKey(key), requestsKey)).toBe(false);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe("resource-keys: companyMdDocVersion (composite version identity)", () => {
|
|
277
|
+
// The second composite member. Its identity is (docId, versionId), so it
|
|
278
|
+
// exercises the same four-segment path `commentThreads` opened — and, unlike
|
|
279
|
+
// that key, it has a SIBLING of the same doc scope one segment shorter
|
|
280
|
+
// (companyMdDocVersions), which is the collision the split exists to avoid.
|
|
281
|
+
const DOC_ID = "44444444-4444-4444-8444-444444444444";
|
|
282
|
+
const VERSION_ID = "66666666-6666-4666-8666-666666666666";
|
|
283
|
+
const key: ResourceKey = {
|
|
284
|
+
type: "companyMdDocVersion",
|
|
285
|
+
orgId: ORG_ID,
|
|
286
|
+
docId: DOC_ID,
|
|
287
|
+
versionId: VERSION_ID,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
it("serialises to [type, orgId, docId, versionId]", () => {
|
|
291
|
+
const qk = toQueryKey(key);
|
|
292
|
+
expect(qk).toEqual(["companyMdDocVersion", ORG_ID, DOC_ID, VERSION_ID]);
|
|
293
|
+
expect(qk).toHaveLength(4);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("round-trips through fromQueryKey", () => {
|
|
297
|
+
expect(fromQueryKey(toQueryKey(key))).toEqual(key);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("is recognised by isResourceKeyShape", () => {
|
|
301
|
+
expect(isResourceKeyShape(key)).toBe(true);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("discriminates two versions of the same document", () => {
|
|
305
|
+
const other: ResourceKey = {
|
|
306
|
+
...key,
|
|
307
|
+
versionId: "77777777-7777-4777-8777-777777777777",
|
|
308
|
+
};
|
|
309
|
+
expect(matchesResourceKey(toQueryKey(key), key)).toBe(true);
|
|
310
|
+
expect(matchesResourceKey(toQueryKey(other), key)).toBe(false);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("discriminates the same versionId under two different documents", () => {
|
|
314
|
+
const other: ResourceKey = {
|
|
315
|
+
...key,
|
|
316
|
+
docId: "55555555-5555-4555-8555-555555555555",
|
|
317
|
+
};
|
|
318
|
+
expect(matchesResourceKey(toQueryKey(other), key)).toBe(false);
|
|
319
|
+
expect(matchesResourceKey(toQueryKey(key), other)).toBe(false);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("is NOT invalidated by a sweep of the document's version LIST", () => {
|
|
323
|
+
// The whole reason the two members are distinct. A restore appends to the
|
|
324
|
+
// history and so invalidates `companyMdDocVersions`; the sealed bodies
|
|
325
|
+
// already fetched are immutable and must survive it.
|
|
326
|
+
const listKey: ResourceKey = {
|
|
327
|
+
type: "companyMdDocVersions",
|
|
328
|
+
orgId: ORG_ID,
|
|
329
|
+
docId: DOC_ID,
|
|
330
|
+
};
|
|
331
|
+
expect(matchesResourceKey(toQueryKey(key), listKey)).toBe(false);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("refuses a query key that is missing the version segment", () => {
|
|
335
|
+
expect(() => fromQueryKey(["companyMdDocVersion", ORG_ID, DOC_ID])).toThrow(
|
|
336
|
+
/companyMdDocVersion/,
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
237
341
|
describe("resource-keys: commentThreads (composite subject identity)", () => {
|
|
238
342
|
// The FIRST member of the union whose identity needs two segments. Every
|
|
239
343
|
// other key is [type, scope] or [type, scope, oneId], and `fromQueryKey`'s
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
|
|
2
|
-
export const SPEC_HASH = '
|
|
3
|
-
export const SPEC_HASH_FULL = '
|
|
2
|
+
export const SPEC_HASH = '6aca68a12d82' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = '6aca68a12d828a1efb9e1d1865a3955e8540d9c83563d2d6be6b04ae3eac7da1' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -1548,6 +1548,57 @@ export interface paths {
|
|
|
1548
1548
|
patch?: never;
|
|
1549
1549
|
trace?: never;
|
|
1550
1550
|
};
|
|
1551
|
+
"/api/company-md/docs/{id}/versions": {
|
|
1552
|
+
parameters: {
|
|
1553
|
+
query?: never;
|
|
1554
|
+
header?: never;
|
|
1555
|
+
path?: never;
|
|
1556
|
+
cookie?: never;
|
|
1557
|
+
};
|
|
1558
|
+
/** List a company.md document’s version history (metadata only) */
|
|
1559
|
+
get: operations["listCompanyMdDocVersions"];
|
|
1560
|
+
put?: never;
|
|
1561
|
+
post?: never;
|
|
1562
|
+
delete?: never;
|
|
1563
|
+
options?: never;
|
|
1564
|
+
head?: never;
|
|
1565
|
+
patch?: never;
|
|
1566
|
+
trace?: never;
|
|
1567
|
+
};
|
|
1568
|
+
"/api/company-md/docs/{id}/versions/{versionId}": {
|
|
1569
|
+
parameters: {
|
|
1570
|
+
query?: never;
|
|
1571
|
+
header?: never;
|
|
1572
|
+
path?: never;
|
|
1573
|
+
cookie?: never;
|
|
1574
|
+
};
|
|
1575
|
+
/** Read one sealed company.md version, body included */
|
|
1576
|
+
get: operations["getCompanyMdDocVersion"];
|
|
1577
|
+
put?: never;
|
|
1578
|
+
post?: never;
|
|
1579
|
+
delete?: never;
|
|
1580
|
+
options?: never;
|
|
1581
|
+
head?: never;
|
|
1582
|
+
patch?: never;
|
|
1583
|
+
trace?: never;
|
|
1584
|
+
};
|
|
1585
|
+
"/api/company-md/docs/{id}/versions/{versionId}/restore": {
|
|
1586
|
+
parameters: {
|
|
1587
|
+
query?: never;
|
|
1588
|
+
header?: never;
|
|
1589
|
+
path?: never;
|
|
1590
|
+
cookie?: never;
|
|
1591
|
+
};
|
|
1592
|
+
get?: never;
|
|
1593
|
+
put?: never;
|
|
1594
|
+
/** Restore a company.md document to a previous version */
|
|
1595
|
+
post: operations["restoreCompanyMdDocVersion"];
|
|
1596
|
+
delete?: never;
|
|
1597
|
+
options?: never;
|
|
1598
|
+
head?: never;
|
|
1599
|
+
patch?: never;
|
|
1600
|
+
trace?: never;
|
|
1601
|
+
};
|
|
1551
1602
|
"/api/company-md/extract": {
|
|
1552
1603
|
parameters: {
|
|
1553
1604
|
query?: never;
|
|
@@ -4915,6 +4966,51 @@ export interface components {
|
|
|
4915
4966
|
CompanyMdMentionableResponse: {
|
|
4916
4967
|
items: components["schemas"]["CompanyMdMentionableCandidate"][];
|
|
4917
4968
|
};
|
|
4969
|
+
/** @description A page of a company.md document’s version history, plus the opaque token a restore is gated on. */
|
|
4970
|
+
CompanyMdDocVersionsResponse: {
|
|
4971
|
+
currentRevision: string;
|
|
4972
|
+
entries: components["schemas"]["CompanyMdDocVersionEntry"][];
|
|
4973
|
+
nextCursor: string | null;
|
|
4974
|
+
};
|
|
4975
|
+
/** @description One sealed company.md version — its metadata and the body it sealed. */
|
|
4976
|
+
CompanyMdDocVersionContent: {
|
|
4977
|
+
id: string | null;
|
|
4978
|
+
/** @enum {string} */
|
|
4979
|
+
status: "sealed" | "open";
|
|
4980
|
+
/** @enum {string} */
|
|
4981
|
+
mutationSource: "human_edit" | "agent_edit" | "ingest" | "restore" | "system" | "baseline";
|
|
4982
|
+
sourceOperationId: string | null;
|
|
4983
|
+
actor: components["schemas"]["CompanyMdDocVersionActor"] | null;
|
|
4984
|
+
contributors: string[];
|
|
4985
|
+
initiatedBy: string | null;
|
|
4986
|
+
sessionStartedAt: string;
|
|
4987
|
+
sessionEndedAt: string;
|
|
4988
|
+
contentBytes: number;
|
|
4989
|
+
restoredFromVersionId: string | null;
|
|
4990
|
+
isCurrent: boolean;
|
|
4991
|
+
content: string;
|
|
4992
|
+
};
|
|
4993
|
+
/** @description One sealed version, or the in-progress session, as metadata only. */
|
|
4994
|
+
CompanyMdDocVersionEntry: {
|
|
4995
|
+
id: string | null;
|
|
4996
|
+
/** @enum {string} */
|
|
4997
|
+
status: "sealed" | "open";
|
|
4998
|
+
/** @enum {string} */
|
|
4999
|
+
mutationSource: "human_edit" | "agent_edit" | "ingest" | "restore" | "system" | "baseline";
|
|
5000
|
+
sourceOperationId: string | null;
|
|
5001
|
+
actor: components["schemas"]["CompanyMdDocVersionActor"] | null;
|
|
5002
|
+
contributors: string[];
|
|
5003
|
+
initiatedBy: string | null;
|
|
5004
|
+
sessionStartedAt: string;
|
|
5005
|
+
sessionEndedAt: string;
|
|
5006
|
+
contentBytes: number;
|
|
5007
|
+
restoredFromVersionId: string | null;
|
|
5008
|
+
isCurrent: boolean;
|
|
5009
|
+
};
|
|
5010
|
+
/** @description The document revision the client believes it is restoring over. A mismatch is a 409. */
|
|
5011
|
+
RestoreCompanyMdDocVersionRequest: {
|
|
5012
|
+
expectedRevision: string;
|
|
5013
|
+
};
|
|
4918
5014
|
CompanyMdContextBankResponse: {
|
|
4919
5015
|
items: {
|
|
4920
5016
|
id: string;
|
|
@@ -6351,6 +6447,12 @@ export interface components {
|
|
|
6351
6447
|
displayName: string;
|
|
6352
6448
|
avatarUrl: string | null;
|
|
6353
6449
|
};
|
|
6450
|
+
/** @description The singular actor a version is attributed to, in product vocabulary. */
|
|
6451
|
+
CompanyMdDocVersionActor: {
|
|
6452
|
+
/** @enum {string} */
|
|
6453
|
+
type: "member" | "agent" | "system";
|
|
6454
|
+
userId: string;
|
|
6455
|
+
};
|
|
6354
6456
|
/** @description An ACL-admitted context-doc discovery hit. */
|
|
6355
6457
|
ContextDocDiscoveryHit: {
|
|
6356
6458
|
id: string;
|
|
@@ -8868,6 +8970,123 @@ export interface operations {
|
|
|
8868
8970
|
};
|
|
8869
8971
|
};
|
|
8870
8972
|
};
|
|
8973
|
+
listCompanyMdDocVersions: {
|
|
8974
|
+
parameters: {
|
|
8975
|
+
query?: {
|
|
8976
|
+
cursor?: string;
|
|
8977
|
+
limit?: number;
|
|
8978
|
+
};
|
|
8979
|
+
header?: never;
|
|
8980
|
+
path: {
|
|
8981
|
+
id: string;
|
|
8982
|
+
};
|
|
8983
|
+
cookie?: never;
|
|
8984
|
+
};
|
|
8985
|
+
requestBody?: never;
|
|
8986
|
+
responses: {
|
|
8987
|
+
/** @description A page of version metadata, plus the document’s live revision token */
|
|
8988
|
+
200: {
|
|
8989
|
+
headers: {
|
|
8990
|
+
[name: string]: unknown;
|
|
8991
|
+
};
|
|
8992
|
+
content: {
|
|
8993
|
+
"application/json": components["schemas"]["CompanyMdDocVersionsResponse"];
|
|
8994
|
+
};
|
|
8995
|
+
};
|
|
8996
|
+
/** @description Invalid cursor or limit */
|
|
8997
|
+
400: {
|
|
8998
|
+
headers: {
|
|
8999
|
+
[name: string]: unknown;
|
|
9000
|
+
};
|
|
9001
|
+
content?: never;
|
|
9002
|
+
};
|
|
9003
|
+
/** @description Document not found, or the caller may not read its body */
|
|
9004
|
+
404: {
|
|
9005
|
+
headers: {
|
|
9006
|
+
[name: string]: unknown;
|
|
9007
|
+
};
|
|
9008
|
+
content?: never;
|
|
9009
|
+
};
|
|
9010
|
+
};
|
|
9011
|
+
};
|
|
9012
|
+
getCompanyMdDocVersion: {
|
|
9013
|
+
parameters: {
|
|
9014
|
+
query?: never;
|
|
9015
|
+
header?: never;
|
|
9016
|
+
path: {
|
|
9017
|
+
id: string;
|
|
9018
|
+
versionId: string;
|
|
9019
|
+
};
|
|
9020
|
+
cookie?: never;
|
|
9021
|
+
};
|
|
9022
|
+
requestBody?: never;
|
|
9023
|
+
responses: {
|
|
9024
|
+
/** @description The version’s metadata and the body it sealed */
|
|
9025
|
+
200: {
|
|
9026
|
+
headers: {
|
|
9027
|
+
[name: string]: unknown;
|
|
9028
|
+
};
|
|
9029
|
+
content: {
|
|
9030
|
+
"application/json": components["schemas"]["CompanyMdDocVersionContent"];
|
|
9031
|
+
};
|
|
9032
|
+
};
|
|
9033
|
+
/** @description Document or version not found, or its body is withheld */
|
|
9034
|
+
404: {
|
|
9035
|
+
headers: {
|
|
9036
|
+
[name: string]: unknown;
|
|
9037
|
+
};
|
|
9038
|
+
content?: never;
|
|
9039
|
+
};
|
|
9040
|
+
};
|
|
9041
|
+
};
|
|
9042
|
+
restoreCompanyMdDocVersion: {
|
|
9043
|
+
parameters: {
|
|
9044
|
+
query?: never;
|
|
9045
|
+
header?: never;
|
|
9046
|
+
path: {
|
|
9047
|
+
id: string;
|
|
9048
|
+
versionId: string;
|
|
9049
|
+
};
|
|
9050
|
+
cookie?: never;
|
|
9051
|
+
};
|
|
9052
|
+
requestBody: {
|
|
9053
|
+
content: {
|
|
9054
|
+
"application/json": components["schemas"]["RestoreCompanyMdDocVersionRequest"];
|
|
9055
|
+
};
|
|
9056
|
+
};
|
|
9057
|
+
responses: {
|
|
9058
|
+
/** @description The restore version that was appended to the document’s history */
|
|
9059
|
+
200: {
|
|
9060
|
+
headers: {
|
|
9061
|
+
[name: string]: unknown;
|
|
9062
|
+
};
|
|
9063
|
+
content: {
|
|
9064
|
+
"application/json": components["schemas"]["CompanyMdDocVersionEntry"];
|
|
9065
|
+
};
|
|
9066
|
+
};
|
|
9067
|
+
/** @description Missing or malformed expectedRevision */
|
|
9068
|
+
400: {
|
|
9069
|
+
headers: {
|
|
9070
|
+
[name: string]: unknown;
|
|
9071
|
+
};
|
|
9072
|
+
content?: never;
|
|
9073
|
+
};
|
|
9074
|
+
/** @description Document or version not found, or the caller may not edit it */
|
|
9075
|
+
404: {
|
|
9076
|
+
headers: {
|
|
9077
|
+
[name: string]: unknown;
|
|
9078
|
+
};
|
|
9079
|
+
content?: never;
|
|
9080
|
+
};
|
|
9081
|
+
/** @description The document changed since the caller read its revision */
|
|
9082
|
+
409: {
|
|
9083
|
+
headers: {
|
|
9084
|
+
[name: string]: unknown;
|
|
9085
|
+
};
|
|
9086
|
+
content?: never;
|
|
9087
|
+
};
|
|
9088
|
+
};
|
|
9089
|
+
};
|
|
8871
9090
|
extractCompanyMd: {
|
|
8872
9091
|
parameters: {
|
|
8873
9092
|
query?: never;
|
|
@@ -53,6 +53,9 @@ export const openApiRoutes = {
|
|
|
53
53
|
'/api/company-md/docs/{id}/sharing/policy': ['PUT'],
|
|
54
54
|
'/api/company-md/docs/{id}/title': ['PUT'],
|
|
55
55
|
'/api/company-md/docs/{id}/transfer-owner': ['POST'],
|
|
56
|
+
'/api/company-md/docs/{id}/versions': ['GET'],
|
|
57
|
+
'/api/company-md/docs/{id}/versions/{versionId}': ['GET'],
|
|
58
|
+
'/api/company-md/docs/{id}/versions/{versionId}/restore': ['POST'],
|
|
56
59
|
'/api/company-md/extract': ['POST'],
|
|
57
60
|
'/api/company-md/ownership/reassign': ['POST'],
|
|
58
61
|
'/api/company-md/ownership/reassign/suggestion': ['GET'],
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResourceKey — the shared resource VOCABULARY: one union member per resource
|
|
3
|
+
* the system can name, address, and invalidate.
|
|
4
|
+
*
|
|
5
|
+
* Split out of resource-keys.ts, which owns the OPERATIONS over these keys
|
|
6
|
+
* (scope resolution, the query-key round trip, matching). The union is a
|
|
7
|
+
* registry that grows one member at a time and carries a paragraph of rationale
|
|
8
|
+
* per non-obvious member; the operations are a fixed set of functions. They
|
|
9
|
+
* change for entirely different reasons, and only the union is appended to on
|
|
10
|
+
* essentially every resource addition.
|
|
11
|
+
*
|
|
12
|
+
* All keys MUST include scope (orgId or userId) to prevent cross-org cache leaks.
|
|
13
|
+
* Naming: camelCase nouns. Collections plural, identities singular.
|
|
14
|
+
*/
|
|
15
|
+
export type ResourceKey =
|
|
16
|
+
// Collections (plural) — lists of entities
|
|
17
|
+
| { type: "members"; orgId: string }
|
|
18
|
+
| { type: "departments"; orgId: string }
|
|
19
|
+
| { type: "chats"; orgId: string }
|
|
20
|
+
| { type: "teams"; orgId: string }
|
|
21
|
+
| { type: "integrations"; orgId: string }
|
|
22
|
+
| { type: "invites"; orgId: string }
|
|
23
|
+
| { type: "auditEvents"; orgId: string }
|
|
24
|
+
| { type: "timeline"; orgId: string }
|
|
25
|
+
// Identities (singular) — single entities
|
|
26
|
+
| { type: "member"; orgId: string; memberId: string }
|
|
27
|
+
| { type: "team"; orgId: string; teamId: string }
|
|
28
|
+
| { type: "department"; orgId: string; departmentId: string }
|
|
29
|
+
| { type: "chat"; orgId: string; chatId: string }
|
|
30
|
+
| { type: "companyMdDoc"; orgId: string; slug: string }
|
|
31
|
+
| { type: "companyMdContextBank"; orgId: string; slug: string }
|
|
32
|
+
// The access requests filed against ONE document — the owner's inbox, read by
|
|
33
|
+
// the Share dialog.
|
|
34
|
+
//
|
|
35
|
+
// Discriminated by `docId`, NOT `slug`, and the difference is load-bearing.
|
|
36
|
+
// `companyMdDoc` above carries the stable doc id under a field named `slug`
|
|
37
|
+
// for historical reasons; ADR-BE-315 made slugs only PARENT-SCOPED unique, so
|
|
38
|
+
// a bare slug does not identify a document. That field name is a wart to be
|
|
39
|
+
// contained, not propagated — a new key gets the honest name.
|
|
40
|
+
| { type: "companyMdAccessRequests"; orgId: string; docId: string }
|
|
41
|
+
// The sealed version history of ONE document (PRD-00927) — the metadata-only
|
|
42
|
+
// list the history panel pages through. Keyed by `docId` for the same reason
|
|
43
|
+
// `companyMdAccessRequests` is: the honest name for the stable document id,
|
|
44
|
+
// rather than propagating `companyMdDoc`'s legacy `slug` field name.
|
|
45
|
+
| { type: "companyMdDocVersions"; orgId: string; docId: string }
|
|
46
|
+
// ONE sealed version, body included.
|
|
47
|
+
//
|
|
48
|
+
// A member of its own rather than a `versionId` segment folded into the list
|
|
49
|
+
// key above, and the split is load-bearing. A restore APPENDS to the history,
|
|
50
|
+
// so it must invalidate `companyMdDocVersions` — and because a sealed version
|
|
51
|
+
// is immutable, every already-fetched body must SURVIVE that invalidation.
|
|
52
|
+
// Fold the two together and each restore evicts the whole set of historical
|
|
53
|
+
// bodies the reader just paged through, to re-fetch content that cannot have
|
|
54
|
+
// changed.
|
|
55
|
+
| {
|
|
56
|
+
type: "companyMdDocVersion";
|
|
57
|
+
orgId: string;
|
|
58
|
+
docId: string;
|
|
59
|
+
versionId: string;
|
|
60
|
+
}
|
|
61
|
+
// The comment threads hanging off ONE subject (ADR-CONTRACTS-116).
|
|
62
|
+
//
|
|
63
|
+
// Registered here rather than declared app-locally because THE KEY REGISTRY IS
|
|
64
|
+
// THE SSE COALESCER'S VOCABULARY: a `resource.invalidated` frame names a key,
|
|
65
|
+
// and a key the registry does not know cannot be routed through the coalescer
|
|
66
|
+
// at all. An app-local "commentThreads" would be a second vocabulary the
|
|
67
|
+
// server could never address.
|
|
68
|
+
//
|
|
69
|
+
// The only member with a TWO-SEGMENT identity, and it has to be: a thread
|
|
70
|
+
// list is addressed by (subjectType, subjectId), and the id alone is not
|
|
71
|
+
// enough — subject ids are per-class uuids, so two classes could collide and
|
|
72
|
+
// one subject's invalidation would flush another's cache. `subjectType` is
|
|
73
|
+
// typed `string` and not the closed `CommentSubjectType`: this is a cache
|
|
74
|
+
// vocabulary, `fromQueryKey` reconstructs keys from arbitrary wire strings,
|
|
75
|
+
// and narrowing it here would make that reconstruction a lie.
|
|
76
|
+
| {
|
|
77
|
+
type: "commentThreads";
|
|
78
|
+
orgId: string;
|
|
79
|
+
subjectType: string;
|
|
80
|
+
subjectId: string;
|
|
81
|
+
}
|
|
82
|
+
| { type: "workspace"; orgId: string }
|
|
83
|
+
| { type: "workspaceDomains"; orgId: string }
|
|
84
|
+
| { type: "authSettings"; orgId: string }
|
|
85
|
+
| { type: "billing"; orgId: string }
|
|
86
|
+
| { type: "aiUsage"; orgId: string }
|
|
87
|
+
| { type: "deletionEligibility"; orgId: string }
|
|
88
|
+
| { type: "transferOwnership"; orgId: string }
|
|
89
|
+
| { type: "companyMdDocs"; orgId: string }
|
|
90
|
+
// Directly-granted (non-inherited) ACL rows for the org, read by the admin
|
|
91
|
+
// grants surface (ADR-CONTRACTS-119).
|
|
92
|
+
| { type: "directGrants"; orgId: string }
|
|
93
|
+
// OrgUnit canonical model (ADR-BE-120) — Phase 2 Wave 4
|
|
94
|
+
| { type: "orgTree"; orgId: string }
|
|
95
|
+
| { type: "orgLevelConfig"; orgId: string }
|
|
96
|
+
| { type: "orgUnit"; orgId: string; unitId: string }
|
|
97
|
+
| { type: "orgUnitChildren"; orgId: string; unitId: string }
|
|
98
|
+
| { type: "orgUnitAncestors"; orgId: string; unitId: string }
|
|
99
|
+
| { type: "orgUnitMemberships"; orgId: string; unitId: string }
|
|
100
|
+
| { type: "orgUnitPermissions"; orgId: string; unitId: string }
|
|
101
|
+
// Open roles seated in a unit (ADR-BE-277) — a per-unit list, unitId-scoped
|
|
102
|
+
// like memberships/permissions. Read in the unit members view and the
|
|
103
|
+
// org-chart card; refreshed as a graph target of orgTree on structural
|
|
104
|
+
// mutations (create / advance lifecycle / fill). See ADR-CONTRACTS-065.
|
|
105
|
+
| { type: "orgUnitOpenRoles"; orgId: string; unitId: string }
|
|
106
|
+
// Org-unit owners list (ADR-CONTRACTS-052) — owners are an org-wide
|
|
107
|
+
// projection, not a per-unit collection, so unitId is intentionally excluded.
|
|
108
|
+
| { type: "orgUnitOwners"; orgId: string }
|
|
109
|
+
// THIS VIEWER'S authority over one unit (ADR-CONTRACTS-119) — what the viewer
|
|
110
|
+
// may do here, not what the unit's permission rows say. Deliberately distinct
|
|
111
|
+
// from `orgUnitPermissions`: same subject, different resource and different
|
|
112
|
+
// invalidation trigger (a membership change alters the viewer's authority
|
|
113
|
+
// without touching the unit's rows), the `actionItems`/`feed` split above.
|
|
114
|
+
| { type: "orgUnitMyAuthority"; orgId: string; unitId: string }
|
|
115
|
+
// People reporting (ADR-BE-166) — drives the settings Org chart drill-down
|
|
116
|
+
| { type: "peopleOrgChart"; orgId: string }
|
|
117
|
+
// System-scoped (ADR-CONTRACTS-052) — tenant-less super-admin resources.
|
|
118
|
+
// No orgId/userId: these live above any single org. scope is the literal 'system'.
|
|
119
|
+
| { type: "internalAdminAiProviders"; scope: "system" }
|
|
120
|
+
| { type: "internalAdminPrompts"; scope: "system" }
|
|
121
|
+
| { type: "internalAdminAiRuntimeDefaults"; scope: "system" }
|
|
122
|
+
// Software-factory surfaces (ADR-BE-239 / ADR-BE-243) — internal-admin
|
|
123
|
+
// dashboard reads over Global-infra factory tables; tenant-less, scope 'system'.
|
|
124
|
+
| { type: "factoryFloor"; scope: "system" }
|
|
125
|
+
| { type: "factorySnapshot"; scope: "system" }
|
|
126
|
+
// The KPI rollup over the same factory tables (ADR-CONTRACTS-119). Registered
|
|
127
|
+
// late: the app shipped it as a double-cast key literal, and because
|
|
128
|
+
// `useResource` disables a query whose `toQueryKey` throws, the panel it backs
|
|
129
|
+
// never fetched at all. An unregistered key is not a stale read, it is no read.
|
|
130
|
+
| { type: "factoryKpis"; scope: "system" }
|
|
131
|
+
// Everything awaiting the viewer's decision, across every domain
|
|
132
|
+
// (ADR-CONT-104). ORG-scoped, not user-scoped, even though the content is
|
|
133
|
+
// per-viewer: the bucket differs per org, so a user-keyed entry would serve
|
|
134
|
+
// one org's items after switching to another. Same shape as `chats` and
|
|
135
|
+
// `timeline`, which are likewise viewer-filtered but org-partitioned — and it
|
|
136
|
+
// inherits `resolveScope`'s impersonation handling for free.
|
|
137
|
+
| { type: "actionItems"; orgId: string }
|
|
138
|
+
// The merged top-of-/me/work read: action items AND the durable inbox
|
|
139
|
+
// (ADR-CONT-108). Org-scoped for the same reason `actionItems` is — the
|
|
140
|
+
// content is per-viewer but the bucket differs per org, so a user-keyed entry
|
|
141
|
+
// would serve one org's feed after switching to another.
|
|
142
|
+
//
|
|
143
|
+
// Distinct from `actionItems` rather than replacing it: that key still backs
|
|
144
|
+
// every badge in the shell, and the two have different invalidation triggers
|
|
145
|
+
// (a notification being READ changes the feed and not the bucket).
|
|
146
|
+
| { type: "feed"; orgId: string }
|
|
147
|
+
// User-scoped
|
|
148
|
+
| { type: "dismissedBanners"; userId: string }
|
|
149
|
+
| { type: "userOrgs"; userId: string }
|
|
150
|
+
| { type: "sessions"; userId: string }
|
|
151
|
+
// The viewer's personal doc (`/api/me/md`) — a per-user singleton, so it is
|
|
152
|
+
// keyed by userId alone with no doc identity segment. Its PAYLOAD is a work
|
|
153
|
+
// item, lazily materialized on first read (ADR-BE-487), but the resource is
|
|
154
|
+
// "this user's personal doc", not "work item <id>": a future generic
|
|
155
|
+
// `workItem` key would be a different resource with a different identity.
|
|
156
|
+
| { type: "userMd"; userId: string }
|
|
157
|
+
| { type: "viewer"; userId: string };
|
package/src/resource-keys.ts
CHANGED
|
@@ -1,131 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ResourceKey —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* ResourceKey operations — scope resolution, the query-key round trip, and
|
|
3
|
+
* shape/identity matching.
|
|
4
|
+
*
|
|
5
|
+
* The union itself lives in `resource-key-types.ts` and is re-exported here, so
|
|
6
|
+
* `./resource-keys` remains the one import path for the whole vocabulary
|
|
7
|
+
* (the package barrel and every consumer reach for it through this module).
|
|
5
8
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
| { type: "departments"; orgId: string }
|
|
10
|
-
| { type: "chats"; orgId: string }
|
|
11
|
-
| { type: "teams"; orgId: string }
|
|
12
|
-
| { type: "integrations"; orgId: string }
|
|
13
|
-
| { type: "invites"; orgId: string }
|
|
14
|
-
| { type: "auditEvents"; orgId: string }
|
|
15
|
-
| { type: "timeline"; orgId: string }
|
|
16
|
-
// Identities (singular) — single entities
|
|
17
|
-
| { type: "member"; orgId: string; memberId: string }
|
|
18
|
-
| { type: "team"; orgId: string; teamId: string }
|
|
19
|
-
| { type: "department"; orgId: string; departmentId: string }
|
|
20
|
-
| { type: "chat"; orgId: string; chatId: string }
|
|
21
|
-
| { type: "companyMdDoc"; orgId: string; slug: string }
|
|
22
|
-
| { type: "companyMdContextBank"; orgId: string; slug: string }
|
|
23
|
-
// The access requests filed against ONE document — the owner's inbox, read by
|
|
24
|
-
// the Share dialog.
|
|
25
|
-
//
|
|
26
|
-
// Discriminated by `docId`, NOT `slug`, and the difference is load-bearing.
|
|
27
|
-
// `companyMdDoc` above carries the stable doc id under a field named `slug`
|
|
28
|
-
// for historical reasons; ADR-BE-315 made slugs only PARENT-SCOPED unique, so
|
|
29
|
-
// a bare slug does not identify a document. That field name is a wart to be
|
|
30
|
-
// contained, not propagated — a new key gets the honest name.
|
|
31
|
-
| { type: "companyMdAccessRequests"; orgId: string; docId: string }
|
|
32
|
-
// The comment threads hanging off ONE subject (ADR-CONTRACTS-116).
|
|
33
|
-
//
|
|
34
|
-
// Registered here rather than declared app-locally because THE KEY REGISTRY IS
|
|
35
|
-
// THE SSE COALESCER'S VOCABULARY: a `resource.invalidated` frame names a key,
|
|
36
|
-
// and a key the registry does not know cannot be routed through the coalescer
|
|
37
|
-
// at all. An app-local "commentThreads" would be a second vocabulary the
|
|
38
|
-
// server could never address.
|
|
39
|
-
//
|
|
40
|
-
// The only member with a TWO-SEGMENT identity, and it has to be: a thread
|
|
41
|
-
// list is addressed by (subjectType, subjectId), and the id alone is not
|
|
42
|
-
// enough — subject ids are per-class uuids, so two classes could collide and
|
|
43
|
-
// one subject's invalidation would flush another's cache. `subjectType` is
|
|
44
|
-
// typed `string` and not the closed `CommentSubjectType`: this is a cache
|
|
45
|
-
// vocabulary, `fromQueryKey` reconstructs keys from arbitrary wire strings,
|
|
46
|
-
// and narrowing it here would make that reconstruction a lie.
|
|
47
|
-
| {
|
|
48
|
-
type: "commentThreads";
|
|
49
|
-
orgId: string;
|
|
50
|
-
subjectType: string;
|
|
51
|
-
subjectId: string;
|
|
52
|
-
}
|
|
53
|
-
| { type: "workspace"; orgId: string }
|
|
54
|
-
| { type: "workspaceDomains"; orgId: string }
|
|
55
|
-
| { type: "authSettings"; orgId: string }
|
|
56
|
-
| { type: "billing"; orgId: string }
|
|
57
|
-
| { type: "aiUsage"; orgId: string }
|
|
58
|
-
| { type: "deletionEligibility"; orgId: string }
|
|
59
|
-
| { type: "transferOwnership"; orgId: string }
|
|
60
|
-
| { type: "companyMdDocs"; orgId: string }
|
|
61
|
-
// Directly-granted (non-inherited) ACL rows for the org, read by the admin
|
|
62
|
-
// grants surface (ADR-CONTRACTS-119).
|
|
63
|
-
| { type: "directGrants"; orgId: string }
|
|
64
|
-
// OrgUnit canonical model (ADR-BE-120) — Phase 2 Wave 4
|
|
65
|
-
| { type: "orgTree"; orgId: string }
|
|
66
|
-
| { type: "orgLevelConfig"; orgId: string }
|
|
67
|
-
| { type: "orgUnit"; orgId: string; unitId: string }
|
|
68
|
-
| { type: "orgUnitChildren"; orgId: string; unitId: string }
|
|
69
|
-
| { type: "orgUnitAncestors"; orgId: string; unitId: string }
|
|
70
|
-
| { type: "orgUnitMemberships"; orgId: string; unitId: string }
|
|
71
|
-
| { type: "orgUnitPermissions"; orgId: string; unitId: string }
|
|
72
|
-
// Open roles seated in a unit (ADR-BE-277) — a per-unit list, unitId-scoped
|
|
73
|
-
// like memberships/permissions. Read in the unit members view and the
|
|
74
|
-
// org-chart card; refreshed as a graph target of orgTree on structural
|
|
75
|
-
// mutations (create / advance lifecycle / fill). See ADR-CONTRACTS-065.
|
|
76
|
-
| { type: "orgUnitOpenRoles"; orgId: string; unitId: string }
|
|
77
|
-
// Org-unit owners list (ADR-CONTRACTS-052) — owners are an org-wide
|
|
78
|
-
// projection, not a per-unit collection, so unitId is intentionally excluded.
|
|
79
|
-
| { type: "orgUnitOwners"; orgId: string }
|
|
80
|
-
// THIS VIEWER'S authority over one unit (ADR-CONTRACTS-119) — what the viewer
|
|
81
|
-
// may do here, not what the unit's permission rows say. Deliberately distinct
|
|
82
|
-
// from `orgUnitPermissions`: same subject, different resource and different
|
|
83
|
-
// invalidation trigger (a membership change alters the viewer's authority
|
|
84
|
-
// without touching the unit's rows), the `actionItems`/`feed` split above.
|
|
85
|
-
| { type: "orgUnitMyAuthority"; orgId: string; unitId: string }
|
|
86
|
-
// People reporting (ADR-BE-166) — drives the settings Org chart drill-down
|
|
87
|
-
| { type: "peopleOrgChart"; orgId: string }
|
|
88
|
-
// System-scoped (ADR-CONTRACTS-052) — tenant-less super-admin resources.
|
|
89
|
-
// No orgId/userId: these live above any single org. scope is the literal 'system'.
|
|
90
|
-
| { type: "internalAdminAiProviders"; scope: "system" }
|
|
91
|
-
| { type: "internalAdminPrompts"; scope: "system" }
|
|
92
|
-
| { type: "internalAdminAiRuntimeDefaults"; scope: "system" }
|
|
93
|
-
// Software-factory surfaces (ADR-BE-239 / ADR-BE-243) — internal-admin
|
|
94
|
-
// dashboard reads over Global-infra factory tables; tenant-less, scope 'system'.
|
|
95
|
-
| { type: "factoryFloor"; scope: "system" }
|
|
96
|
-
| { type: "factorySnapshot"; scope: "system" }
|
|
97
|
-
// The KPI rollup over the same factory tables (ADR-CONTRACTS-119). Registered
|
|
98
|
-
// late: the app shipped it as a double-cast key literal, and because
|
|
99
|
-
// `useResource` disables a query whose `toQueryKey` throws, the panel it backs
|
|
100
|
-
// never fetched at all. An unregistered key is not a stale read, it is no read.
|
|
101
|
-
| { type: "factoryKpis"; scope: "system" }
|
|
102
|
-
// Everything awaiting the viewer's decision, across every domain
|
|
103
|
-
// (ADR-CONT-104). ORG-scoped, not user-scoped, even though the content is
|
|
104
|
-
// per-viewer: the bucket differs per org, so a user-keyed entry would serve
|
|
105
|
-
// one org's items after switching to another. Same shape as `chats` and
|
|
106
|
-
// `timeline`, which are likewise viewer-filtered but org-partitioned — and it
|
|
107
|
-
// inherits `resolveScope`'s impersonation handling for free.
|
|
108
|
-
| { type: "actionItems"; orgId: string }
|
|
109
|
-
// The merged top-of-/me/work read: action items AND the durable inbox
|
|
110
|
-
// (ADR-CONT-108). Org-scoped for the same reason `actionItems` is — the
|
|
111
|
-
// content is per-viewer but the bucket differs per org, so a user-keyed entry
|
|
112
|
-
// would serve one org's feed after switching to another.
|
|
113
|
-
//
|
|
114
|
-
// Distinct from `actionItems` rather than replacing it: that key still backs
|
|
115
|
-
// every badge in the shell, and the two have different invalidation triggers
|
|
116
|
-
// (a notification being READ changes the feed and not the bucket).
|
|
117
|
-
| { type: "feed"; orgId: string }
|
|
118
|
-
// User-scoped
|
|
119
|
-
| { type: "dismissedBanners"; userId: string }
|
|
120
|
-
| { type: "userOrgs"; userId: string }
|
|
121
|
-
| { type: "sessions"; userId: string }
|
|
122
|
-
// The viewer's personal doc (`/api/me/md`) — a per-user singleton, so it is
|
|
123
|
-
// keyed by userId alone with no doc identity segment. Its PAYLOAD is a work
|
|
124
|
-
// item, lazily materialized on first read (ADR-BE-487), but the resource is
|
|
125
|
-
// "this user's personal doc", not "work item <id>": a future generic
|
|
126
|
-
// `workItem` key would be a different resource with a different identity.
|
|
127
|
-
| { type: "userMd"; userId: string }
|
|
128
|
-
| { type: "viewer"; userId: string };
|
|
9
|
+
|
|
10
|
+
export type { ResourceKey } from "./resource-key-types";
|
|
11
|
+
import type { ResourceKey } from "./resource-key-types";
|
|
129
12
|
|
|
130
13
|
/**
|
|
131
14
|
* Action — structured mutation key used across execution system, audit, permissions.
|
|
@@ -213,6 +96,7 @@ const IDENTITY_FIELDS = {
|
|
|
213
96
|
companyMdDoc: "slug",
|
|
214
97
|
companyMdContextBank: "slug",
|
|
215
98
|
companyMdAccessRequests: "docId",
|
|
99
|
+
companyMdDocVersions: "docId",
|
|
216
100
|
orgUnit: "unitId",
|
|
217
101
|
orgUnitChildren: "unitId",
|
|
218
102
|
orgUnitAncestors: "unitId",
|
|
@@ -233,6 +117,7 @@ const IDENTITY_FIELDS = {
|
|
|
233
117
|
*/
|
|
234
118
|
const COMPOSITE_IDENTITY_FIELDS = {
|
|
235
119
|
commentThreads: ["subjectType", "subjectId"],
|
|
120
|
+
companyMdDocVersion: ["docId", "versionId"],
|
|
236
121
|
} as const satisfies {
|
|
237
122
|
[T in ResourceKey["type"]]?: readonly IdentityFieldsOf<T>[];
|
|
238
123
|
};
|
|
@@ -303,9 +188,15 @@ type Assert<T extends true> = T;
|
|
|
303
188
|
* The false branch resolves to the UNROUTED LITERALS rather than to `false`, so
|
|
304
189
|
* the diagnostic names the culprit:
|
|
305
190
|
* `Type '"directGrants"' does not satisfy the constraint 'true'`.
|
|
306
|
-
*
|
|
191
|
+
*
|
|
192
|
+
* EXPORTED, though nothing consumes it. This package ships `src`, so every
|
|
193
|
+
* consumer typechecks this file under ITS OWN compiler options — and the
|
|
194
|
+
* backend sets `noUnusedLocals`, which rejects an unexported type alias that
|
|
195
|
+
* nothing references. Keeping it private broke `tsc` in a consumer while
|
|
196
|
+
* passing here, so the export is what makes the assertion portable, not a
|
|
197
|
+
* widening of the public vocabulary. Do not "tidy" it away.
|
|
307
198
|
*/
|
|
308
|
-
type
|
|
199
|
+
export type RoutingExhaustivenessWitness = Assert<
|
|
309
200
|
[Exclude<ResourceKey["type"], RoutedType>] extends [never]
|
|
310
201
|
? true
|
|
311
202
|
: Exclude<ResourceKey["type"], RoutedType>
|
|
@@ -335,6 +226,7 @@ export function toQueryKey(key: ResourceKey): readonly string[] {
|
|
|
335
226
|
case "companyMdContextBank":
|
|
336
227
|
return [key.type, key.orgId, key.slug] as const;
|
|
337
228
|
case "companyMdAccessRequests":
|
|
229
|
+
case "companyMdDocVersions":
|
|
338
230
|
return [key.type, key.orgId, key.docId] as const;
|
|
339
231
|
|
|
340
232
|
// Composite identity — subject CLASS then subject id, in that order, so the
|
|
@@ -343,6 +235,12 @@ export function toQueryKey(key: ResourceKey): readonly string[] {
|
|
|
343
235
|
case "commentThreads":
|
|
344
236
|
return [key.type, key.orgId, key.subjectType, key.subjectId] as const;
|
|
345
237
|
|
|
238
|
+
// Composite identity — the document, then the version within it. Outer
|
|
239
|
+
// scope first, same ordering rule as `commentThreads`, so the segments read
|
|
240
|
+
// in the order the URL that produced them spells: /docs/{id}/versions/{v}.
|
|
241
|
+
case "companyMdDocVersion":
|
|
242
|
+
return [key.type, key.orgId, key.docId, key.versionId] as const;
|
|
243
|
+
|
|
346
244
|
// OrgUnit identity keys (ADR-BE-120)
|
|
347
245
|
case "orgUnit":
|
|
348
246
|
case "orgUnitChildren":
|
package/src/resource-registry.ts
CHANGED
|
@@ -35,3 +35,72 @@ export const ResourceEntrySchema = z
|
|
|
35
35
|
|
|
36
36
|
export type ResourceEntry = z.infer<typeof ResourceEntrySchema>;
|
|
37
37
|
export type ResourceRegistry = ReadonlyMap<ResourceKey, ResourceEntry>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The shape a row is WRITTEN in, before the schema's branding transforms run.
|
|
41
|
+
*
|
|
42
|
+
* `ResourceEntry` is the PARSED form: `resource`, `staleTimeMs` and friends come
|
|
43
|
+
* out branded, and nothing hand-authored can ever be assignable to a brand. So
|
|
44
|
+
* `z.input` is the only type a literal row can be checked against — which is
|
|
45
|
+
* what makes the `satisfies` below a real check and not decoration.
|
|
46
|
+
*/
|
|
47
|
+
export type ResourceEntryInput = z.input<typeof ResourceEntrySchema>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Company.md version-history rows (PRD-00927) — the first rows this registry has
|
|
51
|
+
* carried since it was introduced as a schema with no entries.
|
|
52
|
+
*
|
|
53
|
+
* Exported as CHECKED LITERALS rather than a built `ResourceRegistry` Map on
|
|
54
|
+
* purpose. Building the Map means running `ResourceEntrySchema.parse` at module
|
|
55
|
+
* load to obtain the branded values, and this package is types-first: every
|
|
56
|
+
* consumer would pay that at import time to read a table that is fully known at
|
|
57
|
+
* compile time. `satisfies readonly ResourceEntryInput[]` gets the same
|
|
58
|
+
* field-level enforcement for free, and a consumer that wants the Map can parse
|
|
59
|
+
* these rows itself.
|
|
60
|
+
*
|
|
61
|
+
* TWO VOCABULARY CAVEATS, both deliberate and both for ADR-CONTRACTS-120:
|
|
62
|
+
*
|
|
63
|
+
* 1. `resource` here is the {@link ResourceKey} type TAG (`companyMdDocVersions`).
|
|
64
|
+
* The registry that currently holds live rows is the app's copy, and its
|
|
65
|
+
* `resource` values are PATH-shaped (`company-md/docs`, `orgs/:orgId/billing`).
|
|
66
|
+
* The two do not line up, and reconciling them is not this PRD's work —
|
|
67
|
+
* naming these rows after the keys they describe is the only choice that is
|
|
68
|
+
* self-consistent here.
|
|
69
|
+
* 2. `hydrationDepends` names only rows in THIS array. Pointing at the app's
|
|
70
|
+
* path-shaped names would be a dangling reference across that same gap.
|
|
71
|
+
*/
|
|
72
|
+
export const COMPANY_MD_VERSION_RESOURCE_ENTRIES = [
|
|
73
|
+
{
|
|
74
|
+
// The history panel is opened, never hydrated with the page — hence
|
|
75
|
+
// background/P3. A restore appends to this list, so it is the row that
|
|
76
|
+
// takes the invalidation.
|
|
77
|
+
resource: "companyMdDocVersions",
|
|
78
|
+
priority: "P3",
|
|
79
|
+
hydrationPhase: "background",
|
|
80
|
+
hydrationDepends: [],
|
|
81
|
+
mutationBehavior: "collapse",
|
|
82
|
+
staleTimeMs: 30_000,
|
|
83
|
+
degradationSection: "core",
|
|
84
|
+
queryBudgetMs: 5_000,
|
|
85
|
+
resourceFairnessCap: 1,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
// A sealed version is immutable, which is what makes the long staleTime
|
|
89
|
+
// safe rather than merely convenient: there is no edit that could make an
|
|
90
|
+
// already-fetched body wrong. Reached only through the list, hence the
|
|
91
|
+
// dependency.
|
|
92
|
+
//
|
|
93
|
+
// `serial`, not `collapse`: the mutation that targets this resource is
|
|
94
|
+
// restore, and it is guarded by `expectedRevision` — two in flight at once
|
|
95
|
+
// means one of them is guaranteed a 409 for no reason.
|
|
96
|
+
resource: "companyMdDocVersion",
|
|
97
|
+
priority: "P3",
|
|
98
|
+
hydrationPhase: "background",
|
|
99
|
+
hydrationDepends: ["companyMdDocVersions"],
|
|
100
|
+
mutationBehavior: "serial",
|
|
101
|
+
staleTimeMs: 300_000,
|
|
102
|
+
degradationSection: "core",
|
|
103
|
+
queryBudgetMs: 5_000,
|
|
104
|
+
resourceFairnessCap: 1,
|
|
105
|
+
},
|
|
106
|
+
] satisfies readonly ResourceEntryInput[];
|