rastack 0.0.58 → 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 (83) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +190 -0
  3. package/dist/deploy/ci.js +2 -2
  4. package/dist/deploy/io.js +2 -2
  5. package/dist/dev/harness.d.ts +1 -1
  6. package/dist/dev/harness.js +1 -2
  7. package/dist/external/cache.d.ts +28 -0
  8. package/dist/external/cache.js +35 -0
  9. package/dist/external/envelope.d.ts +35 -0
  10. package/dist/external/envelope.js +48 -0
  11. package/dist/external/graphql.d.ts +25 -0
  12. package/dist/external/graphql.js +78 -0
  13. package/dist/external/handler.d.ts +52 -0
  14. package/dist/external/handler.js +139 -0
  15. package/dist/external/index.d.ts +19 -0
  16. package/dist/external/index.js +38 -0
  17. package/dist/external/lambda.d.ts +49 -0
  18. package/dist/external/lambda.js +86 -0
  19. package/dist/external/rest.d.ts +17 -0
  20. package/dist/external/rest.js +46 -0
  21. package/dist/external/route.d.ts +12 -0
  22. package/dist/external/route.js +33 -0
  23. package/dist/external/service.d.ts +50 -0
  24. package/dist/external/service.js +59 -0
  25. package/dist/external/types.d.ts +107 -0
  26. package/dist/external/types.js +22 -0
  27. package/dist/external/util.d.ts +16 -0
  28. package/dist/external/util.js +58 -0
  29. package/dist/rastack-dev.js +10 -0
  30. package/dist/rastack-wasm-build.d.ts +1 -1
  31. package/dist/rastack-wasm-build.js +6 -6
  32. package/dist/wasm/rastack_wasm_bg.wasm +0 -0
  33. package/external.ts +10 -0
  34. package/hooks/query/delete.ts +12 -11
  35. package/hooks/query/fetch.ts +6 -8
  36. package/hooks/query/interfaces.ts +1 -4
  37. package/hooks/query/list.ts +16 -9
  38. package/hooks/query/update.ts +8 -6
  39. package/package.json +24 -2
  40. package/scripts/copy-templates.js +1 -1
  41. package/scripts/copy-wasm.js +1 -1
  42. package/scripts/dev-install.mjs +63 -0
  43. package/src/deploy/ci.ts +2 -2
  44. package/src/deploy/io.ts +2 -2
  45. package/src/dev/harness.ts +1 -2
  46. package/src/external/cache.ts +53 -0
  47. package/src/external/envelope.ts +78 -0
  48. package/src/external/graphql.ts +98 -0
  49. package/src/external/handler.ts +207 -0
  50. package/src/external/index.ts +25 -0
  51. package/src/external/lambda.ts +108 -0
  52. package/src/external/rest.ts +60 -0
  53. package/src/external/route.ts +33 -0
  54. package/src/external/service.ts +86 -0
  55. package/src/external/types.ts +121 -0
  56. package/src/external/util.ts +57 -0
  57. package/src/rastack-dev.ts +11 -0
  58. package/src/rastack-wasm-build.ts +6 -6
  59. package/jest.config.cjs +0 -12
  60. package/test/auth.spec.ts +0 -207
  61. package/test/cache.spec.ts +0 -263
  62. package/test/cognito.spec.ts +0 -281
  63. package/test/compile.spec.ts +0 -240
  64. package/test/components.spec.ts +0 -524
  65. package/test/csv-schema.spec.ts +0 -143
  66. package/test/deploy.spec.ts +0 -571
  67. package/test/dev.spec.ts +0 -414
  68. package/test/entities.spec.ts +0 -597
  69. package/test/import.spec.ts +0 -241
  70. package/test/plugin.spec.ts +0 -315
  71. package/test/runtime-manifest.spec.ts +0 -309
  72. package/test/schema-entities.spec.ts +0 -688
  73. package/test/tokens.spec.ts +0 -302
  74. package/test/transitions.spec.ts +0 -372
  75. package/test/typed-hooks.spec.ts +0 -412
  76. package/test/update.spec.ts +0 -152
  77. package/test/validate.spec.ts +0 -319
  78. package/tsconfig.json +0 -27
  79. package/wasm/package.json +0 -4
  80. package/wasm/rastack_wasm.d.ts +0 -100
  81. package/wasm/rastack_wasm.js +0 -745
  82. package/wasm/rastack_wasm_bg.wasm +0 -0
  83. package/wasm/rastack_wasm_bg.wasm.d.ts +0 -28
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ /**
3
+ * External-API bindings — how a rastack `app.model` is served from an upstream
4
+ * REST or GraphQL API.
5
+ *
6
+ * The stack's own resources are served by the Rust API / WASM engine over the
7
+ * `/api/{app}/v1/{model}/` contract. An **external** resource (a GitHub repo, an
8
+ * Expo project, an AWS stack) is served by the proxy instead: it speaks the same
9
+ * contract to the client, and translates each request into an upstream call,
10
+ * normalises the response into rastack's DRF-shaped envelope, and caches it.
11
+ *
12
+ * One {@link ResourceBinding} describes that translation for a single model. The
13
+ * proxy core ({@link ./handler}) is a pure function of a request + the bindings +
14
+ * an injected `fetch`/cache, so the *same* logic runs in the deployed Lambda and
15
+ * in the mini service inside `rastack dev`.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.findBinding = findBinding;
19
+ /** Look a binding up by `app`/`model`. */
20
+ function findBinding(bindings, app, model) {
21
+ return bindings.find((b) => b.app === app && b.model === model);
22
+ }
@@ -0,0 +1,16 @@
1
+ /** Small pure helpers shared by the proxy core. */
2
+ /**
3
+ * Read a value at a dot path (`"me.accounts.0.apps"`) out of a JSON value.
4
+ * Numeric segments index arrays. Returns `undefined` for any missing hop, so
5
+ * callers can default gracefully rather than throw on a shape they didn't get.
6
+ */
7
+ export declare function getPath(value: unknown, path: string | undefined): unknown;
8
+ /** Keep only `fields` on a row (plus always its `id`). No-op when unset. */
9
+ export declare function projectFields(row: Record<string, unknown>, fields: string[] | undefined): Record<string, unknown>;
10
+ /**
11
+ * Ensure a row carries an `id` (the rastack detail-URL key). If the upstream's
12
+ * identity lives under a different `primaryKey`, mirror it onto `id`.
13
+ */
14
+ export declare function ensureId(row: Record<string, unknown>, primaryKey: string): Record<string, unknown>;
15
+ /** First value for a key in a parsed query (`{ page: ["2"] }` → `"2"`). */
16
+ export declare function firstParam(query: Record<string, string[]>, key: string): string | undefined;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ /** Small pure helpers shared by the proxy core. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.getPath = getPath;
5
+ exports.projectFields = projectFields;
6
+ exports.ensureId = ensureId;
7
+ exports.firstParam = firstParam;
8
+ /**
9
+ * Read a value at a dot path (`"me.accounts.0.apps"`) out of a JSON value.
10
+ * Numeric segments index arrays. Returns `undefined` for any missing hop, so
11
+ * callers can default gracefully rather than throw on a shape they didn't get.
12
+ */
13
+ function getPath(value, path) {
14
+ if (!path)
15
+ return value;
16
+ let cur = value;
17
+ for (const segment of path.split(".")) {
18
+ if (cur == null)
19
+ return undefined;
20
+ if (Array.isArray(cur)) {
21
+ const idx = Number(segment);
22
+ cur = Number.isInteger(idx) ? cur[idx] : undefined;
23
+ }
24
+ else if (typeof cur === "object") {
25
+ cur = cur[segment];
26
+ }
27
+ else {
28
+ return undefined;
29
+ }
30
+ }
31
+ return cur;
32
+ }
33
+ /** Keep only `fields` on a row (plus always its `id`). No-op when unset. */
34
+ function projectFields(row, fields) {
35
+ if (!fields || !fields.length)
36
+ return row;
37
+ const out = {};
38
+ if ("id" in row)
39
+ out.id = row.id;
40
+ for (const f of fields)
41
+ if (f in row)
42
+ out[f] = row[f];
43
+ return out;
44
+ }
45
+ /**
46
+ * Ensure a row carries an `id` (the rastack detail-URL key). If the upstream's
47
+ * identity lives under a different `primaryKey`, mirror it onto `id`.
48
+ */
49
+ function ensureId(row, primaryKey) {
50
+ if (primaryKey !== "id" && row.id === undefined && row[primaryKey] !== undefined) {
51
+ return { ...row, id: row[primaryKey] };
52
+ }
53
+ return row;
54
+ }
55
+ /** First value for a key in a parsed query (`{ page: ["2"] }` → `"2"`). */
56
+ function firstParam(query, key) {
57
+ return query[key]?.[0];
58
+ }
@@ -311,6 +311,16 @@ async function runMetroMode(args, cwd) {
311
311
  return proxyToMetro(req, res, metroPort);
312
312
  });
