rastack 0.0.60 → 0.0.62

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 CHANGED
@@ -2,6 +2,30 @@
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.62](https://github.com/theserverkid/reactapistack/compare/v0.0.61...v0.0.62) (2026-07-19)
6
+
7
+
8
+ ### Features
9
+
10
+ * **admin:** Iceberg table discovery — surface warehouse tables the manifest doesn't declare ([9a3b687](https://github.com/theserverkid/reactapistack/commit/9a3b68768878c545e3d1c4c7a95bc3ba75c94856))
11
+ * **catalog:** Iceberg REST Catalog so DuckDB/PyIceberg can write the warehouse (phase 2) ([bf402eb](https://github.com/theserverkid/reactapistack/commit/bf402eb9d2b5ff99ac8074d55fc132a9f26466ab))
12
+ * **store:** make the native engine Iceberg-native — read rows by walking the manifests ([b15fe8b](https://github.com/theserverkid/reactapistack/commit/b15fe8bdbb7053ed4fcadd7dc750c2a4ce313878))
13
+ * **store:** publish real Apache Iceberg tables so DuckDB/PyIceberg can read the warehouse (phase 1) ([13835cd](https://github.com/theserverkid/reactapistack/commit/13835cdeab8f2a48360947ac767808e43d5bd2f8))
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * **ci:** avoid raw f-string SQL in verify_iceberg.py (Semgrep SAST) ([cffd674](https://github.com/theserverkid/reactapistack/commit/cffd6742ddb7ec04ed8f7b65c293c8de5eca1060))
19
+
20
+ ### [0.0.61](https://github.com/theserverkid/reactapistack/compare/v0.0.60...v0.0.61) (2026-07-19)
21
+
22
+
23
+ ### Features
24
+
25
+ * **engine:** activate typed Parquet columns from the manifest ([45711c3](https://github.com/theserverkid/reactapistack/commit/45711c3bfc9c69cc2e841e1d8a31bc38728e58de))
26
+ * **engine:** implement local-first sync server endpoints ([10d7e86](https://github.com/theserverkid/reactapistack/commit/10d7e861e8d574e0bb059ef3e3ce3538069d818f))
27
+ * **store:** real typed per-field Parquet columns (Iceberg encoding) ([12d6f1f](https://github.com/theserverkid/reactapistack/commit/12d6f1fbe3d6326f54c79c33a3c9ea576d389f8f))
28
+
5
29
  ### [0.0.60](https://github.com/theserverkid/reactapistack/compare/v0.0.59...v0.0.60) (2026-07-18)
6
30
 
7
31
 
package/README.md CHANGED
@@ -15,7 +15,7 @@ S3 at the bottom, with a stateless Rust API in between.
15
15
  └───────────────────────▲─────────────────────┘
16
16
  │ your types, resolved via the manifest
17
17
  ┌───────────────────────┴─────────────────────┐
18
- │ TypeScript resources → `rastack compile` │ API EXPOSURE
18
+ │ TypeScript resources → compile step │ API EXPOSURE
19
19
  │ FK inferred from types → manifest+OpenAPI │ SURFACE
20
20
  └───────────────────────▲─────────────────────┘
21
21
  │ schema.rastack.json (manifest)
@@ -41,7 +41,7 @@ S3 at the bottom, with a stateless Rust API in between.
41
41
 
42
42
  | Surface | What it is | Package(s) |
43
43
  | --- | --- | --- |
44
- | **API exposure** | TypeScript resources → `rastack compile` (FK inferred from types) → manifest + OpenAPI → pass your type to `useData` / `useForm` (typed TanStack Query hooks, no codegen) | the repo root, published as `rastack` (npm) — `components/`, `hooks/`, `theme/`, … |
44
+ | **API exposure** | TypeScript resources → in-process compile (FK inferred from types) → manifest + OpenAPI → pass your type to `useData` / `useForm` (typed TanStack Query hooks, no codegen) | the repo root, published as `rastack` (npm) — `components/`, `hooks/`, `theme/`, … |
45
45
  | **Database storage** | A stateless Rust API that stores every model as an Apache Iceberg table on object storage — snapshots, ACID on S3, columnar Parquet | `engine/` (`serve/rastack-server`) |
46
46
  | **DevOps** | Serverless Rust on AWS Lambda + S3: stateless compute, state in object storage, infrastructure-as-code, scale-to-zero — driven by `rastack init` / `deploy` / `ci` | `deploy/cloudformation/`, `examples/serverless/` |
47
47
 
@@ -81,9 +81,9 @@ export const Terminal = resource("airports", "terminal", {
81
81
  ```
82
82
 
83
83
  ```bash
84
- rastack compile resources .rastack # TS → schema.rastack.json + openapi.json (FK detected)
85
- rastack dev # the app + admin, in-browser over the WASM engine
86
- rastack admin # Django-admin-style DB browser
84
+ # No compile step to run — it happens in-process. Author your resources, then:
85
+ rastack dev # compiles + serves your app in-browser over the WASM engine
86
+ rastack admin # Django-admin-style DB browser over the same engine
87
87
  ```
88
88
 
89
89
  ```tsx
@@ -106,22 +106,26 @@ the importer, the server, and the in-browser WASM engine alike.
106
106
 
107
107
  ## Ship it — one command per stage
108
108
 
109
- The DevOps surface is a CLI too. A new repo is scaffolded, configured, and
110
- deployed with three commands, and GitHub Actions stays a one-liner because the
111
- pipeline lives in the CLI, not the YAML:
109
+ The DevOps surface is a CLI too. A new repo goes from nothing to deployed in
110
+ three commands, and GitHub Actions stays a one-liner because the pipeline lives
111
+ in the CLI, not the YAML:
112
112
 
113
113
  ```bash
114
- rastack init # template .github/workflows + .rastack/deploy.json
115
- rastack deploy # configure the deployment; print the stack launch command
114
+ rastack bootstrap # once per AWS account+region: buckets + GitHub OIDC
115
+ rastack init # per app: scaffold .github/workflows + .rastack/deploy.json AND stand the backend up
116
116
  rastack ci deploy # what CI runs: build the arm64 Lambda, bake the JWKS, ship the code
117
117
  ```
118
118
 
119
- `init` scaffolds the base GitHub Actions and a deploy config (defaulting the
120
- GitHub coordinates from your `origin` remote); `deploy` fills that config in and
121
- prints the one-time `aws cloudformation deploy` plus the repo Variables to wire;
122
- `ci` runs the build/deploy pipeline so a workflow is just
123
- `run: rastack ci deploy` (`rastack ci --plan deploy` prints every step). Full
124
- guide: **[docs/serverless-deploy.md](docs/serverless-deploy.md)**.
119
+ `bootstrap` stands the tier-0 account stack up once (the template-hosting
120
+ buckets + the GitHub OIDC provider), decoupled from any app. `init` is per-app:
121
+ it scaffolds the base GitHub Actions and a deploy config (defaulting the GitHub
122
+ coordinates from your `origin` remote), then stands the serverless backend up —
123
+ deploying `app.yaml`, reading the stack Outputs back into the config, and wiring
124
+ the GitHub repo Variables so the first push deploys. From then on infrastructure
125
+ is fixed and CI only ships code: `rastack ci deploy` builds and runs
126
+ `update-function-code` (`rastack deploy` is an alias for it), so a workflow step
127
+ is just `run: rastack ci deploy` (`rastack ci --plan deploy` prints every step).
128
+ Full guide: **[docs/serverless-deploy.md](docs/serverless-deploy.md)**.
125
129
 
126
130
  ## Run the whole API in the browser
127
131
 
@@ -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, resources } = props;
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((f) => ({
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);
@@ -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
  })}
@@ -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;