@synap-core/sdk 1.5.0 → 1.5.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.
- package/CHANGELOG.md +82 -0
- package/README.md +16 -0
- package/dist/connect.cjs +9 -2
- package/dist/connect.cjs.map +1 -1
- package/dist/connect.d.cts +17 -1
- package/dist/connect.d.ts +17 -1
- package/dist/connect.js +9 -2
- package/dist/connect.js.map +1 -1
- package/dist/testing.cjs +124 -11
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +50 -1
- package/dist/testing.d.ts +50 -1
- package/dist/testing.js +124 -11
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
- package/src/connect/auth.ts +15 -2
- package/src/connect/client.ts +3 -0
- package/src/connect/hub-types.ts +17 -0
- package/src/connect/index.ts +1 -0
- package/src/testing/auth-server.ts +120 -12
- package/src/testing/roles.ts +27 -0
- package/src/testing/seed.ts +78 -2
package/dist/testing.d.cts
CHANGED
|
@@ -51,14 +51,63 @@ declare const MOCK_SITE: MockSite;
|
|
|
51
51
|
* `relations` as graph edges for `GET /entities/:id/connections`, `proposals`
|
|
52
52
|
* as `GET /proposals?view=basic` rows, `spaces` name each granted space's
|
|
53
53
|
* template (`synap_grant[].templateSlug` on the token response).
|
|
54
|
+
*
|
|
55
|
+
* An entity's roles ride on its row as `facets`, the shape `GET /entities?
|
|
56
|
+
* includeFacets=true` answers them (`{ facetId, slug, properties, status }`),
|
|
57
|
+
* plus the seed-only space each is stamped in, so a seed row reads like a pod
|
|
58
|
+
* row read with includeFacets. The mock derives `facetSlugs` and the reach rule
|
|
59
|
+
* from them and serves neither field unless asked, as the pod.
|
|
54
60
|
*/
|
|
55
61
|
|
|
62
|
+
/** One role an entity wears (backend `FacetRowAnnotation`, plus the space it is stamped in). */
|
|
63
|
+
interface MockSeedFacet {
|
|
64
|
+
facetId: string;
|
|
65
|
+
/** The role's slug, e.g. `lead`. */
|
|
66
|
+
slug: string;
|
|
67
|
+
/** The facet's overlay properties. Default `{}`. */
|
|
68
|
+
properties?: Record<string, unknown>;
|
|
69
|
+
/** Its lifecycle status. Default null. */
|
|
70
|
+
status?: string | null;
|
|
71
|
+
/**
|
|
72
|
+
* Seed only, never served: the space the role is in, i.e. the space whose
|
|
73
|
+
* members see the facet (stamped there, or a pod-wide facet of a role granted
|
|
74
|
+
* to it). A role in a granted space puts a pod-wide entity in the app's
|
|
75
|
+
* reach; a workspace lens sees the roles of that space only.
|
|
76
|
+
*/
|
|
77
|
+
workspaceId: string;
|
|
78
|
+
/**
|
|
79
|
+
* Seed only, never served: the role is SHARED, i.e. stored pod-wide (null
|
|
80
|
+
* `workspaceId`) and granted to `workspaceId`, which then means "the space it
|
|
81
|
+
* is granted to". Default false = stamped in `workspaceId`. The pod treats the
|
|
82
|
+
* two alike for reach, except `includePodWide=false` on a space lens, which
|
|
83
|
+
* drops a pod-wide row reached only through a shared role.
|
|
84
|
+
*/
|
|
85
|
+
sharedRole?: boolean;
|
|
86
|
+
}
|
|
56
87
|
interface MockSeedEntity {
|
|
57
88
|
id: string;
|
|
58
89
|
profileSlug?: string;
|
|
59
90
|
workspaceId?: string | null;
|
|
60
91
|
title?: string | null;
|
|
61
92
|
properties?: Record<string, unknown>;
|
|
93
|
+
/**
|
|
94
|
+
* The roles (facets) it wears, in the wire shape plus the space each is in
|
|
95
|
+
* (`MockSeedFacet`). SEED INPUT ONLY: the pod sends them only to
|
|
96
|
+
* `includeFacets`, and so does the mock. A pod-wide entity (`workspaceId:
|
|
97
|
+
* null`) with a role in a granted space is readable by the app, as a member
|
|
98
|
+
* of that space sees it. Absent = none.
|
|
99
|
+
*/
|
|
100
|
+
facets?: MockSeedFacet[];
|
|
101
|
+
/**
|
|
102
|
+
* @deprecated Use `facets`; removed in 2.0. Shorthand for `facets` entries
|
|
103
|
+
* with no id and no overlay: `parseMockPodSeed` turns each into a facet (a
|
|
104
|
+
* generated `facetId`, empty properties, a null status). A role stated in
|
|
105
|
+
* both is refused.
|
|
106
|
+
*/
|
|
107
|
+
seedRoles?: Array<{
|
|
108
|
+
profileSlug: string;
|
|
109
|
+
workspaceId: string;
|
|
110
|
+
}>;
|
|
62
111
|
[key: string]: unknown;
|
|
63
112
|
}
|
|
64
113
|
interface MockSeedSession {
|
|
@@ -361,4 +410,4 @@ interface MockPod {
|
|
|
361
410
|
}
|
|
362
411
|
declare function createMockPod(options?: MockPodOptions): MockPod;
|
|
363
412
|
|
|
364
|
-
export { type AuthorizeDecision, MOCK_APP, MOCK_APP_AGENT_ID, MOCK_AUTH_ISSUER, MOCK_CLOUD_URL, MOCK_FAILURE_TOKENS, MOCK_FORM, MOCK_GRANT, MOCK_HTML_SHARE, MOCK_POD_URL, MOCK_RETRY_AFTER_SEC, MOCK_SHARE, MOCK_SITE, MOCK_TOKENS, MOCK_USER, type MockAuthRequest, type MockAuthServer, type MockAuthServerOptions, type MockCloud, type MockCollection, type MockHubFailure, type MockPod, type MockPodOptions, type MockPodSeed, type MockProposal, type MockRequest, type MockSeedDocument, type MockSeedEntity, type MockSeedRelation, type MockSeedSession, type MockSeedSpace, type MockSite, type MockSubmission, createMockAuthServer, createMockCloud, createMockPod, parseMockPodSeed };
|
|
413
|
+
export { type AuthorizeDecision, MOCK_APP, MOCK_APP_AGENT_ID, MOCK_AUTH_ISSUER, MOCK_CLOUD_URL, MOCK_FAILURE_TOKENS, MOCK_FORM, MOCK_GRANT, MOCK_HTML_SHARE, MOCK_POD_URL, MOCK_RETRY_AFTER_SEC, MOCK_SHARE, MOCK_SITE, MOCK_TOKENS, MOCK_USER, type MockAuthRequest, type MockAuthServer, type MockAuthServerOptions, type MockCloud, type MockCollection, type MockHubFailure, type MockPod, type MockPodOptions, type MockPodSeed, type MockProposal, type MockRequest, type MockSeedDocument, type MockSeedEntity, type MockSeedFacet, type MockSeedRelation, type MockSeedSession, type MockSeedSpace, type MockSite, type MockSubmission, createMockAuthServer, createMockCloud, createMockPod, parseMockPodSeed };
|
package/dist/testing.d.ts
CHANGED
|
@@ -51,14 +51,63 @@ declare const MOCK_SITE: MockSite;
|
|
|
51
51
|
* `relations` as graph edges for `GET /entities/:id/connections`, `proposals`
|
|
52
52
|
* as `GET /proposals?view=basic` rows, `spaces` name each granted space's
|
|
53
53
|
* template (`synap_grant[].templateSlug` on the token response).
|
|
54
|
+
*
|
|
55
|
+
* An entity's roles ride on its row as `facets`, the shape `GET /entities?
|
|
56
|
+
* includeFacets=true` answers them (`{ facetId, slug, properties, status }`),
|
|
57
|
+
* plus the seed-only space each is stamped in, so a seed row reads like a pod
|
|
58
|
+
* row read with includeFacets. The mock derives `facetSlugs` and the reach rule
|
|
59
|
+
* from them and serves neither field unless asked, as the pod.
|
|
54
60
|
*/
|
|
55
61
|
|
|
62
|
+
/** One role an entity wears (backend `FacetRowAnnotation`, plus the space it is stamped in). */
|
|
63
|
+
interface MockSeedFacet {
|
|
64
|
+
facetId: string;
|
|
65
|
+
/** The role's slug, e.g. `lead`. */
|
|
66
|
+
slug: string;
|
|
67
|
+
/** The facet's overlay properties. Default `{}`. */
|
|
68
|
+
properties?: Record<string, unknown>;
|
|
69
|
+
/** Its lifecycle status. Default null. */
|
|
70
|
+
status?: string | null;
|
|
71
|
+
/**
|
|
72
|
+
* Seed only, never served: the space the role is in, i.e. the space whose
|
|
73
|
+
* members see the facet (stamped there, or a pod-wide facet of a role granted
|
|
74
|
+
* to it). A role in a granted space puts a pod-wide entity in the app's
|
|
75
|
+
* reach; a workspace lens sees the roles of that space only.
|
|
76
|
+
*/
|
|
77
|
+
workspaceId: string;
|
|
78
|
+
/**
|
|
79
|
+
* Seed only, never served: the role is SHARED, i.e. stored pod-wide (null
|
|
80
|
+
* `workspaceId`) and granted to `workspaceId`, which then means "the space it
|
|
81
|
+
* is granted to". Default false = stamped in `workspaceId`. The pod treats the
|
|
82
|
+
* two alike for reach, except `includePodWide=false` on a space lens, which
|
|
83
|
+
* drops a pod-wide row reached only through a shared role.
|
|
84
|
+
*/
|
|
85
|
+
sharedRole?: boolean;
|
|
86
|
+
}
|
|
56
87
|
interface MockSeedEntity {
|
|
57
88
|
id: string;
|
|
58
89
|
profileSlug?: string;
|
|
59
90
|
workspaceId?: string | null;
|
|
60
91
|
title?: string | null;
|
|
61
92
|
properties?: Record<string, unknown>;
|
|
93
|
+
/**
|
|
94
|
+
* The roles (facets) it wears, in the wire shape plus the space each is in
|
|
95
|
+
* (`MockSeedFacet`). SEED INPUT ONLY: the pod sends them only to
|
|
96
|
+
* `includeFacets`, and so does the mock. A pod-wide entity (`workspaceId:
|
|
97
|
+
* null`) with a role in a granted space is readable by the app, as a member
|
|
98
|
+
* of that space sees it. Absent = none.
|
|
99
|
+
*/
|
|
100
|
+
facets?: MockSeedFacet[];
|
|
101
|
+
/**
|
|
102
|
+
* @deprecated Use `facets`; removed in 2.0. Shorthand for `facets` entries
|
|
103
|
+
* with no id and no overlay: `parseMockPodSeed` turns each into a facet (a
|
|
104
|
+
* generated `facetId`, empty properties, a null status). A role stated in
|
|
105
|
+
* both is refused.
|
|
106
|
+
*/
|
|
107
|
+
seedRoles?: Array<{
|
|
108
|
+
profileSlug: string;
|
|
109
|
+
workspaceId: string;
|
|
110
|
+
}>;
|
|
62
111
|
[key: string]: unknown;
|
|
63
112
|
}
|
|
64
113
|
interface MockSeedSession {
|
|
@@ -361,4 +410,4 @@ interface MockPod {
|
|
|
361
410
|
}
|
|
362
411
|
declare function createMockPod(options?: MockPodOptions): MockPod;
|
|
363
412
|
|
|
364
|
-
export { type AuthorizeDecision, MOCK_APP, MOCK_APP_AGENT_ID, MOCK_AUTH_ISSUER, MOCK_CLOUD_URL, MOCK_FAILURE_TOKENS, MOCK_FORM, MOCK_GRANT, MOCK_HTML_SHARE, MOCK_POD_URL, MOCK_RETRY_AFTER_SEC, MOCK_SHARE, MOCK_SITE, MOCK_TOKENS, MOCK_USER, type MockAuthRequest, type MockAuthServer, type MockAuthServerOptions, type MockCloud, type MockCollection, type MockHubFailure, type MockPod, type MockPodOptions, type MockPodSeed, type MockProposal, type MockRequest, type MockSeedDocument, type MockSeedEntity, type MockSeedRelation, type MockSeedSession, type MockSeedSpace, type MockSite, type MockSubmission, createMockAuthServer, createMockCloud, createMockPod, parseMockPodSeed };
|
|
413
|
+
export { type AuthorizeDecision, MOCK_APP, MOCK_APP_AGENT_ID, MOCK_AUTH_ISSUER, MOCK_CLOUD_URL, MOCK_FAILURE_TOKENS, MOCK_FORM, MOCK_GRANT, MOCK_HTML_SHARE, MOCK_POD_URL, MOCK_RETRY_AFTER_SEC, MOCK_SHARE, MOCK_SITE, MOCK_TOKENS, MOCK_USER, type MockAuthRequest, type MockAuthServer, type MockAuthServerOptions, type MockCloud, type MockCollection, type MockHubFailure, type MockPod, type MockPodOptions, type MockPodSeed, type MockProposal, type MockRequest, type MockSeedDocument, type MockSeedEntity, type MockSeedFacet, type MockSeedRelation, type MockSeedSession, type MockSeedSpace, type MockSite, type MockSubmission, createMockAuthServer, createMockCloud, createMockPod, parseMockPodSeed };
|
package/dist/testing.js
CHANGED
|
@@ -120,6 +120,20 @@ var MOCK_SITE = {
|
|
|
120
120
|
}
|
|
121
121
|
};
|
|
122
122
|
|
|
123
|
+
// src/testing/roles.ts
|
|
124
|
+
function desugarSeedRoles(row, at) {
|
|
125
|
+
if (!Array.isArray(row.seedRoles)) return row;
|
|
126
|
+
const { seedRoles, ...rest } = row;
|
|
127
|
+
const facets = Array.isArray(row.facets) ? row.facets : [];
|
|
128
|
+
const shorthand = seedRoles.filter((r) => typeof r?.profileSlug === "string" && typeof r.workspaceId === "string").map((r, i) => ({ facetId: `seedrole_${String(row.id)}_${i}`, slug: r.profileSlug, workspaceId: r.workspaceId }));
|
|
129
|
+
for (const r of shorthand) {
|
|
130
|
+
if (facets.some((f) => f?.slug === r.slug && f.workspaceId === r.workspaceId)) {
|
|
131
|
+
throw new TypeError(`${at} ("${String(row.id)}"): the role "${r.slug}" in ${r.workspaceId} is in both facets and seedRoles; keep it in facets only`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { ...rest, facets: [...facets, ...shorthand] };
|
|
135
|
+
}
|
|
136
|
+
|
|
123
137
|
// src/testing/auth-server.ts
|
|
124
138
|
var MOCK_AUTH_ISSUER = "https://pod.mock-synap.test";
|
|
125
139
|
var MOCK_GRANT = [
|
|
@@ -153,13 +167,27 @@ function createMockAuthServer(options = {}) {
|
|
|
153
167
|
const writes = [];
|
|
154
168
|
const proposals = [];
|
|
155
169
|
const seed = options.seed ?? {};
|
|
156
|
-
const entities = [...seed.entities ?? [], ...options.entities ?? []]
|
|
170
|
+
const entities = [...seed.entities ?? [], ...options.entities ?? []].map(
|
|
171
|
+
(e, i) => desugarSeedRoles(e, `entities[${i}]`)
|
|
172
|
+
);
|
|
157
173
|
const sessions = [...seed.sessions ?? []];
|
|
158
174
|
const documents = [...seed.documents ?? []];
|
|
159
175
|
const relations = [...seed.relations ?? []];
|
|
160
176
|
const templateOf = new Map((seed.spaces ?? []).filter((sp) => sp.templateSlug).map((sp) => [sp.id, sp.templateSlug]));
|
|
161
177
|
const approvals = [];
|
|
162
178
|
const seededAt = new Date(now()).toISOString();
|
|
179
|
+
const rolesOf = (e) => Array.isArray(e.facets) ? e.facets.filter((f) => typeof f?.slug === "string") : [];
|
|
180
|
+
function readableEntity(e, spaces, only) {
|
|
181
|
+
if (typeof e.workspaceId === "string" && spaces.has(e.workspaceId) && (!only || e.workspaceId === only)) return true;
|
|
182
|
+
return facetsOf(e, spaces, only ?? void 0).some((f) => e.workspaceId == null || !f.sharedRole);
|
|
183
|
+
}
|
|
184
|
+
const facetsOf = (e, spaces, lens) => rolesOf(e).filter((f) => typeof f.workspaceId === "string" && spaces.has(f.workspaceId) && (lens === void 0 || f.workspaceId === lens));
|
|
185
|
+
const facetRow = (f) => ({ facetId: f.facetId, slug: f.slug, properties: f.properties ?? {}, status: f.status ?? null });
|
|
186
|
+
const entityWire = (e) => {
|
|
187
|
+
const { facets: _facets, facetSlugs: _facetSlugs, ...wire } = e;
|
|
188
|
+
return wire;
|
|
189
|
+
};
|
|
190
|
+
const readSpaces = (scope) => new Set(grant.filter((g) => /^entity\.(.+\.)?read$/.test(g.permission) && scope.includes(g.permission)).map((g) => g.workspaceId));
|
|
163
191
|
for (const p of seed.proposals ?? []) {
|
|
164
192
|
proposals.push({
|
|
165
193
|
proposalType: "create",
|
|
@@ -307,11 +335,12 @@ function createMockAuthServer(options = {}) {
|
|
|
307
335
|
triage: { pending: false, acceptedAt: null, acceptedBy: null },
|
|
308
336
|
...s
|
|
309
337
|
});
|
|
310
|
-
function readSeeded(path, query) {
|
|
338
|
+
function readSeeded(path, query, scope) {
|
|
339
|
+
const spaces = readSpaces(scope);
|
|
311
340
|
const conn = /^\/entities\/([^/]+)\/connections$/.exec(path);
|
|
312
341
|
if (conn) {
|
|
313
342
|
const id = decodeURIComponent(conn[1]);
|
|
314
|
-
const ws = query.get("workspaceId");
|
|
343
|
+
const ws = query.get("workspaceId") ?? void 0;
|
|
315
344
|
const limit = Math.min(Math.max(Number(query.get("limit") ?? 50) || 50, 1), 200);
|
|
316
345
|
const inLens = (e) => !ws || e.workspaceId === ws || e.workspaceId == null;
|
|
317
346
|
const connections = [];
|
|
@@ -320,10 +349,15 @@ function createMockAuthServer(options = {}) {
|
|
|
320
349
|
const outgoing = r.sourceEntityId === id;
|
|
321
350
|
const otherId = outgoing ? r.targetEntityId : r.sourceEntityId;
|
|
322
351
|
const other = entities.find((e) => e.id === otherId);
|
|
323
|
-
if (!other || !inLens(other)) continue;
|
|
352
|
+
if (!other || !inLens(other) || !readableEntity(other, spaces)) continue;
|
|
324
353
|
connections.push({
|
|
325
354
|
entityId: otherId,
|
|
326
|
-
|
|
355
|
+
// relations.ts: the raw row plus its role slugs under the read's facet lens (a 1.5.0 seed's own facetSlugs otherwise).
|
|
356
|
+
entity: {
|
|
357
|
+
...entityWire(other),
|
|
358
|
+
type: other.profileSlug ?? other.type ?? null,
|
|
359
|
+
facetSlugs: rolesOf(other).length > 0 ? facetsOf(other, spaces, ws).map((f) => f.slug) : other.facetSlugs ?? []
|
|
360
|
+
},
|
|
327
361
|
label: r.type,
|
|
328
362
|
direction: outgoing ? "outgoing" : "incoming",
|
|
329
363
|
source: "graph",
|
|
@@ -356,7 +390,39 @@ function createMockAuthServer(options = {}) {
|
|
|
356
390
|
const entity = /^\/entities\/([^/]+)$/.exec(path);
|
|
357
391
|
if (entity) {
|
|
358
392
|
const found = entities.find((e) => e.id === decodeURIComponent(entity[1]));
|
|
359
|
-
return found ? json(200, found) : new Response(null, { status: 404 });
|
|
393
|
+
return found && readableEntity(found, spaces) ? json(200, entityWire(found)) : new Response(null, { status: 404 });
|
|
394
|
+
}
|
|
395
|
+
const roles = /^\/entities\/([^/]+)\/facets$/.exec(path);
|
|
396
|
+
if (roles) {
|
|
397
|
+
const found = entities.find((e) => e.id === decodeURIComponent(roles[1]));
|
|
398
|
+
if (!found || !readableEntity(found, spaces)) return new Response(null, { status: 404 });
|
|
399
|
+
return json(200, {
|
|
400
|
+
facets: facetsOf(found, spaces).map((f) => ({
|
|
401
|
+
facet: {
|
|
402
|
+
id: f.facetId,
|
|
403
|
+
entityId: found.id,
|
|
404
|
+
profileId: `profile_${f.slug}`,
|
|
405
|
+
userId: found.userId ?? user.id,
|
|
406
|
+
// A stamped facet answers its space; a shared role's facet is stored pod-wide.
|
|
407
|
+
workspaceId: f.sharedRole ? null : f.workspaceId,
|
|
408
|
+
contextEntityId: null,
|
|
409
|
+
status: f.status ?? null,
|
|
410
|
+
statusCategory: null,
|
|
411
|
+
properties: f.properties ?? {},
|
|
412
|
+
metadata: {},
|
|
413
|
+
createdByKind: null,
|
|
414
|
+
createdByUserId: null,
|
|
415
|
+
agentUserId: null,
|
|
416
|
+
sourceProposalId: null,
|
|
417
|
+
correlationId: null,
|
|
418
|
+
createdAt: seededAt,
|
|
419
|
+
updatedAt: seededAt,
|
|
420
|
+
deletedAt: null
|
|
421
|
+
},
|
|
422
|
+
profile: { id: `profile_${f.slug}`, slug: f.slug, profileKind: "role" },
|
|
423
|
+
effectiveProperties: []
|
|
424
|
+
}))
|
|
425
|
+
});
|
|
360
426
|
}
|
|
361
427
|
const doc = /^\/documents\/([^/]+)$/.exec(path);
|
|
362
428
|
if (doc) {
|
|
@@ -422,12 +488,27 @@ function createMockAuthServer(options = {}) {
|
|
|
422
488
|
}
|
|
423
489
|
if (method === "GET" && path === "/users/me") return json(200, user);
|
|
424
490
|
if (method === "GET" && path === "/entities") {
|
|
425
|
-
const ws = query.get("workspaceId");
|
|
491
|
+
const ws = query.get("workspaceId") || void 0;
|
|
426
492
|
const kind = query.get("profileSlug");
|
|
493
|
+
const role = query.get("facetSlug");
|
|
494
|
+
const podWide = query.get("includePodWide") !== "false";
|
|
495
|
+
const withFacets = query.get("includeFacets") === "true";
|
|
427
496
|
const offset = Number(query.get("offset") ?? 0) || 0;
|
|
428
497
|
const limit = Number(query.get("limit") ?? 50) || 50;
|
|
429
|
-
const
|
|
430
|
-
|
|
498
|
+
const spaces = readSpaces(row.scope);
|
|
499
|
+
const rows = entities.filter((e) => {
|
|
500
|
+
if (!readableEntity(e, spaces, ws)) return false;
|
|
501
|
+
if (ws && !podWide && e.workspaceId !== ws && !facetsOf(e, spaces, ws).some((f) => !f.sharedRole)) return false;
|
|
502
|
+
const slugs = facetsOf(e, spaces, ws).map((f) => f.slug);
|
|
503
|
+
return (!kind || e.profileSlug === kind || slugs.includes(kind)) && (!role || slugs.includes(role));
|
|
504
|
+
});
|
|
505
|
+
return json(
|
|
506
|
+
200,
|
|
507
|
+
rows.slice(offset, offset + limit).map((e) => {
|
|
508
|
+
const facets = withFacets ? facetsOf(e, spaces, ws).map(facetRow) : [];
|
|
509
|
+
return facets.length > 0 ? { ...entityWire(e), facetSlugs: facets.map((f) => f.slug), facets } : entityWire(e);
|
|
510
|
+
})
|
|
511
|
+
);
|
|
431
512
|
}
|
|
432
513
|
if (method === "GET" && path === "/proposals") {
|
|
433
514
|
const readable = new Set(grant.filter((g) => g.permission === "proposal.read" && row.scope.includes(g.permission)).map((g) => g.workspaceId));
|
|
@@ -442,7 +523,7 @@ function createMockAuthServer(options = {}) {
|
|
|
442
523
|
return json(200, { proposals: page, limit, offset, total: rows.length, hasMore: offset + page.length < rows.length });
|
|
443
524
|
}
|
|
444
525
|
if (method === "GET") {
|
|
445
|
-
const read = readSeeded(path, query);
|
|
526
|
+
const read = readSeeded(path, query, row.scope);
|
|
446
527
|
if (read) return read;
|
|
447
528
|
}
|
|
448
529
|
const approve = method === "POST" ? /^\/proposals\/([^/]+)\/approve$/.exec(path) : null;
|
|
@@ -460,6 +541,11 @@ function createMockAuthServer(options = {}) {
|
|
|
460
541
|
approvals.push(p.id);
|
|
461
542
|
return json(200, { success: true, proposalId: p.id, proposal: { ...p } });
|
|
462
543
|
}
|
|
544
|
+
const patched = method === "PATCH" ? /^\/entities\/([^/]+)$/.exec(path) : null;
|
|
545
|
+
if (patched) {
|
|
546
|
+
const target = entities.find((e) => e.id === decodeURIComponent(patched[1]));
|
|
547
|
+
if (target && target.workspaceId == null) return json(403, { error: "grant does not allow" });
|
|
548
|
+
}
|
|
463
549
|
if (method !== "GET") {
|
|
464
550
|
writes.push({ method, path, body });
|
|
465
551
|
const fields = body && typeof body === "object" ? body : {};
|
|
@@ -637,9 +723,36 @@ function parseMockPodSeed(input) {
|
|
|
637
723
|
for (const field of REQUIRED[key]) {
|
|
638
724
|
if (typeof row[field] !== "string") throw new TypeError(`seed.${key}[${i}].${field}: expected a string`);
|
|
639
725
|
}
|
|
726
|
+
if (key === "entities") checkFacets(row.facets, `seed.entities[${i}].facets`);
|
|
640
727
|
});
|
|
641
728
|
}
|
|
642
|
-
|
|
729
|
+
obj.entities?.forEach((row, i) => {
|
|
730
|
+
const roles = row.seedRoles;
|
|
731
|
+
if (roles === void 0) return;
|
|
732
|
+
if (!Array.isArray(roles)) throw new TypeError(`seed.entities[${i}].seedRoles: expected an array`);
|
|
733
|
+
roles.forEach((r, j) => {
|
|
734
|
+
for (const field of ["profileSlug", "workspaceId"]) {
|
|
735
|
+
if (!r || typeof r !== "object" || typeof r[field] !== "string") {
|
|
736
|
+
throw new TypeError(`seed.entities[${i}].seedRoles[${j}].${field}: expected a string`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
const out = JSON.parse(JSON.stringify(obj));
|
|
742
|
+
if (out.entities) out.entities = out.entities.map((row, i) => desugarSeedRoles(row, `seed.entities[${i}]`));
|
|
743
|
+
return out;
|
|
744
|
+
}
|
|
745
|
+
function checkFacets(facets, at) {
|
|
746
|
+
if (facets === void 0) return;
|
|
747
|
+
if (!Array.isArray(facets)) throw new TypeError(`${at}: expected an array`);
|
|
748
|
+
facets.forEach((f, j) => {
|
|
749
|
+
if (!f || typeof f !== "object" || Array.isArray(f)) throw new TypeError(`${at}[${j}]: expected an object`);
|
|
750
|
+
for (const field of ["facetId", "slug", "workspaceId"]) {
|
|
751
|
+
if (typeof f[field] !== "string") throw new TypeError(`${at}[${j}].${field}: expected a string`);
|
|
752
|
+
}
|
|
753
|
+
const shared = f.sharedRole;
|
|
754
|
+
if (shared !== void 0 && typeof shared !== "boolean") throw new TypeError(`${at}[${j}].sharedRole: expected a boolean`);
|
|
755
|
+
});
|
|
643
756
|
}
|
|
644
757
|
|
|
645
758
|
// src/testing/cloud.ts
|