rastack 0.0.61 → 0.0.63
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 +22 -0
- package/admin/AdminApp.tsx +36 -1
- package/admin/adapters.ts +80 -9
- package/admin/ui/Sidebar.tsx +8 -0
- package/admin/ui/styles.ts +13 -0
- package/auth/provider.ts +71 -8
- package/dist/admin.js +26 -13
- package/dist/deploy/ci.d.ts +10 -3
- package/dist/deploy/ci.js +22 -11
- package/dist/deploy/config.d.ts +155 -5
- package/dist/deploy/config.js +216 -6
- package/dist/deploy/io.d.ts +7 -0
- package/dist/deploy/io.js +17 -0
- package/dist/deploy/workflows.d.ts +30 -0
- package/dist/deploy/workflows.js +117 -12
- package/dist/rastack-ci.js +19 -1
- package/dist/rastack-dev.js +48 -2
- package/dist/rastack-init.js +190 -64
- package/dist/rastack.js +1 -0
- package/dist/templates/cloudformation/app.yaml +133 -2
- package/dist/wasm/rastack_wasm.js +1 -1
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/package.json +1 -1
- package/src/deploy/ci.ts +50 -16
- package/src/deploy/config.ts +340 -7
- package/src/deploy/io.ts +17 -1
- package/src/deploy/workflows.ts +122 -12
- package/src/rastack-ci.ts +20 -2
- package/src/rastack-dev.ts +61 -6
- package/src/rastack-init.ts +235 -76
- package/src/rastack.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [0.0.63](https://github.com/theserverkid/reactapistack/compare/v0.0.62...v0.0.63) (2026-07-20)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **deploy:** dev + prod environments with per-env Cognito, proxy, and .env ([996e726](https://github.com/theserverkid/reactapistack/commit/996e726e7e0d55f01e3111579594c2b66e88a79c))
|
|
11
|
+
|
|
12
|
+
### [0.0.62](https://github.com/theserverkid/reactapistack/compare/v0.0.61...v0.0.62) (2026-07-19)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **admin:** Iceberg table discovery — surface warehouse tables the manifest doesn't declare ([9a3b687](https://github.com/theserverkid/reactapistack/commit/9a3b68768878c545e3d1c4c7a95bc3ba75c94856))
|
|
18
|
+
* **catalog:** Iceberg REST Catalog so DuckDB/PyIceberg can write the warehouse (phase 2) ([bf402eb](https://github.com/theserverkid/reactapistack/commit/bf402eb9d2b5ff99ac8074d55fc132a9f26466ab))
|
|
19
|
+
* **store:** make the native engine Iceberg-native — read rows by walking the manifests ([b15fe8b](https://github.com/theserverkid/reactapistack/commit/b15fe8bdbb7053ed4fcadd7dc750c2a4ce313878))
|
|
20
|
+
* **store:** publish real Apache Iceberg tables so DuckDB/PyIceberg can read the warehouse (phase 1) ([13835cd](https://github.com/theserverkid/reactapistack/commit/13835cdeab8f2a48360947ac767808e43d5bd2f8))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* **ci:** avoid raw f-string SQL in verify_iceberg.py (Semgrep SAST) ([cffd674](https://github.com/theserverkid/reactapistack/commit/cffd6742ddb7ec04ed8f7b65c293c8de5eca1060))
|
|
26
|
+
|
|
5
27
|
### [0.0.61](https://github.com/theserverkid/reactapistack/compare/v0.0.60...v0.0.61) (2026-07-19)
|
|
6
28
|
|
|
7
29
|
|
package/admin/AdminApp.tsx
CHANGED
|
@@ -23,7 +23,9 @@ import type { RastackEngine } from "../provider/types";
|
|
|
23
23
|
import {
|
|
24
24
|
AdminResource,
|
|
25
25
|
changelistHeaders,
|
|
26
|
+
DISCOVER_PATH,
|
|
26
27
|
findResource,
|
|
28
|
+
mergeDiscovered,
|
|
27
29
|
} from "./adapters";
|
|
28
30
|
import {
|
|
29
31
|
useDashboardStats,
|
|
@@ -653,12 +655,45 @@ function useSidebarCollapsed(): [boolean, () => void] {
|
|
|
653
655
|
return [collapsed, toggle];
|
|
654
656
|
}
|
|
655
657
|
|
|
658
|
+
/**
|
|
659
|
+
* Fold warehouse-discovered tables (Iceberg discovery, `GET /api/discover/`)
|
|
660
|
+
* into the manifest-declared resources. Runs once against the ready engine —
|
|
661
|
+
* the same seam in both modes (WASM locally, HTTP to `rastack-server`
|
|
662
|
+
* remotely). Discovery is best-effort: an older server without the endpoint, or
|
|
663
|
+
* a non-admin identity, just leaves the declared set untouched.
|
|
664
|
+
*/
|
|
665
|
+
function useDiscoveredResources(
|
|
666
|
+
engine: RastackEngine,
|
|
667
|
+
declared: AdminResource[],
|
|
668
|
+
): AdminResource[] {
|
|
669
|
+
const [resources, setResources] = useState(declared);
|
|
670
|
+
useEffect(() => {
|
|
671
|
+
let live = true;
|
|
672
|
+
setResources(declared);
|
|
673
|
+
(async () => {
|
|
674
|
+
try {
|
|
675
|
+
const res = await engine.handle("GET", DISCOVER_PATH, {}, undefined);
|
|
676
|
+
if (live && res && res.status === 200) {
|
|
677
|
+
setResources(mergeDiscovered(declared, res.body));
|
|
678
|
+
}
|
|
679
|
+
} catch {
|
|
680
|
+
/* discovery is optional — keep the manifest resources */
|
|
681
|
+
}
|
|
682
|
+
})();
|
|
683
|
+
return () => {
|
|
684
|
+
live = false;
|
|
685
|
+
};
|
|
686
|
+
}, [engine, declared]);
|
|
687
|
+
return resources;
|
|
688
|
+
}
|
|
689
|
+
|
|
656
690
|
export function AdminApp(props: {
|
|
657
691
|
engine: RastackEngine;
|
|
658
692
|
resources: AdminResource[];
|
|
659
693
|
}): React.JSX.Element {
|
|
660
694
|
const route = useRoute();
|
|
661
|
-
const { engine
|
|
695
|
+
const { engine } = props;
|
|
696
|
+
const resources = useDiscoveredResources(engine, props.resources);
|
|
662
697
|
const [collapsed, toggleCollapsed] = useSidebarCollapsed();
|
|
663
698
|
|
|
664
699
|
const activeKey =
|
package/admin/adapters.ts
CHANGED
|
@@ -47,6 +47,12 @@ export interface AdminResource {
|
|
|
47
47
|
listDisplay: string[] | null;
|
|
48
48
|
/** Whether this resource is shown in the admin (admin !== false). */
|
|
49
49
|
adminEnabled: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* True when this table was found in the warehouse (Iceberg discovery) but is
|
|
52
|
+
* *not* declared in the compiled manifest. Its fields are inferred from a
|
|
53
|
+
* sample of its rows rather than authored, so the UI badges it as discovered.
|
|
54
|
+
*/
|
|
55
|
+
discovered?: boolean;
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
interface RawField {
|
|
@@ -69,6 +75,19 @@ interface RawManifest {
|
|
|
69
75
|
resources?: RawResource[];
|
|
70
76
|
}
|
|
71
77
|
|
|
78
|
+
/** Map one raw manifest/discovery field to an admin field descriptor. */
|
|
79
|
+
function toAdminField(f: RawField): AdminField {
|
|
80
|
+
return {
|
|
81
|
+
name: f.name,
|
|
82
|
+
type: f.type,
|
|
83
|
+
primaryKey: f.primaryKey === true,
|
|
84
|
+
maxLength: f.maxLength,
|
|
85
|
+
nullable: f.null === true,
|
|
86
|
+
unique: f.unique === true,
|
|
87
|
+
relation: f.relation,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
72
91
|
/** Parse a manifest (object or JSON string) into admin resource descriptors. */
|
|
73
92
|
export function parseManifest(manifest: object | string): AdminResource[] {
|
|
74
93
|
const doc: RawManifest =
|
|
@@ -78,15 +97,7 @@ export function parseManifest(manifest: object | string): AdminResource[] {
|
|
|
78
97
|
model: r.model,
|
|
79
98
|
key: `${r.app}.${r.model}`,
|
|
80
99
|
label: humanizeKey(r.model),
|
|
81
|
-
fields: (r.fields ?? []).map(
|
|
82
|
-
name: f.name,
|
|
83
|
-
type: f.type,
|
|
84
|
-
primaryKey: f.primaryKey === true,
|
|
85
|
-
maxLength: f.maxLength,
|
|
86
|
-
nullable: f.null === true,
|
|
87
|
-
unique: f.unique === true,
|
|
88
|
-
relation: f.relation,
|
|
89
|
-
})),
|
|
100
|
+
fields: (r.fields ?? []).map(toAdminField),
|
|
90
101
|
search: r.search ?? [],
|
|
91
102
|
listDisplay:
|
|
92
103
|
r.admin && Array.isArray(r.admin.listDisplay) ? r.admin.listDisplay : null,
|
|
@@ -94,6 +105,66 @@ export function parseManifest(manifest: object | string): AdminResource[] {
|
|
|
94
105
|
}));
|
|
95
106
|
}
|
|
96
107
|
|
|
108
|
+
// -- Iceberg table discovery (`GET /api/discover/`) -------------------------
|
|
109
|
+
|
|
110
|
+
/** The discovery endpoint the WASM/HTTP engine answers, over the `handle` seam. */
|
|
111
|
+
export const DISCOVER_PATH = "/api/discover/";
|
|
112
|
+
|
|
113
|
+
/** One table in the discovery payload (`{ tables: [...] }`). */
|
|
114
|
+
interface DiscoveredTable {
|
|
115
|
+
app: string;
|
|
116
|
+
model: string;
|
|
117
|
+
declared: boolean;
|
|
118
|
+
adminEnabled?: boolean;
|
|
119
|
+
rowCount?: number;
|
|
120
|
+
fields?: RawField[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Read the `{ tables: [...] }` discovery body defensively. */
|
|
124
|
+
export function parseDiscovered(body: unknown): DiscoveredTable[] {
|
|
125
|
+
const tables = (body as { tables?: unknown } | null)?.tables;
|
|
126
|
+
if (!Array.isArray(tables)) return [];
|
|
127
|
+
return tables.filter(
|
|
128
|
+
(t): t is DiscoveredTable =>
|
|
129
|
+
!!t && typeof t === "object" && typeof (t as any).app === "string" &&
|
|
130
|
+
typeof (t as any).model === "string",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Merge warehouse-discovered tables over the manifest-declared resources. The
|
|
136
|
+
* manifest is authoritative — a declared table keeps its authored schema. A
|
|
137
|
+
* table present in the warehouse but absent from the manifest is appended as a
|
|
138
|
+
* `discovered` resource, its fields inferred from the sampled rows, so it shows
|
|
139
|
+
* up in the console even though the compiled schema never mentioned it.
|
|
140
|
+
*/
|
|
141
|
+
export function mergeDiscovered(
|
|
142
|
+
declared: AdminResource[],
|
|
143
|
+
body: unknown,
|
|
144
|
+
): AdminResource[] {
|
|
145
|
+
const have = new Set(declared.map((r) => r.key));
|
|
146
|
+
const extra: AdminResource[] = [];
|
|
147
|
+
for (const t of parseDiscovered(body)) {
|
|
148
|
+
const key = `${t.app}.${t.model}`;
|
|
149
|
+
if (t.declared || have.has(key)) continue;
|
|
150
|
+
have.add(key);
|
|
151
|
+
extra.push({
|
|
152
|
+
app: t.app,
|
|
153
|
+
model: t.model,
|
|
154
|
+
key,
|
|
155
|
+
label: humanizeKey(t.model),
|
|
156
|
+
fields: (t.fields ?? []).map(toAdminField),
|
|
157
|
+
search: [],
|
|
158
|
+
listDisplay: null,
|
|
159
|
+
adminEnabled: t.adminEnabled !== false,
|
|
160
|
+
discovered: true,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
// Declared first (stable), then discovered tables in app/model order.
|
|
164
|
+
extra.sort((a, b) => a.key.localeCompare(b.key));
|
|
165
|
+
return [...declared, ...extra];
|
|
166
|
+
}
|
|
167
|
+
|
|
97
168
|
/** The resources shown in the admin index (admin not disabled). */
|
|
98
169
|
export function adminResources(resources: AdminResource[]): AdminResource[] {
|
|
99
170
|
return resources.filter((r) => r.adminEnabled);
|
package/admin/ui/Sidebar.tsx
CHANGED
|
@@ -80,6 +80,14 @@ export function Sidebar(props: {
|
|
|
80
80
|
>
|
|
81
81
|
<IconTable size={16} className="ras-ico" />
|
|
82
82
|
<span className="ras-nav-text">{r.label}</span>
|
|
83
|
+
{r.discovered ? (
|
|
84
|
+
<span
|
|
85
|
+
className="ras-nav-tag"
|
|
86
|
+
title="Discovered from the warehouse — not in the compiled schema"
|
|
87
|
+
>
|
|
88
|
+
discovered
|
|
89
|
+
</span>
|
|
90
|
+
) : null}
|
|
83
91
|
</a>
|
|
84
92
|
);
|
|
85
93
|
})}
|
package/admin/ui/styles.ts
CHANGED
|
@@ -145,6 +145,19 @@ a { color: inherit; text-decoration: none; }
|
|
|
145
145
|
font-weight: 600;
|
|
146
146
|
}
|
|
147
147
|
.ras-nav-link.is-active .ras-ico { color: var(--color-accent); }
|
|
148
|
+
.ras-nav-tag {
|
|
149
|
+
margin-left: auto;
|
|
150
|
+
font-size: 9px;
|
|
151
|
+
font-weight: 700;
|
|
152
|
+
letter-spacing: 0.04em;
|
|
153
|
+
text-transform: uppercase;
|
|
154
|
+
padding: 2px 6px;
|
|
155
|
+
border-radius: 999px;
|
|
156
|
+
color: var(--color-warning);
|
|
157
|
+
background: color-mix(in srgb, var(--color-warning) 16%, transparent);
|
|
158
|
+
white-space: nowrap;
|
|
159
|
+
}
|
|
160
|
+
.is-collapsed .ras-nav-tag { display: none; }
|
|
148
161
|
.ras-nav-link.is-active::before {
|
|
149
162
|
content: "";
|
|
150
163
|
position: absolute;
|
package/auth/provider.ts
CHANGED
|
@@ -74,10 +74,7 @@ export interface CognitoAuthProviderConfig extends Partial<CognitoAuthConfig> {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
export type CognitoAuthStatus =
|
|
77
|
-
| "
|
|
78
|
-
| "authenticated"
|
|
79
|
-
| "unauthenticated"
|
|
80
|
-
| "error";
|
|
77
|
+
"loading" | "authenticated" | "unauthenticated" | "error";
|
|
81
78
|
|
|
82
79
|
/** What `useCognitoAuth()` returns. */
|
|
83
80
|
export interface CognitoAuthContextValue {
|
|
@@ -178,7 +175,8 @@ export function CognitoAuthProvider({
|
|
|
178
175
|
const keyBase = config.storageKey ?? "rastack.auth";
|
|
179
176
|
const sessionKey = `${keyBase}.session`;
|
|
180
177
|
const pkceKey = `${keyBase}.pkce`;
|
|
181
|
-
const doFetch =
|
|
178
|
+
const doFetch =
|
|
179
|
+
config.fetchImpl ?? (globalThis.fetch?.bind(globalThis) as typeof fetch);
|
|
182
180
|
|
|
183
181
|
const [session, setSession] = useState<CognitoSession | null>(null);
|
|
184
182
|
const [status, setStatus] = useState<CognitoAuthStatus>("loading");
|
|
@@ -242,7 +240,8 @@ export function CognitoAuthProvider({
|
|
|
242
240
|
const redirect = parseRedirect(dom.window.location.search);
|
|
243
241
|
if (redirect.error) {
|
|
244
242
|
clearUrl();
|
|
245
|
-
if (!cancelled)
|
|
243
|
+
if (!cancelled)
|
|
244
|
+
fail(new Error(redirect.errorDescription ?? redirect.error));
|
|
246
245
|
return;
|
|
247
246
|
}
|
|
248
247
|
if (redirect.code) {
|
|
@@ -288,14 +287,17 @@ export function CognitoAuthProvider({
|
|
|
288
287
|
): Promise<CognitoSession> {
|
|
289
288
|
const hosted = requireHostedConfig(config);
|
|
290
289
|
const pending = storage.getItem(pkceKey);
|
|
291
|
-
if (!pending)
|
|
290
|
+
if (!pending)
|
|
291
|
+
throw new Error("No pending PKCE verifier — restart sign-in.");
|
|
292
292
|
const { verifier, state: expectedState } = JSON.parse(pending) as {
|
|
293
293
|
verifier: string;
|
|
294
294
|
state: string;
|
|
295
295
|
};
|
|
296
296
|
storage.removeItem(pkceKey);
|
|
297
297
|
if (expectedState && state && expectedState !== state) {
|
|
298
|
-
throw new Error(
|
|
298
|
+
throw new Error(
|
|
299
|
+
"OAuth state mismatch — possible CSRF, sign-in rejected.",
|
|
300
|
+
);
|
|
299
301
|
}
|
|
300
302
|
const response = await doFetch(tokenEndpoint(hosted), {
|
|
301
303
|
method: "POST",
|
|
@@ -484,3 +486,64 @@ export function useRastackAuthFromCognito(): RastackAuth {
|
|
|
484
486
|
[auth.isAuthenticated, auth.claims],
|
|
485
487
|
);
|
|
486
488
|
}
|
|
489
|
+
|
|
490
|
+
// -- Dev-time Cognito runtime config (real dev pool, opt-in) ------------------
|
|
491
|
+
// `rastack dev` serves the dev environment's Cognito coordinates at
|
|
492
|
+
// `/__rastack/cognito.json` so a locally-running app can sign in against the
|
|
493
|
+
// *real* dev user pool (to obtain real tokens — e.g. for the deployed
|
|
494
|
+
// external-API proxy) instead of the unsigned `mode:"local"` shim. These
|
|
495
|
+
// helpers turn that JSON into a `<CognitoAuthProvider config>`; a developer opts
|
|
496
|
+
// in with `cognitoConfigFromGlobal() ?? { mode: "local" }`.
|
|
497
|
+
|
|
498
|
+
/** The shape `rastack dev` serves at `/__rastack/cognito.json`. */
|
|
499
|
+
export interface RastackCognitoRuntime {
|
|
500
|
+
userPoolId?: string;
|
|
501
|
+
clientId: string;
|
|
502
|
+
/** Hosted UI base URL (`https://…amazoncognito.com`). */
|
|
503
|
+
hostedUiUrl: string;
|
|
504
|
+
region: string;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Map a served runtime config into a `CognitoAuthProviderConfig` for the real
|
|
509
|
+
* Hosted UI flow. `redirectUri` defaults to the current page origin (where
|
|
510
|
+
* `rastack dev` serves the app), so local sign-in round-trips back correctly.
|
|
511
|
+
*/
|
|
512
|
+
export function cognitoRuntimeToConfig(
|
|
513
|
+
runtime: RastackCognitoRuntime,
|
|
514
|
+
opts: { redirectUri?: string } & Partial<CognitoAuthProviderConfig> = {},
|
|
515
|
+
): CognitoAuthProviderConfig {
|
|
516
|
+
const origin = dom.window?.location?.origin as string | undefined;
|
|
517
|
+
const { redirectUri, ...rest } = opts;
|
|
518
|
+
return {
|
|
519
|
+
mode: "hosted-ui",
|
|
520
|
+
domain: runtime.hostedUiUrl,
|
|
521
|
+
clientId: runtime.clientId,
|
|
522
|
+
region: runtime.region,
|
|
523
|
+
userPoolId: runtime.userPoolId,
|
|
524
|
+
redirectUri: redirectUri ?? origin ?? "",
|
|
525
|
+
...rest,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Read the runtime Cognito config `rastack dev` injects as
|
|
531
|
+
* `globalThis.__RASTACK_COGNITO__`, mapped to a provider config — or undefined
|
|
532
|
+
* when none is present (so the caller falls back to `{ mode: "local" }`). Set
|
|
533
|
+
* the global from the served JSON, e.g.:
|
|
534
|
+
*
|
|
535
|
+
* ```html
|
|
536
|
+
* <script>
|
|
537
|
+
* fetch("/__rastack/cognito.json").then(r => r.ok && r.json())
|
|
538
|
+
* .then(c => { if (c) window.__RASTACK_COGNITO__ = c; })
|
|
539
|
+
* </script>
|
|
540
|
+
* ```
|
|
541
|
+
*/
|
|
542
|
+
export function cognitoConfigFromGlobal(
|
|
543
|
+
opts: { redirectUri?: string } & Partial<CognitoAuthProviderConfig> = {},
|
|
544
|
+
): CognitoAuthProviderConfig | undefined {
|
|
545
|
+
const runtime = (dom as { __RASTACK_COGNITO__?: RastackCognitoRuntime })
|
|
546
|
+
.__RASTACK_COGNITO__;
|
|
547
|
+
if (!runtime?.clientId || !runtime?.hostedUiUrl) return undefined;
|
|
548
|
+
return cognitoRuntimeToConfig(runtime, opts);
|
|
549
|
+
}
|