313
313
  server.on("upgrade", (req, socket, head) => proxyUpgrade(req, socket, head, metroPort));
314
+ server.on("error", (err) => {
315
+ if (err.code === "EADDRINUSE") {
316
+ console.error(`\n ✗ Port ${args.port} is already in use — is \`rastack dev\` already running?\n` +
317
+ ` Stop it, or pick another port: rastack dev --port <n>\n`);
318
+ }
319
+ else {
320
+ console.error(`\n ✗ dev server error: ${err.message}`);
321
+ }
322
+ stop(1);
323
+ });
314
324
  server.listen(args.port, () => {
315
325
  const url = `http://localhost:${args.port}`;
316
326
  console.log(` ✓ Metro live on :${metroPort} — fast refresh on`);
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Pipeline:
7
7
  * 1. cargo build -p rastack-wasm --release --target wasm32-unknown-unknown
8
- * 2. wasm-bindgen --target web → tools/wasm/rastack_wasm.js + rastack_wasm_bg.wasm
8
+ * 2. wasm-bindgen --target web → wasm/rastack_wasm.js + rastack_wasm_bg.wasm
9
9
  * 3. wasm-opt -Oz (if available) to shrink the binary
10
10
  *
11
11
  * This is invoked by the release CI (to bundle a prebuilt engine into the npm
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Pipeline:
8
8
  * 1. cargo build -p rastack-wasm --release --target wasm32-unknown-unknown
9
- * 2. wasm-bindgen --target web → tools/wasm/rastack_wasm.js + rastack_wasm_bg.wasm
9
+ * 2. wasm-bindgen --target web → wasm/rastack_wasm.js + rastack_wasm_bg.wasm
10
10
  * 3. wasm-opt -Oz (if available) to shrink the binary
11
11
  *
12
12
  * This is invoked by the release CI (to bundle a prebuilt engine into the npm
@@ -23,9 +23,9 @@ exports.buildWasm = buildWasm;
23
23
  const child_process_1 = require("child_process");
24
24
  const fs_1 = __importDefault(require("fs"));
25
25
  const path_1 = __importDefault(require("path"));
26
- const repoRoot = path_1.default.resolve(__dirname, "..", "..");
27
- const rustDir = path_1.default.join(repoRoot, "rust");
28
- const outDir = path_1.default.join(repoRoot, "tools", "wasm");
26
+ const repoRoot = path_1.default.resolve(__dirname, "..");
27
+ const rustDir = path_1.default.join(repoRoot, "engine");
28
+ const outDir = path_1.default.join(repoRoot, "wasm");
29
29
  const wasmPath = path_1.default.join(rustDir, "target", "wasm32-unknown-unknown", "release", "rastack_wasm.wasm");
30
30
  function run(cmd, args, cwd = repoRoot) {
31
31
  // Local build tool: `cmd`/`args` are the fixed toolchain invocations below
@@ -65,7 +65,7 @@ function buildWasm() {
65
65
  "--target",
66
66
  "wasm32-unknown-unknown",
67
67
  ], rustDir);
68
- console.log("→ wasm-bindgen → tools/wasm/");
68
+ console.log("→ wasm-bindgen → wasm/");
69
69
  run("wasm-bindgen", [
70
70
  "--target",
71
71
  "web",
@@ -87,7 +87,7 @@ function buildWasm() {
87
87
  console.log("· wasm-opt not found — skipping size optimisation (optional)");
88
88
  }
89
89
  const size = fs_1.default.statSync(path_1.default.join(outDir, "rastack_wasm_bg.wasm")).size;
90
- console.log(`✓ wasm engine built: tools/wasm/ (${(size / 1024).toFixed(0)} KB)`);
90
+ console.log(`✓ wasm engine built: wasm/ (${(size / 1024).toFixed(0)} KB)`);
91
91
  }
92
92
  if (require.main === module) {
93
93
  buildWasm();
Binary file
package/external.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `rastack/external` — the external-API proxy core.
3
+ *
4
+ * Serve external REST/GraphQL APIs (GitHub, Expo, AWS) over the rastack
5
+ * `/api/{app}/v1/{model}/` contract so the normal typed hooks consume them
6
+ * unchanged. The same core runs in two adapters: the mini service inside
7
+ * `rastack dev` ({@link createExternalService}) and an AWS Lambda behind API
8
+ * Gateway ({@link handleLambdaEvent}).
9
+ */
10
+ export * from "./src/external";
@@ -1,6 +1,9 @@
1
1
  import { AxiosResponse } from "axios";
2
- import { UseMutationResult } from "react-query/types/react/types";
3
- import { useMutation, useQueryClient } from "react-query";
2
+ import {
3
+ useMutation,
4
+ useQueryClient,
5
+ type UseMutationResult,
6
+ } from "@tanstack/react-query";
4
7
  import { IDeleteQueryResult } from "./interfaces";
5
8
  import { API } from "./api";
6
9
 
@@ -15,8 +18,8 @@ export function useDeleteQuery<TResult>({
15
18
  }: DeleteQueryProps): IDeleteQueryResult<TResult> {
16
19
  const queryClient = useQueryClient();
17
20
 
18
- return useMutation(
19
- async (params: { id?: string }) => {
21
+ return useMutation({
22
+ mutationFn: async (params: { id?: string }) => {
20
23
  if (params.id === undefined) {
21
24
  throw new Error("ID is required for delete operation");
22
25
  }
@@ -36,12 +39,10 @@ export function useDeleteQuery<TResult>({
36
39
  // assume `.results` exists on an empty body.
37
40
  return response.data?.results ?? response.data ?? null;
38
41
  },
39
- {
40
- onSuccess: () => {
41
- if (listQueryCacheKey) {
42
- queryClient.invalidateQueries(listQueryCacheKey);
43
- }
44
- },
42
+ onSuccess: () => {
43
+ if (listQueryCacheKey) {
44
+ queryClient.invalidateQueries({ queryKey: [listQueryCacheKey] });
45
+ }
45
46
  },
46
- ) as UseMutationResult<TResult>;
47
+ }) as UseMutationResult<TResult>;
47
48
  }
@@ -1,5 +1,5 @@
1
1
  import { AxiosResponse } from "axios";
2
- import { useQuery } from "react-query";
2
+ import { useQuery } from "@tanstack/react-query";
3
3
  import { API } from "./api";
4
4
  import { stringifyQueryParams } from "./url";
5
5
  import { GetQueryOptions, IGetQueryResult } from "./interfaces";
@@ -31,9 +31,9 @@ export function useGetQuery<TParam, TResult>({
31
31
 
32
32
  const params = options?.params;
33
33
 
34
- return useQuery(
35
- [queryKey, getUrl, params],
36
- async () => {
34
+ return useQuery({
35
+ queryKey: [queryKey, getUrl, params],
36
+ queryFn: async () => {
37
37
  const urlParams = stringifyQueryParams(pathParameters, params);
38
38
  const serverURL = `${getUrl}${urlParams ? `?${urlParams}` : ""}`;
39
39
 
@@ -41,8 +41,6 @@ export function useGetQuery<TParam, TResult>({
41
41
 
42
42
  return response.data;
43
43
  },
44
- {
45
- enabled: isAuthenticated && isRequiredParamsPresent,
46
- },
47
- );
44
+ enabled: isAuthenticated && isRequiredParamsPresent,
45
+ });
48
46
  }
@@ -1,7 +1,4 @@
1
- import {
2
- UseMutationResult,
3
- UseQueryResult,
4
- } from "react-query/types/react/types";
1
+ import { UseMutationResult, UseQueryResult } from "@tanstack/react-query";
5
2
  import { ExtractKey } from "../../types";
6
3
 
7
4
  export interface IPaginationActions {
@@ -1,5 +1,5 @@
1
1
  import { AxiosResponse } from "axios";
2
- import { useQuery } from "react-query";
2
+ import { useQuery } from "@tanstack/react-query";
3
3
  import { useEffect, useState } from "react";
4
4
  import {
5
5
  IFilterActions,
@@ -125,14 +125,21 @@ export function useGetPaginatedQueryList<TParam, TResult>({
125
125
  }
126
126
  };
127
127
 
128
- const query = useQuery<IPaginatedResponse<TResult>>(
129
- [queryKey, getUrl, options?.params, serverQueryParamState],
130
- fetchData,
131
- {
132
- enabled: isRequiredQueryParamsPresent && isAuthenticated,
133
- onSuccess: options?.onSuccess,
134
- },
135
- );
128
+ const query = useQuery<IPaginatedResponse<TResult>>({
129
+ queryKey: [queryKey, getUrl, options?.params, serverQueryParamState],
130
+ queryFn: fetchData,
131
+ enabled: isRequiredQueryParamsPresent && isAuthenticated,
132
+ });
133
+
134
+ // react-query v5 dropped the query-level `onSuccess` callback; mirror it with
135
+ // an effect so callers that pass `options.onSuccess` keep firing on each new
136
+ // successful result.
137
+ useEffect(() => {
138
+ if (query.isSuccess && query.data) {
139
+ options?.onSuccess?.(query.data);
140
+ }
141
+ // eslint-disable-next-line react-hooks/exhaustive-deps
142
+ }, [query.isSuccess, query.data]);
136
143
 
137
144
  // The list envelope is either nested (`{ pagination: { count, last } }`, the
138
145
  // Django/DRF-paginated shape) or flat DRF-classic (`{ count, next, previous,
@@ -1,5 +1,5 @@
1
1
  import { AxiosResponse } from "axios";
2
- import { useMutation, useQueryClient } from "react-query";
2
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
3
3
  import { IUpdateQueryResult, WriteQueryVariables } from "./interfaces";
4
4
  import { API } from "./api";
5
5
  import { stringifyQueryParams } from "./url";
@@ -29,8 +29,9 @@ export function useUpdateQuery<TParams, TPayload, TResult>({
29
29
  unknown,
30
30
  WriteQueryVariables<TParams, TPayload>,
31
31
  unknown
32
- >(({ params, payload }) => {
33
- return new Promise((resolve, reject) => {
32
+ >({
33
+ mutationFn: ({ params, payload }) => {
34
+ return new Promise((resolve, reject) => {
34
35
  let urlType: "put" | "patch" | "post" = "patch";
35
36
  let url: string | undefined;
36
37
 
@@ -73,16 +74,17 @@ export function useUpdateQuery<TParams, TPayload, TResult>({
73
74
  getAPIFunction(urlType, url, payload)
74
75
  .then((response: AxiosResponse<TResult>) => {
75
76
  if (itemQueryCacheKey) {
76
- queryClient.invalidateQueries([itemQueryCacheKey]);
77
+ queryClient.invalidateQueries({ queryKey: [itemQueryCacheKey] });
77
78
  }
78
79
  if (listQueryCacheKey) {
79
- queryClient.invalidateQueries(listQueryCacheKey);
80
+ queryClient.invalidateQueries({ queryKey: [listQueryCacheKey] });
80
81
  }
81
82
  resolve(response.data);
82
83
  })
83
84
  .catch((error) => {
84
85
  reject(error);
85
86
  });
86
- });
87
+ });
88
+ },
87
89
  });
88
90
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rastack",
3
- "version": "0.0.58",
3
+ "version": "0.0.59",
4
4
  "description": "",
5
5
  "main": "runtime.ts",
6
6
  "types": "runtime.ts",
@@ -11,6 +11,28 @@
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
14
+ "files": [
15
+ "admin",
16
+ "auth",
17
+ "cache",
18
+ "components",
19
+ "hooks",
20
+ "provider",
21
+ "sync",
22
+ "theme",
23
+ "src",
24
+ "scripts",
25
+ "dist",
26
+ "define.ts",
27
+ "db.ts",
28
+ "validate.ts",
29
+ "plugin.ts",
30
+ "external.ts",
31
+ "tokens.ts",
32
+ "types.ts",
33
+ "runtime.ts",
34
+ "CHANGELOG.md"
35
+ ],
14
36
  "scripts": {
15
37
  "format": "prettier --write \"**/*.{ts,tsx,md}\"",
16
38
  "build": "tsc && cp src/.prettierrc dist/.prettierrc && node scripts/copy-templates.js && node scripts/build-admin.js && node scripts/copy-wasm.js",
@@ -39,7 +61,6 @@
39
61
  "luxon": "^3.7.2",
40
62
  "minimatch": "^10.2.5",
41
63
  "prettier": "^3.9.5",
42
- "react-query": "^3.39.3",
43
64
  "typescript": "~5.9.3",
44
65
  "usehooks-ts": "^3.1.1"
45
66
  },
@@ -47,6 +68,7 @@
47
68
  "react": "^19.2.7"
48
69
  },
49
70
  "devDependencies": {
71
+ "@testing-library/react-hooks": "8.0.1",
50
72
  "@types/jest": "^30.0.0",
51
73
  "@types/js-yaml": "^4.0.9",
52
74
  "@types/mocha": "^10.0.6",
@@ -13,7 +13,7 @@
13
13
  const fs = require("fs");
14
14
  const path = require("path");
15
15
 
16
- const repoRoot = path.resolve(__dirname, "..", "..");
16
+ const repoRoot = path.resolve(__dirname, "..");
17
17
  const srcDir = path.join(repoRoot, "deploy", "cloudformation");
18
18
  const destDir = path.join(__dirname, "..", "dist", "templates", "cloudformation");
19
19
 
@@ -17,7 +17,7 @@ const entry = path.join(src, "rastack_wasm.js");
17
17
 
18
18
  if (!fs.existsSync(entry)) {
19
19
  console.log(
20
- "copy-wasm: no prebuilt engine at tools/wasm (skipping — build it with " +
20
+ "copy-wasm: no prebuilt engine at wasm/ (skipping — build it with " +
21
21
  "rastack-wasm-build.js to ship it).",
22
22
  );
23
23
  process.exit(0);
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dev-install — fast local build + install of the rastack CLI.
4
+ *
5
+ * Compiles `src/ → dist/` with the local TypeScript, then merge-copies the fresh
6
+ * dist over the globally installed copy (`~/.rastack/node_modules/rastack/dist`)
7
+ * so `rastack` and `ras` on the PATH immediately run the new code. The copy
8
+ * merges (it never deletes), which preserves the shipped runtime artifacts that
9
+ * already live there — the wasm engine, the admin bundle, and the templates —
10
+ * none of which a tsc-only build emits.
11
+ *
12
+ * This is the fast local iteration loop; a full `npm run build && npm pack`
13
+ * publish is unchanged. Usage:
14
+ *
15
+ * node scripts/dev-install.mjs [--prefix <dir>] # defaults to ~/.rastack
16
+ */
17
+ import { execFileSync } from "node:child_process";
18
+ import * as fs from "node:fs";
19
+ import * as os from "node:os";
20
+ import * as path from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+
23
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
24
+ const TOOLS = path.resolve(HERE, "..");
25
+
26
+ function flag(name, fallback) {
27
+ const i = process.argv.indexOf(name);
28
+ return i !== -1 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
29
+ }
30
+
31
+ // The install prefix rastack ships into (the hosted installer uses ~/.rastack).
32
+ const prefix = flag("--prefix", path.join(os.homedir(), ".rastack"));
33
+ const target = path.join(prefix, "node_modules", "rastack");
34
+
35
+ if (!fs.existsSync(target)) {
36
+ console.error(
37
+ ` ✗ no rastack install found at ${target}\n` +
38
+ " Install it first (hosted installer / npm install -g rastack), or pass --prefix <dir>.",
39
+ );
40
+ process.exit(1);
41
+ }
42
+
43
+ // 1) Compile TypeScript → dist/. Run tsc through node (avoids the Windows .cmd
44
+ // spawn EINVAL) so this loop is identical on every platform.
45
+ console.log("▸ tsc — compiling src → dist");
46
+ const tscJs = path.join(TOOLS, "node_modules", "typescript", "bin", "tsc");
47
+ execFileSync(process.execPath, [tscJs, "-p", path.join(TOOLS, "tsconfig.json")], {
48
+ stdio: "inherit",
49
+ cwd: TOOLS,
50
+ });
51
+
52
+ // 2) Merge-copy the fresh dist over the global install (never delete → the wasm
53
+ // engine, admin bundle and templates already there are left untouched).
54
+ console.log(`▸ installing → ${target}`);
55
+ fs.cpSync(path.join(TOOLS, "dist"), path.join(target, "dist"), { recursive: true });
56
+ fs.copyFileSync(path.join(TOOLS, "package.json"), path.join(target, "package.json"));
57
+
58
+ const version = JSON.parse(
59
+ fs.readFileSync(path.join(target, "package.json"), "utf-8"),
60
+ ).version;
61
+ console.log(
62
+ ` ✓ installed rastack ${version} — \`rastack\` and \`ras\` now run this local build`,
63
+ );
package/src/deploy/ci.ts CHANGED
@@ -83,7 +83,7 @@ function stackOutput(
83
83
 
84
84
  /** The Lambda staging directory `cargo lambda build` writes the binary into. */
85
85
  function stageDir(cfg: DeployConfig): string {
86
- return `rust/target/lambda/${cfg.lambda.bin}`;
86
+ return `engine/target/lambda/${cfg.lambda.bin}`;
87
87
  }
88
88
 
89
89
  /** `check` — validate the resource graph and compile the manifest. A CI gate. */
@@ -118,7 +118,7 @@ export function buildSteps(
118
118
  {
119
119
  name: "Cross-build the Lambda binary",
120
120
  run: `cargo lambda build --release ${archFlag} --bin ${cfg.lambda.bin} --features ${cfg.lambda.features}`,
121
- cwd: "rust",
121
+ cwd: "engine",
122
122
  },
123
123
  {
124
124
  name: "Bake the Cognito JWKS into the bundle",
package/src/deploy/io.ts CHANGED
@@ -39,8 +39,8 @@ export function resolveDeployAsset(
39
39
  const candidates = [
40
40
  // Installed package: dist/deploy/io.js → dist/templates/cloudformation/…
41
41
  path.join(__dirname, "..", "templates", "cloudformation", relPath),
42
- // Running from TS source: tools/src/deploy/io.ts → repo-root deploy/…
43
- path.join(__dirname, "..", "..", "..", "deploy", "cloudformation", relPath),
42
+ // Running from TS source: src/deploy/io.ts → repo-root deploy/…
43
+ path.join(__dirname, "..", "..", "deploy", "cloudformation", relPath),
44
44
  // Invoked inside the framework repo itself (cwd is the repo root).
45
45
  path.join(cwd, "deploy", "cloudformation", relPath),
46
46
  ];
@@ -155,7 +155,7 @@ export function contentType(filePath: string): string {
155
155
  * Ordered directories to look for the prebuilt wasm bundle (`rastack_wasm.js` +
156
156
  * `rastack_wasm_bg.wasm`). The bundle is shipped inside the package
157
157
  * (`dist/wasm/`, copied at build), so `rastack dev` works from any repo with no
158
- * cargo; the repo-local `tools/wasm/` (from a source build) and a sibling of the
158
+ * cargo; the repo-local `wasm/` (from a source build) and a sibling of the
159
159
  * running module are fallbacks for development. `dirnameDir` is the directory of
160
160
  * the compiled `rastack-dev.js` (`dist/`), so `../wasm` and `./wasm` cover both
161
161
  * `dist/rastack-dev.js` → `dist/wasm` and `dist/dev/…` layouts.
@@ -167,7 +167,6 @@ export function wasmBundleCandidates(
167
167
  return [
168
168
  path.join(dirnameDir, "wasm"),
169
169
  path.join(dirnameDir, "..", "wasm"),
170
- path.join(cwd, "tools", "wasm"),
171
170
  path.join(cwd, "wasm"),
172
171
  ];
173
172
  }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The proxy's response cache — the reason `all API calls are proxied in Lambda`.
3
+ *
4
+ * A tiny pure abstraction (`get`/`set` over string keys) so the *same* handler
5
+ * runs against an in-memory `Map` (the mini `rastack dev` service, a warm Lambda
6
+ * container) or a shared store (DynamoDB/ElastiCache in production) with no code
7
+ * change. Freshness and ETag bookkeeping are pure functions here; the transport
8
+ * (conditional `If-None-Match` requests) lives in the handler.
9
+ */
10
+
11
+ export interface CacheEntry {
12
+ /** The normalised rastack response body that was cached. */
13
+ value: unknown;
14
+ /** Epoch ms the entry was (re)validated. */
15
+ storedAt: number;
16
+ /** The upstream ETag, if any, for conditional revalidation. */
17
+ etag?: string;
18
+ }
19
+
20
+ export interface ProxyCache {
21
+ get(key: string): CacheEntry | undefined;
22
+ set(key: string, entry: CacheEntry): void;
23
+ delete(key: string): void;
24
+ }
25
+
26
+ /** An in-memory cache — the default for `rastack dev` and warm Lambda containers. */
27
+ export function memoryCache(): ProxyCache {
28
+ const store = new Map<string, CacheEntry>();
29
+ return {
30
+ get: (key) => store.get(key),
31
+ set: (key, entry) => void store.set(key, entry),
32
+ delete: (key) => void store.delete(key),
33
+ };
34
+ }
35
+
36
+ /** A stable cache key for a request (order-independent over query params). */
37
+ export function cacheKey(
38
+ app: string,
39
+ model: string,
40
+ id: string | undefined,
41
+ query: Record<string, string[]>,
42
+ ): string {
43
+ const params = Object.keys(query)
44
+ .sort()
45
+ .map((k) => `${k}=${[...query[k]].sort().join(",")}`)
46
+ .join("&");
47
+ return `${app}.${model}/${id ?? "list"}?${params}`;
48
+ }
49
+
50
+ /** Whether an entry is still within its TTL (and so served without an upstream call). */
51
+ export function isFresh(entry: CacheEntry, ttlMs: number, now: number): boolean {
52
+ return now - entry.storedAt < ttlMs;
53
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Normalise an upstream response into rastack's REST contract — the DRF-shaped
3
+ * list envelope and bare detail object the typed hooks and `<DataTable>` expect,
4
+ * identical to what the Rust API and the WASM/local engine emit.
5
+ *
6
+ * External APIs rarely page the way rastack does (GitHub uses `per_page` + Link
7
+ * headers; a GraphQL list just returns an array), so the envelope is synthesised
8
+ * from what we have: the page the client asked for and the number of items this
9
+ * page actually returned. `count`/`last` are best-effort when the upstream total
10
+ * is unknown — enough for the pager to advance while there is a full page.
11
+ */
12
+
13
+ import { ensureId, projectFields } from "./util";
14
+
15
+ export interface ListEnvelope {
16
+ results: Record<string, unknown>[];
17
+ pagination: {
18
+ current: number;
19
+ last: number;
20
+ next: number | null;
21
+ previous: number | null;
22
+ page_size: number;
23
+ /** Best-effort total; equals what we can prove when the upstream omits it. */
24
+ count: number;
25
+ };
26
+ }
27
+
28
+ export interface NormalizeOptions {
29
+ page: number;
30
+ pageSize: number;
31
+ primaryKey: string;
32
+ fields?: string[];
33
+ /** A known upstream total, if the transport could determine one. */
34
+ totalCount?: number;
35
+ }
36
+
37
+ /** Shape a page of upstream items into the list envelope. */
38
+ export function toListEnvelope(
39
+ items: unknown,
40
+ opts: NormalizeOptions,
41
+ ): ListEnvelope {
42
+ const rows = (Array.isArray(items) ? items : [])
43
+ .filter((r): r is Record<string, unknown> => !!r && typeof r === "object")
44
+ .map((r) => projectFields(ensureId(r, opts.primaryKey), opts.fields));
45
+
46
+ const { page, pageSize } = opts;
47
+ // A full page implies there may be more; a short page is the last one.
48
+ const hasMore = rows.length >= pageSize;
49
+ const count =
50
+ opts.totalCount ?? (page - 1) * pageSize + rows.length;
51
+ const last = opts.totalCount
52
+ ? Math.max(1, Math.ceil(opts.totalCount / pageSize))
53
+ : hasMore
54
+ ? page + 1
55
+ : page;
56
+
57
+ return {
58
+ results: rows,
59
+ pagination: {
60
+ current: page,
61
+ last,
62
+ next: page < last ? page + 1 : null,
63
+ previous: page > 1 ? page - 1 : null,
64
+ page_size: pageSize,
65
+ count,
66
+ },
67
+ };
68
+ }
69
+
70
+ /** Shape a single upstream object into a rastack detail row. */
71
+ export function toDetail(
72
+ item: unknown,
73
+ primaryKey: string,
74
+ fields?: string[],
75
+ ): Record<string, unknown> | null {
76
+ if (!item || typeof item !== "object") return null;
77
+ return projectFields(ensureId(item as Record<string, unknown>, primaryKey), fields);
78
+ }