rastack 0.0.57 → 0.0.59

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 (86) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +190 -0
  3. package/admin/index.tsx +19 -13
  4. package/admin/ui/BootScreen.tsx +201 -0
  5. package/dist/admin.js +156 -18
  6. package/dist/deploy/ci.js +2 -2
  7. package/dist/deploy/io.js +2 -2
  8. package/dist/dev/harness.d.ts +1 -1
  9. package/dist/dev/harness.js +21 -5
  10. package/dist/external/cache.d.ts +28 -0
  11. package/dist/external/cache.js +35 -0
  12. package/dist/external/envelope.d.ts +35 -0
  13. package/dist/external/envelope.js +48 -0
  14. package/dist/external/graphql.d.ts +25 -0
  15. package/dist/external/graphql.js +78 -0
  16. package/dist/external/handler.d.ts +52 -0
  17. package/dist/external/handler.js +139 -0
  18. package/dist/external/index.d.ts +19 -0
  19. package/dist/external/index.js +38 -0
  20. package/dist/external/lambda.d.ts +49 -0
  21. package/dist/external/lambda.js +86 -0
  22. package/dist/external/rest.d.ts +17 -0
  23. package/dist/external/rest.js +46 -0
  24. package/dist/external/route.d.ts +12 -0
  25. package/dist/external/route.js +33 -0
  26. package/dist/external/service.d.ts +50 -0
  27. package/dist/external/service.js +59 -0
  28. package/dist/external/types.d.ts +107 -0
  29. package/dist/external/types.js +22 -0
  30. package/dist/external/util.d.ts +16 -0
  31. package/dist/external/util.js +58 -0
  32. package/dist/rastack-dev.js +200 -4
  33. package/dist/rastack-wasm-build.d.ts +1 -1
  34. package/dist/rastack-wasm-build.js +6 -6
  35. package/dist/wasm/rastack_wasm_bg.wasm +0 -0
  36. package/external.ts +10 -0
  37. package/hooks/query/delete.ts +12 -11
  38. package/hooks/query/fetch.ts +6 -8
  39. package/hooks/query/interfaces.ts +1 -4
  40. package/hooks/query/list.ts +16 -9
  41. package/hooks/query/update.ts +8 -6
  42. package/package.json +25 -2
  43. package/scripts/copy-templates.js +1 -1
  44. package/scripts/copy-wasm.js +1 -1
  45. package/scripts/dev-install.mjs +63 -0
  46. package/src/deploy/ci.ts +2 -2
  47. package/src/deploy/io.ts +2 -2
  48. package/src/dev/harness.ts +21 -5
  49. package/src/external/cache.ts +53 -0
  50. package/src/external/envelope.ts +78 -0
  51. package/src/external/graphql.ts +98 -0
  52. package/src/external/handler.ts +207 -0
  53. package/src/external/index.ts +25 -0
  54. package/src/external/lambda.ts +108 -0
  55. package/src/external/rest.ts +60 -0
  56. package/src/external/route.ts +33 -0
  57. package/src/external/service.ts +86 -0
  58. package/src/external/types.ts +121 -0
  59. package/src/external/util.ts +57 -0
  60. package/src/rastack-dev.ts +233 -6
  61. package/src/rastack-wasm-build.ts +6 -6
  62. package/jest.config.cjs +0 -12
  63. package/test/auth.spec.ts +0 -207
  64. package/test/cache.spec.ts +0 -263
  65. package/test/cognito.spec.ts +0 -281
  66. package/test/compile.spec.ts +0 -240
  67. package/test/components.spec.ts +0 -524
  68. package/test/csv-schema.spec.ts +0 -143
  69. package/test/deploy.spec.ts +0 -571
  70. package/test/dev.spec.ts +0 -414
  71. package/test/entities.spec.ts +0 -597
  72. package/test/import.spec.ts +0 -241
  73. package/test/plugin.spec.ts +0 -315
  74. package/test/runtime-manifest.spec.ts +0 -309
  75. package/test/schema-entities.spec.ts +0 -688
  76. package/test/tokens.spec.ts +0 -302
  77. package/test/transitions.spec.ts +0 -372
  78. package/test/typed-hooks.spec.ts +0 -412
  79. package/test/update.spec.ts +0 -152
  80. package/test/validate.spec.ts +0 -319
  81. package/tsconfig.json +0 -27
  82. package/wasm/package.json +0 -4
  83. package/wasm/rastack_wasm.d.ts +0 -100
  84. package/wasm/rastack_wasm.js +0 -745
  85. package/wasm/rastack_wasm_bg.wasm +0 -0
  86. package/wasm/rastack_wasm_bg.wasm.d.ts +0 -28
