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.
- package/CHANGELOG.md +9 -0
- package/README.md +190 -0
- package/dist/deploy/ci.js +2 -2
- package/dist/deploy/io.js +2 -2
- package/dist/dev/harness.d.ts +1 -1
- package/dist/dev/harness.js +1 -2
- package/dist/external/cache.d.ts +28 -0
- package/dist/external/cache.js +35 -0
- package/dist/external/envelope.d.ts +35 -0
- package/dist/external/envelope.js +48 -0
- package/dist/external/graphql.d.ts +25 -0
- package/dist/external/graphql.js +78 -0
- package/dist/external/handler.d.ts +52 -0
- package/dist/external/handler.js +139 -0
- package/dist/external/index.d.ts +19 -0
- package/dist/external/index.js +38 -0
- package/dist/external/lambda.d.ts +49 -0
- package/dist/external/lambda.js +86 -0
- package/dist/external/rest.d.ts +17 -0
- package/dist/external/rest.js +46 -0
- package/dist/external/route.d.ts +12 -0
- package/dist/external/route.js +33 -0
- package/dist/external/service.d.ts +50 -0
- package/dist/external/service.js +59 -0
- package/dist/external/types.d.ts +107 -0
- package/dist/external/types.js +22 -0
- package/dist/external/util.d.ts +16 -0
- package/dist/external/util.js +58 -0
- package/dist/rastack-dev.js +10 -0
- package/dist/rastack-wasm-build.d.ts +1 -1
- package/dist/rastack-wasm-build.js +6 -6
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/external.ts +10 -0
- package/hooks/query/delete.ts +12 -11
- package/hooks/query/fetch.ts +6 -8
- package/hooks/query/interfaces.ts +1 -4
- package/hooks/query/list.ts +16 -9
- package/hooks/query/update.ts +8 -6
- package/package.json +24 -2
- package/scripts/copy-templates.js +1 -1
- package/scripts/copy-wasm.js +1 -1
- package/scripts/dev-install.mjs +63 -0
- package/src/deploy/ci.ts +2 -2
- package/src/deploy/io.ts +2 -2
- package/src/dev/harness.ts +1 -2
- package/src/external/cache.ts +53 -0
- package/src/external/envelope.ts +78 -0
- package/src/external/graphql.ts +98 -0
- package/src/external/handler.ts +207 -0
- package/src/external/index.ts +25 -0
- package/src/external/lambda.ts +108 -0
- package/src/external/rest.ts +60 -0
- package/src/external/route.ts +33 -0
- package/src/external/service.ts +86 -0
- package/src/external/types.ts +121 -0
- package/src/external/util.ts +57 -0
- package/src/rastack-dev.ts +11 -0
- package/src/rastack-wasm-build.ts +6 -6
- package/jest.config.cjs +0 -12
- package/test/auth.spec.ts +0 -207
- package/test/cache.spec.ts +0 -263
- package/test/cognito.spec.ts +0 -281
- package/test/compile.spec.ts +0 -240
- package/test/components.spec.ts +0 -524
- package/test/csv-schema.spec.ts +0 -143
- package/test/deploy.spec.ts +0 -571
- package/test/dev.spec.ts +0 -414
- package/test/entities.spec.ts +0 -597
- package/test/import.spec.ts +0 -241
- package/test/plugin.spec.ts +0 -315
- package/test/runtime-manifest.spec.ts +0 -309
- package/test/schema-entities.spec.ts +0 -688
- package/test/tokens.spec.ts +0 -302
- package/test/transitions.spec.ts +0 -372
- package/test/typed-hooks.spec.ts +0 -412
- package/test/update.spec.ts +0 -152
- package/test/validate.spec.ts +0 -319
- package/tsconfig.json +0 -27
- package/wasm/package.json +0 -4
- package/wasm/rastack_wasm.d.ts +0 -100
- package/wasm/rastack_wasm.js +0 -745
- package/wasm/rastack_wasm_bg.wasm +0 -0
- 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
|
+
}
|
package/dist/rastack-dev.js
CHANGED
|
@@ -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 →
|
|
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 →
|
|
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, "
|
|
28
|
-
const outDir = path_1.default.join(repoRoot, "
|
|
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 →
|
|
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:
|
|
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";
|
package/hooks/query/delete.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { AxiosResponse } from "axios";
|
|
2
|
-
import {
|
|
3
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
}
|
package/hooks/query/fetch.ts
CHANGED
|
@@ -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
|
-
|
|
46
|
-
},
|
|
47
|
-
);
|
|
44
|
+
enabled: isAuthenticated && isRequiredParamsPresent,
|
|
45
|
+
});
|
|
48
46
|
}
|
package/hooks/query/list.ts
CHANGED
|
@@ -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
|
-
|
|
133
|
-
|
|
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,
|
package/hooks/query/update.ts
CHANGED
|
@@ -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
|
-
>(
|
|
33
|
-
|
|
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.
|
|
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
|
|
package/scripts/copy-wasm.js
CHANGED
|
@@ -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
|
|
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 `
|
|
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: "
|
|
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:
|
|
43
|
-
path.join(__dirname, "..", "..", "
|
|
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
|
];
|
package/src/dev/harness.ts
CHANGED
|
@@ -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 `
|
|
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
|
+
}
|