@@ -1,281 +0,0 @@
1
- /**
2
- * The Cognito auth core (`rastack/auth`): PKCE (S256, against the RFC 7636 test
3
- * vector), Hosted UI URL builders, the `/oauth2/token` request body, JWT
4
- * decoding, session shaping + expiry, redirect parsing, and the server-less
5
- * local session.
6
- */
7
-
8
- import {
9
- authorizeUrl,
10
- base64UrlDecode,
11
- base64UrlDecodeToBytes,
12
- base64UrlEncode,
13
- codeChallengeFromVerifier,
14
- createPkcePair,
15
- decodeJwt,
16
- hostedUiOrigin,
17
- isSessionValid,
18
- issuerUrl,
19
- localSession,
20
- logoutUrl,
21
- parseRedirect,
22
- randomCodeVerifier,
23
- sessionFromTokens,
24
- tokenEndpoint,
25
- tokenRequestBody,
26
- userFromClaims,
27
- type CognitoAuthConfig,
28
- type CognitoTokenResponse,
29
- } from "../src/auth/cognito";
30
- import { cognitoToRastackAuth } from "../auth/provider";
31
-
32
- const CONFIG: CognitoAuthConfig = {
33
- domain: "myapp.auth.us-east-1.amazoncognito.com",
34
- clientId: "abc123clientid",
35
- redirectUri: "https://app.example.com/callback",
36
- region: "us-east-1",
37
- userPoolId: "us-east-1_Pool",
38
- };
39
-
40
- /** Build a signed-shaped JWT (unsigned) carrying the given payload. */
41
- function makeJwt(payload: Record<string, unknown>): string {
42
- const seg = (v: unknown) =>
43
- base64UrlEncode(new TextEncoder().encode(JSON.stringify(v)));
44
- return `${seg({ alg: "RS256", typ: "JWT" })}.${seg(payload)}.sig`;
45
- }
46
-
47
- describe("base64url", () => {
48
- it("round-trips bytes and strips padding", () => {
49
- const bytes = new Uint8Array([0, 1, 2, 250, 251, 252, 253]);
50
- const encoded = base64UrlEncode(bytes);
51
- expect(encoded).not.toMatch(/[+/=]/);
52
- expect([...base64UrlDecodeToBytes(encoded)]).toEqual([...bytes]);
53
- });
54
-
55
- it("decodes a UTF-8 payload", () => {
56
- const encoded = base64UrlEncode(new TextEncoder().encode('{"é":"✓"}'));
57
- expect(base64UrlDecode(encoded)).toBe('{"é":"✓"}');
58
- });
59
- });
60
-
61
- describe("PKCE", () => {
62
- it("derives the S256 challenge from the RFC 7636 test vector", async () => {
63
- const verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
64
- const challenge = await codeChallengeFromVerifier(verifier);
65
- expect(challenge).toBe("E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM");
66
- });
67
-
68
- it("generates a url-safe verifier of the expected length", () => {
69
- const verifier = randomCodeVerifier(new Uint8Array(32).fill(7));
70
- expect(verifier).not.toMatch(/[+/=]/);
71
- expect(verifier.length).toBeGreaterThanOrEqual(43);
72
- });
73
-
74
- it("creates a matching verifier/challenge pair", async () => {
75
- const bytes = new Uint8Array(32).map((_, i) => i);
76
- const { verifier, challenge } = await createPkcePair(bytes);
77
- expect(verifier).toBe(randomCodeVerifier(bytes));
78
- expect(challenge).toBe(await codeChallengeFromVerifier(verifier));
79
- });
80
- });
81
-
82
- describe("Hosted UI URLs", () => {
83
- it("builds the authorize URL with PKCE + default scopes", () => {
84
- const url = new URL(
85
- authorizeUrl(CONFIG, { state: "st8", codeChallenge: "chal" }),
86
- );
87
- expect(url.origin + url.pathname).toBe(
88
- "https://myapp.auth.us-east-1.amazoncognito.com/oauth2/authorize",
89
- );
90
- expect(url.searchParams.get("response_type")).toBe("code");
91
- expect(url.searchParams.get("client_id")).toBe("abc123clientid");
92
- expect(url.searchParams.get("redirect_uri")).toBe(
93
- "https://app.example.com/callback",
94
- );
95
- expect(url.searchParams.get("scope")).toBe("openid email profile");
96
- expect(url.searchParams.get("code_challenge")).toBe("chal");
97
- expect(url.searchParams.get("code_challenge_method")).toBe("S256");
98
- expect(url.searchParams.get("state")).toBe("st8");
99
- });
100
-
101
- it("honours custom scopes and strips a scheme from the domain", () => {
102
- const url = new URL(
103
- authorizeUrl(
104
- { ...CONFIG, domain: "https://auth.example.com/", scopes: ["openid"] },
105
- { state: "s", codeChallenge: "c" },
106
- ),
107
- );
108
- expect(url.origin).toBe("https://auth.example.com");
109
- expect(url.searchParams.get("scope")).toBe("openid");
110
- });
111
-
112
- it("builds the logout URL, defaulting logout_uri to redirect_uri", () => {
113
- const url = new URL(logoutUrl(CONFIG));
114
- expect(url.pathname).toBe("/logout");
115
- expect(url.searchParams.get("logout_uri")).toBe(
116
- "https://app.example.com/callback",
117
- );
118
- });
119
-
120
- it("derives the origin, token endpoint and issuer", () => {
121
- expect(hostedUiOrigin(CONFIG)).toBe(
122
- "https://myapp.auth.us-east-1.amazoncognito.com",
123
- );
124
- expect(tokenEndpoint(CONFIG)).toBe(
125
- "https://myapp.auth.us-east-1.amazoncognito.com/oauth2/token",
126
- );
127
- expect(issuerUrl(CONFIG)).toBe(
128
- "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Pool",
129
- );
130
- });
131
- });
132
-
133
- describe("token request body", () => {
134
- it("encodes an authorization_code grant with the verifier", () => {
135
- const body = new URLSearchParams(
136
- tokenRequestBody(CONFIG, {
137
- grantType: "authorization_code",
138
- code: "the-code",
139
- codeVerifier: "the-verifier",
140
- }),
141
- );
142
- expect(body.get("grant_type")).toBe("authorization_code");
143
- expect(body.get("client_id")).toBe("abc123clientid");
144
- expect(body.get("code")).toBe("the-code");
145
- expect(body.get("redirect_uri")).toBe("https://app.example.com/callback");
146
- expect(body.get("code_verifier")).toBe("the-verifier");
147
- });
148
-
149
- it("encodes a refresh_token grant", () => {
150
- const body = new URLSearchParams(
151
- tokenRequestBody(CONFIG, {
152
- grantType: "refresh_token",
153
- refreshToken: "rt",
154
- }),
155
- );
156
- expect(body.get("grant_type")).toBe("refresh_token");
157
- expect(body.get("refresh_token")).toBe("rt");
158
- expect(body.has("code")).toBe(false);
159
- });
160
- });
161
-
162
- describe("JWT + claims", () => {
163
- it("decodes a JWT payload without verifying", () => {
164
- const token = makeJwt({ sub: "user-1", email: "a@b.co" });
165
- expect(decodeJwt(token)).toMatchObject({ sub: "user-1", email: "a@b.co" });
166
- });
167
-
168
- it("throws on a malformed token", () => {
169
- expect(() => decodeJwt("not-a-jwt")).toThrow(/Malformed JWT/);
170
- });
171
-
172
- it("projects cognito:groups into the user (empty when absent)", () => {
173
- expect(
174
- userFromClaims({ sub: "s", "cognito:groups": ["admin", "ops"] }),
175
- ).toEqual({ sub: "s", email: undefined, username: "s", groups: ["admin", "ops"] });
176
- expect(userFromClaims({ sub: "s", email: "e@x.co" }).groups).toEqual([]);
177
- });
178
- });
179
-
180
- describe("sessions", () => {
181
- const now = 1_000_000_000_000;
182
-
183
- it("builds a session from tokens, using the token's own exp", () => {
184
- const idToken = makeJwt({
185
- sub: "u9",
186
- exp: Math.floor(now / 1000) + 3600,
187
- "cognito:groups": ["pilots"],
188
- email: "pilot@sky.co",
189
- });
190
- const tokens: CognitoTokenResponse = {
191
- id_token: idToken,
192
- access_token: makeJwt({ sub: "u9" }),
193
- refresh_token: "rt",
194
- expires_in: 3600,
195
- };
196
- const session = sessionFromTokens(tokens, now);
197
- expect(session.user).toMatchObject({ sub: "u9", groups: ["pilots"] });
198
- expect(session.expiresAt).toBe((Math.floor(now / 1000) + 3600) * 1000);
199
- expect(session.refreshToken).toBe("rt");
200
- });
201
-
202
- it("validates against expiry with skew", () => {
203
- const session = sessionFromTokens(
204
- {
205
- id_token: makeJwt({ sub: "u", exp: Math.floor(now / 1000) + 3600 }),
206
- access_token: "a",
207
- },
208
- now,
209
- );
210
- expect(isSessionValid(session, now)).toBe(true);
211
- expect(isSessionValid(session, session.expiresAt - 10_000, 30_000)).toBe(false);
212
- expect(isSessionValid(null, now)).toBe(false);
213
- });
214
- });
215
-
216
- describe("redirect parsing", () => {
217
- it("parses code + state from a search string or full URL", () => {
218
- expect(parseRedirect("?code=abc&state=xyz")).toMatchObject({
219
- code: "abc",
220
- state: "xyz",
221
- });
222
- expect(
223
- parseRedirect("https://app.example.com/callback?code=c1&state=s1"),
224
- ).toMatchObject({ code: "c1", state: "s1" });
225
- });
226
-
227
- it("surfaces an OAuth error", () => {
228
- const r = parseRedirect("?error=access_denied&error_description=nope");
229
- expect(r.error).toBe("access_denied");
230
- expect(r.errorDescription).toBe("nope");
231
- expect(r.code).toBeUndefined();
232
- });
233
- });
234
-
235
- describe("local (server-less) session", () => {
236
- const now = 2_000_000_000_000;
237
-
238
- it("mints a decodable, valid session with groups", () => {
239
- const session = localSession(
240
- { sub: "ops-lead", email: "lead@sky.co", groups: ["admins"] },
241
- now,
242
- );
243
- expect(session.user).toMatchObject({
244
- sub: "ops-lead",
245
- email: "lead@sky.co",
246
- groups: ["admins"],
247
- });
248
- expect(isSessionValid(session, now)).toBe(true);
249
- // The minted token is a real-shaped JWT the same decoder reads back.
250
- expect(decodeJwt(session.idToken)).toMatchObject({ sub: "ops-lead" });
251
- });
252
-
253
- it("defaults the subject and carries no groups", () => {
254
- const session = localSession({}, now);
255
- expect(session.user.sub).toBe("local-dev");
256
- expect(session.user.groups).toEqual([]);
257
- });
258
- });
259
-
260
- describe("bridge to the authorization half (RastackAuth)", () => {
261
- const now = 2_000_000_000_000;
262
-
263
- it("maps a signed-in session's claims into a RastackAuth", () => {
264
- const session = localSession({ sub: "u1", groups: ["admins"] }, now);
265
- const auth = cognitoToRastackAuth({
266
- isAuthenticated: true,
267
- claims: session.claims,
268
- });
269
- expect(auth).toEqual({
270
- authenticated: true,
271
- sub: "u1",
272
- groups: ["admins"],
273
- });
274
- });
275
-
276
- it("maps a signed-out state to an anonymous identity", () => {
277
- expect(cognitoToRastackAuth({ isAuthenticated: false, claims: null })).toEqual(
278
- { authenticated: false, groups: [] },
279
- );
280
- });
281
- });
@@ -1,240 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
2
- import { join } from "path";
3
- import {
4
- analyzeResources,
5
- buildOpenApi,
6
- collisions,
7
- compile,
8
- ResourceModel,
9
- } from "../src/compile";
10
- import { generateFiles, generateFolders } from "../src/schema-entities";
11
- import { removeNonModelPaths } from "../src/schema/remove-non-model-paths";
12
-
13
- /**
14
- * The compiler's fixtures are real `.ts` files that import the DSL by its
15
- * package name (`rastack/define`); `createProgram` maps that to the in-tree
16
- * source, so the type checker resolves the `Ref`/`Resource` brands exactly as it
17
- * would in a consumer project.
18
- */
19
- const workRoot = join(__dirname, "compile-fixtures");
20
- const resourcesDir = join(workRoot, "resources");
21
- const outDir = join(workRoot, "out");
22
-
23
- const AIRPORTS_TS = `
24
- import { resource, s } from "rastack/define";
25
-
26
- export const Airport = resource("airports", "airport", {
27
- code: s.string({ maxLength: 3, unique: true }),
28
- name: s.string({ maxLength: 120 }),
29
- }, { search: ["code", "name"], admin: { listDisplay: ["code", "name"] } });
30
-
31
- export const Terminal = resource("airports", "terminal", {
32
- airport: s.ref(() => Airport),
33
- label: s.string({ maxLength: 20 }),
34
- });
35
-
36
- export const Flight = resource("airports", "flight", {
37
- number: s.string({ maxLength: 8 }),
38
- status: s.string({ maxLength: 20, default: "scheduled" }),
39
- }, { sync: { mode: "local", scope: "owner_id={user.id}", confirmed: ["update"] } });
40
-
41
- export const Gate = resource("airports", "gate", {
42
- number: s.string({ maxLength: 5 }),
43
- }, { sync: { mode: "local", load: "lazy" } });
44
-
45
- export const Booking = resource("airports", "booking", {
46
- reference: s.string({ maxLength: 12 }),
47
- }, { sync: { mode: "local", clientIds: true } });
48
-
49
- export const Itinerary = resource("airports", "itinerary", {
50
- owner: s.string({ maxLength: 64 }),
51
- notes: s.string({ maxLength: 500 }),
52
- }, {
53
- permission: "authenticated",
54
- access: { ownerField: "owner", scope: "owner", adminGroups: ["staff"] },
55
- });
56
- `;
57
-
58
- function findResource(
59
- resources: ResourceModel[],
60
- app: string,
61
- model: string,
62
- ): ResourceModel {
63
- const found = resources.find((r) => r.app === app && r.model === model);
64
- if (!found) throw new Error(`missing ${app}.${model}`);
65
- return found;
66
- }
67
-
68
- describe("rastack compile — resource analysis & FK inference", () => {
69
- beforeAll(() => {
70
- mkdirSync(resourcesDir, { recursive: true });
71
- writeFileSync(join(resourcesDir, "airports.ts"), AIRPORTS_TS);
72
- });
73
-
74
- afterAll(() => {
75
- rmSync(workRoot, { recursive: true, force: true });
76
- });
77
-
78
- it("infers a foreign key from the field type", () => {
79
- const resources = analyzeResources(resourcesDir);
80
- const terminal = findResource(resources, "airports", "terminal");
81
-
82
- const airportField = terminal.fields.find((f) => f.name === "airport");
83
- expect(airportField?.type).toBe("fk");
84
- expect(airportField?.relation).toEqual({
85
- app: "airports",
86
- model: "airport",
87
- });
88
-
89
- expect(terminal.relations).toEqual([
90
- { field: "airport", target: "airports.airport", kind: "fk" },
91
- ]);
92
- });
93
-
94
- it("maps scalar fields and their constraints", () => {
95
- const resources = analyzeResources(resourcesDir);
96
- const airport = findResource(resources, "airports", "airport");
97
-
98
- const code = airport.fields.find((f) => f.name === "code");
99
- expect(code).toMatchObject({ type: "string", maxLength: 3, unique: true });
100
-
101
- const name = airport.fields.find((f) => f.name === "name");
102
- expect(name).toMatchObject({ type: "string", maxLength: 120 });
103
-
104
- expect(airport.search).toEqual(["code", "name"]);
105
- expect(airport.admin).toEqual({ listDisplay: ["code", "name"] });
106
- });
107
-
108
- it("captures sync config (local / lazy / client-ids / confirmed)", () => {
109
- const resources = analyzeResources(resourcesDir);
110
-
111
- expect(findResource(resources, "airports", "flight").sync).toMatchObject({
112
- mode: "local",
113
- scope: "owner_id={user.id}",
114
- confirmed: ["update"],
115
- });
116
- expect(findResource(resources, "airports", "gate").sync).toMatchObject({
117
- mode: "local",
118
- load: "lazy",
119
- });
120
- expect(findResource(resources, "airports", "booking").sync).toMatchObject({
121
- mode: "local",
122
- clientIds: true,
123
- });
124
- });
125
-
126
- it("captures row-level access config (ownerField / scope / adminGroups)", () => {
127
- const resources = analyzeResources(resourcesDir);
128
- const itinerary = findResource(resources, "airports", "itinerary");
129
-
130
- expect(itinerary.permission).toBe("authenticated");
131
- expect(itinerary.access).toEqual({
132
- ownerField: "owner",
133
- scope: "owner",
134
- adminGroups: ["staff"],
135
- });
136
-
137
- // Resources without an access block stay unstamped.
138
- expect(
139
- findResource(resources, "airports", "airport").access,
140
- ).toBeUndefined();
141
- });
142
-
143
- it("reports no collisions for a valid graph", () => {
144
- const resources = analyzeResources(resourcesDir);
145
- expect(collisions(resources)).toEqual([]);
146
- });
147
- });
148
-
149
- describe("rastack compile — OpenAPI emission", () => {
150
- beforeAll(() => {
151
- mkdirSync(resourcesDir, { recursive: true });
152
- writeFileSync(join(resourcesDir, "airports.ts"), AIRPORTS_TS);
153
- });
154
-
155
- afterAll(() => {
156
- rmSync(workRoot, { recursive: true, force: true });
157
- });
158
-
159
- it("writes schema.rastack.json and openapi.json", () => {
160
- const result = compile(resourcesDir, outDir);
161
- expect(existsSync(join(outDir, "schema.rastack.json"))).toBe(true);
162
- expect(existsSync(join(outDir, "openapi.json"))).toBe(true);
163
- expect(result.manifest.resources.length).toBe(6);
164
- });
165
-
166
- it("emits the /api/{app}/v1/{model}/ contract with FK + sync extensions", () => {
167
- const resources = analyzeResources(resourcesDir);
168
- const doc = buildOpenApi(resources);
169
-
170
- // URL-segment contract the codegen keys off.
171
- expect(doc.paths["/api/airports/v1/terminal/"]).toBeDefined();
172
- expect(doc.paths["/api/airports/v1/terminal/{id}/"]).toBeDefined();
173
-
174
- // FK field carries the new relation extension.
175
- const terminalSchema = doc.components.schemas.Terminal;
176
- expect(terminalSchema.properties.airport["x-rastack-relation"]).toEqual({
177
- app: "airports",
178
- model: "airport",
179
- });
180
-
181
- // Local-mode sync extensions, confirmed on update.
182
- const flightList = doc.paths["/api/airports/v1/flight/"];
183
- expect(flightList.get["x-rastack-sync"]).toBe("local");
184
- expect(
185
- doc.paths["/api/airports/v1/flight/{id}/"].put["x-rastack-sync"],
186
- ).toBe("confirmed");
187
- expect(doc.paths["/api/airports/v1/gate/"].get["x-rastack-sync-load"]).toBe(
188
- "lazy",
189
- );
190
- expect(
191
- doc.paths["/api/airports/v1/booking/"].post["x-rastack-sync-client-ids"],
192
- ).toBe(true);
193
-
194
- // Access-controlled resource carries the full policy on every op — the
195
- // permission gate plus the access block `rastack/auth` mirrors in the UI.
196
- const itineraryList = doc.paths["/api/airports/v1/itinerary/"];
197
- expect(itineraryList.get["x-rastack-permission"]).toBe("authenticated");
198
- expect(itineraryList.get["x-rastack-access"]).toEqual({
199
- ownerField: "owner",
200
- scope: "owner",
201
- adminGroups: ["staff"],
202
- });
203
- expect(
204
- doc.paths["/api/airports/v1/itinerary/{id}/"].delete["x-rastack-access"],
205
- ).toEqual({ ownerField: "owner", scope: "owner", adminGroups: ["staff"] });
206
- // Unscoped resources stay unstamped.
207
- expect(
208
- doc.paths["/api/airports/v1/airport/"].get["x-rastack-access"],
209
- ).toBeUndefined();
210
- expect(
211
- doc.paths["/api/airports/v1/airport/"].get["x-rastack-permission"],
212
- ).toBeUndefined();
213
- });
214
-
215
- it("round-trips through the existing hooks codegen", async () => {
216
- const resources = analyzeResources(resourcesDir);
217
- const doc = buildOpenApi(resources);
218
- const paths = removeNonModelPaths(doc.paths);
219
-
220
- const entitiesPath = join(workRoot, "entities");
221
- mkdirSync(entitiesPath, { recursive: true });
222
- await generateFolders(entitiesPath, paths);
223
- await generateFiles(entitiesPath, paths);
224
-
225
- // Non-sync entity → plain async hook.
226
- const airportApi = readFileSync(
227
- join(entitiesPath, "airports", "airport", "api.ts"),
228
- "utf-8",
229
- );
230
- expect(airportApi).toContain("export function useListAirport(");
231
-
232
- // Sync entity → store-backed hooks + a defineEntity descriptor.
233
- const flightApi = readFileSync(
234
- join(entitiesPath, "airports", "flight", "api.ts"),
235
- "utf-8",
236
- );
237
- expect(flightApi).toContain("defineEntity<IFlight>({");
238
- expect(flightApi).toContain("from 'rastack/sync'");
239
- });
240
- });