rastack 0.0.16 → 0.0.20
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 +43876 -43013
- package/dist/.prettierrc +7 -7
- package/dist/compile/analyze.js +19 -11
- package/dist/compile/index.js +26 -17
- package/dist/compile/manifest.js +2 -3
- package/dist/compile/openapi.js +5 -8
- package/dist/compile/program.js +19 -10
- package/dist/csv-schema.js +5 -6
- package/dist/define/index.js +2 -2
- package/dist/entity-generation/delete-method.js +8 -10
- package/dist/entity-generation/form.js +80 -82
- package/dist/entity-generation/get-method.js +35 -36
- package/dist/entity-generation/imports.js +24 -20
- package/dist/entity-generation/sync-method.d.ts +1 -1
- package/dist/entity-generation/sync-method.js +45 -46
- package/dist/entity-generation/update-method.js +9 -10
- package/dist/rad-wasm-build.d.ts +1 -1
- package/dist/rad-wasm-build.js +15 -6
- package/dist/rad.js +9 -2
- package/dist/schema/camel-to-pastel.js +1 -2
- package/dist/schema/capitalise-first-letter.js +1 -2
- package/dist/schema/extract-response.js +3 -4
- package/dist/schema/fetch-schema.js +4 -16
- package/dist/schema/remove-non-model-paths.js +1 -2
- package/dist/schema/to-camel-case.js +1 -2
- package/dist/schema/to-pastel-case.js +1 -2
- package/dist/schema-convert.js +22 -33
- package/dist/schema-entities.js +177 -189
- package/dist/schema-fetch.js +46 -63
- package/dist/schema-full.js +23 -36
- package/dist/schema-index.js +16 -27
- package/dist/schema-params.js +11 -22
- package/dist/seed.js +95 -97
- package/hooks/form/form.ts +207 -207
- package/hooks/form/index.ts +8 -8
- package/hooks/form/interfaces.ts +217 -217
- package/hooks/form/structure.ts +39 -39
- package/hooks/form/validate-schema.ts +49 -49
- package/hooks/index.ts +3 -3
- package/hooks/query/api.ts +42 -42
- package/hooks/query/delete.ts +45 -45
- package/hooks/query/fetch.ts +48 -48
- package/hooks/query/index.ts +21 -21
- package/hooks/query/interfaces.ts +111 -111
- package/hooks/query/list.ts +286 -286
- package/hooks/query/update.ts +88 -88
- package/hooks/query/url.ts +31 -31
- package/hooks/real-time/index.ts +1 -1
- package/hooks/real-time/pusher.ts +43 -43
- package/jest.config.cjs +6 -6
- package/package.json +57 -57
- package/provider/index.ts +13 -13
- package/provider/provider.tsx +187 -187
- package/provider/types.ts +114 -114
- package/provider/warehouse.ts +167 -167
- package/provider/wasm.ts +124 -124
- package/runtime.ts +3 -3
- package/src/.prettierrc +7 -7
- package/src/compile/analyze.ts +224 -224
- package/src/compile/index.ts +86 -86
- package/src/compile/manifest.ts +10 -10
- package/src/compile/model.ts +69 -69
- package/src/compile/openapi.ts +266 -266
- package/src/compile/program.ts +40 -40
- package/src/csv-schema.ts +187 -187
- package/src/define/index.ts +162 -162
- package/src/entity-generation/delete-method.ts +37 -37
- package/src/entity-generation/form.ts +169 -169
- package/src/entity-generation/get-method.ts +80 -80
- package/src/entity-generation/imports.ts +110 -110
- package/src/entity-generation/sync-method.ts +222 -222
- package/src/entity-generation/update-method.ts +38 -38
- package/src/index.ts +77 -77
- package/src/rad-compile.ts +88 -88
- package/src/rad-wasm-build.ts +102 -102
- package/src/rad.ts +101 -101
- package/src/scan.ts +89 -89
- package/src/schema/camel-to-pastel.ts +3 -3
- package/src/schema/capitalise-first-letter.ts +3 -3
- package/src/schema/extract-response.ts +38 -38
- package/src/schema/fetch-schema.ts +12 -12
- package/src/schema/remove-non-model-paths.ts +13 -13
- package/src/schema/to-camel-case.ts +7 -7
- package/src/schema/to-pastel-case.ts +6 -6
- package/src/schema-convert.ts +61 -61
- package/src/schema-entities.ts +401 -401
- package/src/schema-fetch.ts +82 -82
- package/src/schema-full.ts +38 -38
- package/src/schema-index.ts +29 -29
- package/src/schema-params.ts +116 -116
- package/src/seed.ts +297 -297
- package/sync/engine.ts +392 -392
- package/sync/hooks.ts +450 -450
- package/sync/index.ts +8 -8
- package/sync/persistence.ts +237 -237
- package/sync/provider.tsx +91 -91
- package/sync/registry.ts +36 -36
- package/sync/store.ts +126 -126
- package/sync/transactions.ts +300 -300
- package/sync/types.ts +94 -94
- package/test/compile.spec.ts +192 -192
- package/test/csv-schema.spec.ts +143 -143
- package/test/schema-entities.spec.ts +688 -688
- package/tsconfig.json +24 -24
- package/types.ts +18 -18
package/provider/warehouse.ts
CHANGED
|
@@ -1,167 +1,167 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Persistence for the in-browser warehouse blob produced by
|
|
3
|
-
* `RadApi.exportWarehouse()`. A single blob (the whole Iceberg warehouse)
|
|
4
|
-
* lives in a one-row IndexedDB store so `local` mode survives reloads; `s3`
|
|
5
|
-
* mode swaps this for a {@link WarehouseSource} over object storage.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const STORE = "warehouse";
|
|
9
|
-
const KEY = "blob";
|
|
10
|
-
|
|
11
|
-
function openDb(name: string): Promise<IDBDatabase> {
|
|
12
|
-
return new Promise((resolve, reject) => {
|
|
13
|
-
const request = indexedDB.open(name, 1);
|
|
14
|
-
request.onupgradeneeded = () => {
|
|
15
|
-
const db = request.result;
|
|
16
|
-
if (!db.objectStoreNames.contains(STORE)) {
|
|
17
|
-
db.createObjectStore(STORE);
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
request.onsuccess = () => resolve(request.result);
|
|
21
|
-
request.onerror = () => reject(request.error);
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export class WarehousePersistence {
|
|
26
|
-
private db: Promise<IDBDatabase> | null;
|
|
27
|
-
|
|
28
|
-
constructor(private databaseName: string) {
|
|
29
|
-
this.db =
|
|
30
|
-
typeof indexedDB !== "undefined" ? openDb(databaseName) : null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
get enabled(): boolean {
|
|
34
|
-
return this.db !== null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async load(): Promise<Uint8Array | null> {
|
|
38
|
-
if (!this.db) return null;
|
|
39
|
-
const db = await this.db;
|
|
40
|
-
return new Promise((resolve, reject) => {
|
|
41
|
-
const tx = db.transaction(STORE, "readonly");
|
|
42
|
-
const request = tx.objectStore(STORE).get(KEY);
|
|
43
|
-
request.onsuccess = () => {
|
|
44
|
-
const value = request.result;
|
|
45
|
-
resolve(value ? new Uint8Array(value) : null);
|
|
46
|
-
};
|
|
47
|
-
request.onerror = () => reject(request.error);
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async save(blob: Uint8Array): Promise<void> {
|
|
52
|
-
if (!this.db) return;
|
|
53
|
-
const db = await this.db;
|
|
54
|
-
await new Promise<void>((resolve, reject) => {
|
|
55
|
-
const tx = db.transaction(STORE, "readwrite");
|
|
56
|
-
// Store a copy of the bytes; the ArrayBuffer is structured-clonable.
|
|
57
|
-
tx.objectStore(STORE).put(blob.slice().buffer, KEY);
|
|
58
|
-
tx.oncomplete = () => resolve();
|
|
59
|
-
tx.onerror = () => reject(tx.error);
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Fetch bytes, or `null` on 404. */
|
|
65
|
-
async function fetchBytes(url: string): Promise<Uint8Array | null> {
|
|
66
|
-
const response = await fetch(url);
|
|
67
|
-
if (!response.ok) {
|
|
68
|
-
if (response.status === 404) return null;
|
|
69
|
-
throw new Error(`failed to fetch ${url} (${response.status})`);
|
|
70
|
-
}
|
|
71
|
-
return new Uint8Array(await response.arrayBuffer());
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** The engine surface the seeder needs. */
|
|
75
|
-
interface Seedable {
|
|
76
|
-
seedFile(key: string, bytes: Uint8Array): Promise<void>;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Seed the engine by walking a committed Iceberg warehouse over HTTP — the
|
|
81
|
-
* database is a *list of Iceberg files*, not a bundled blob. For each resource
|
|
82
|
-
* it follows the Iceberg pointers: `version-hint.text` → the current snapshot
|
|
83
|
-
* metadata → that snapshot's `.parquet` data files, handing each raw file to
|
|
84
|
-
* `engine.seedFile`. This is the same walk the server does over object storage;
|
|
85
|
-
* no directory listing is needed because Iceberg metadata is self-describing.
|
|
86
|
-
*
|
|
87
|
-
* Returns the number of tables seeded.
|
|
88
|
-
*/
|
|
89
|
-
export async function seedFromIceberg(
|
|
90
|
-
engine: Seedable,
|
|
91
|
-
baseUrl: string,
|
|
92
|
-
resources: { app: string; model: string }[],
|
|
93
|
-
): Promise<number> {
|
|
94
|
-
const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
95
|
-
const decoder = new TextDecoder();
|
|
96
|
-
let tables = 0;
|
|
97
|
-
|
|
98
|
-
for (const { app, model } of resources) {
|
|
99
|
-
const prefix = `${app}.db/${model}`;
|
|
100
|
-
const hintKey = `${prefix}/metadata/version-hint.text`;
|
|
101
|
-
const hint = await fetchBytes(base + hintKey);
|
|
102
|
-
if (!hint) continue; // table has no committed data yet
|
|
103
|
-
|
|
104
|
-
await engine.seedFile(hintKey, hint);
|
|
105
|
-
const version = decoder.decode(hint).trim();
|
|
106
|
-
|
|
107
|
-
const metaKey = `${prefix}/metadata/v${version}.metadata.json`;
|
|
108
|
-
const metaBytes = await fetchBytes(base + metaKey);
|
|
109
|
-
if (!metaBytes) continue;
|
|
110
|
-
await engine.seedFile(metaKey, metaBytes);
|
|
111
|
-
|
|
112
|
-
const meta = JSON.parse(decoder.decode(metaBytes)) as { data_files?: string[] };
|
|
113
|
-
for (const rel of meta.data_files || []) {
|
|
114
|
-
const key = `${prefix}/${rel}`;
|
|
115
|
-
const bytes = await fetchBytes(base + key);
|
|
116
|
-
if (bytes) await engine.seedFile(key, bytes);
|
|
117
|
-
}
|
|
118
|
-
tables++;
|
|
119
|
-
}
|
|
120
|
-
return tables;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Read the `{app, model}` list out of a compiled manifest (object or string). */
|
|
124
|
-
export function manifestResources(
|
|
125
|
-
manifest: object | string,
|
|
126
|
-
): { app: string; model: string }[] {
|
|
127
|
-
const parsed = typeof manifest === "string" ? JSON.parse(manifest) : manifest;
|
|
128
|
-
return (parsed?.resources || []).map((r: any) => ({
|
|
129
|
-
app: r.app,
|
|
130
|
-
model: r.model,
|
|
131
|
-
}));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Trigger a browser download of the warehouse blob so it can be committed. */
|
|
135
|
-
export function downloadBlob(blob: Uint8Array, filename: string): void {
|
|
136
|
-
const url = URL.createObjectURL(new Blob([blob.slice().buffer]));
|
|
137
|
-
const a = document.createElement("a");
|
|
138
|
-
a.href = url;
|
|
139
|
-
a.download = filename;
|
|
140
|
-
document.body.appendChild(a);
|
|
141
|
-
a.click();
|
|
142
|
-
a.remove();
|
|
143
|
-
URL.revokeObjectURL(url);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** A trailing debounce that always fires a final call. */
|
|
147
|
-
export function debounce<T extends (...args: any[]) => void>(
|
|
148
|
-
fn: T,
|
|
149
|
-
ms: number,
|
|
150
|
-
): T & { flush: () => void } {
|
|
151
|
-
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
152
|
-
const wrapped = ((...args: any[]) => {
|
|
153
|
-
if (timer) clearTimeout(timer);
|
|
154
|
-
timer = setTimeout(() => {
|
|
155
|
-
timer = null;
|
|
156
|
-
fn(...args);
|
|
157
|
-
}, ms);
|
|
158
|
-
}) as T & { flush: () => void };
|
|
159
|
-
wrapped.flush = () => {
|
|
160
|
-
if (timer) {
|
|
161
|
-
clearTimeout(timer);
|
|
162
|
-
timer = null;
|
|
163
|
-
fn();
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
return wrapped;
|
|
167
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Persistence for the in-browser warehouse blob produced by
|
|
3
|
+
* `RadApi.exportWarehouse()`. A single blob (the whole Iceberg warehouse)
|
|
4
|
+
* lives in a one-row IndexedDB store so `local` mode survives reloads; `s3`
|
|
5
|
+
* mode swaps this for a {@link WarehouseSource} over object storage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const STORE = "warehouse";
|
|
9
|
+
const KEY = "blob";
|
|
10
|
+
|
|
11
|
+
function openDb(name: string): Promise<IDBDatabase> {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const request = indexedDB.open(name, 1);
|
|
14
|
+
request.onupgradeneeded = () => {
|
|
15
|
+
const db = request.result;
|
|
16
|
+
if (!db.objectStoreNames.contains(STORE)) {
|
|
17
|
+
db.createObjectStore(STORE);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
request.onsuccess = () => resolve(request.result);
|
|
21
|
+
request.onerror = () => reject(request.error);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class WarehousePersistence {
|
|
26
|
+
private db: Promise<IDBDatabase> | null;
|
|
27
|
+
|
|
28
|
+
constructor(private databaseName: string) {
|
|
29
|
+
this.db =
|
|
30
|
+
typeof indexedDB !== "undefined" ? openDb(databaseName) : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get enabled(): boolean {
|
|
34
|
+
return this.db !== null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async load(): Promise<Uint8Array | null> {
|
|
38
|
+
if (!this.db) return null;
|
|
39
|
+
const db = await this.db;
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const tx = db.transaction(STORE, "readonly");
|
|
42
|
+
const request = tx.objectStore(STORE).get(KEY);
|
|
43
|
+
request.onsuccess = () => {
|
|
44
|
+
const value = request.result;
|
|
45
|
+
resolve(value ? new Uint8Array(value) : null);
|
|
46
|
+
};
|
|
47
|
+
request.onerror = () => reject(request.error);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async save(blob: Uint8Array): Promise<void> {
|
|
52
|
+
if (!this.db) return;
|
|
53
|
+
const db = await this.db;
|
|
54
|
+
await new Promise<void>((resolve, reject) => {
|
|
55
|
+
const tx = db.transaction(STORE, "readwrite");
|
|
56
|
+
// Store a copy of the bytes; the ArrayBuffer is structured-clonable.
|
|
57
|
+
tx.objectStore(STORE).put(blob.slice().buffer, KEY);
|
|
58
|
+
tx.oncomplete = () => resolve();
|
|
59
|
+
tx.onerror = () => reject(tx.error);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Fetch bytes, or `null` on 404. */
|
|
65
|
+
async function fetchBytes(url: string): Promise<Uint8Array | null> {
|
|
66
|
+
const response = await fetch(url);
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
if (response.status === 404) return null;
|
|
69
|
+
throw new Error(`failed to fetch ${url} (${response.status})`);
|
|
70
|
+
}
|
|
71
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The engine surface the seeder needs. */
|
|
75
|
+
interface Seedable {
|
|
76
|
+
seedFile(key: string, bytes: Uint8Array): Promise<void>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Seed the engine by walking a committed Iceberg warehouse over HTTP — the
|
|
81
|
+
* database is a *list of Iceberg files*, not a bundled blob. For each resource
|
|
82
|
+
* it follows the Iceberg pointers: `version-hint.text` → the current snapshot
|
|
83
|
+
* metadata → that snapshot's `.parquet` data files, handing each raw file to
|
|
84
|
+
* `engine.seedFile`. This is the same walk the server does over object storage;
|
|
85
|
+
* no directory listing is needed because Iceberg metadata is self-describing.
|
|
86
|
+
*
|
|
87
|
+
* Returns the number of tables seeded.
|
|
88
|
+
*/
|
|
89
|
+
export async function seedFromIceberg(
|
|
90
|
+
engine: Seedable,
|
|
91
|
+
baseUrl: string,
|
|
92
|
+
resources: { app: string; model: string }[],
|
|
93
|
+
): Promise<number> {
|
|
94
|
+
const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
95
|
+
const decoder = new TextDecoder();
|
|
96
|
+
let tables = 0;
|
|
97
|
+
|
|
98
|
+
for (const { app, model } of resources) {
|
|
99
|
+
const prefix = `${app}.db/${model}`;
|
|
100
|
+
const hintKey = `${prefix}/metadata/version-hint.text`;
|
|
101
|
+
const hint = await fetchBytes(base + hintKey);
|
|
102
|
+
if (!hint) continue; // table has no committed data yet
|
|
103
|
+
|
|
104
|
+
await engine.seedFile(hintKey, hint);
|
|
105
|
+
const version = decoder.decode(hint).trim();
|
|
106
|
+
|
|
107
|
+
const metaKey = `${prefix}/metadata/v${version}.metadata.json`;
|
|
108
|
+
const metaBytes = await fetchBytes(base + metaKey);
|
|
109
|
+
if (!metaBytes) continue;
|
|
110
|
+
await engine.seedFile(metaKey, metaBytes);
|
|
111
|
+
|
|
112
|
+
const meta = JSON.parse(decoder.decode(metaBytes)) as { data_files?: string[] };
|
|
113
|
+
for (const rel of meta.data_files || []) {
|
|
114
|
+
const key = `${prefix}/${rel}`;
|
|
115
|
+
const bytes = await fetchBytes(base + key);
|
|
116
|
+
if (bytes) await engine.seedFile(key, bytes);
|
|
117
|
+
}
|
|
118
|
+
tables++;
|
|
119
|
+
}
|
|
120
|
+
return tables;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Read the `{app, model}` list out of a compiled manifest (object or string). */
|
|
124
|
+
export function manifestResources(
|
|
125
|
+
manifest: object | string,
|
|
126
|
+
): { app: string; model: string }[] {
|
|
127
|
+
const parsed = typeof manifest === "string" ? JSON.parse(manifest) : manifest;
|
|
128
|
+
return (parsed?.resources || []).map((r: any) => ({
|
|
129
|
+
app: r.app,
|
|
130
|
+
model: r.model,
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Trigger a browser download of the warehouse blob so it can be committed. */
|
|
135
|
+
export function downloadBlob(blob: Uint8Array, filename: string): void {
|
|
136
|
+
const url = URL.createObjectURL(new Blob([blob.slice().buffer]));
|
|
137
|
+
const a = document.createElement("a");
|
|
138
|
+
a.href = url;
|
|
139
|
+
a.download = filename;
|
|
140
|
+
document.body.appendChild(a);
|
|
141
|
+
a.click();
|
|
142
|
+
a.remove();
|
|
143
|
+
URL.revokeObjectURL(url);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** A trailing debounce that always fires a final call. */
|
|
147
|
+
export function debounce<T extends (...args: any[]) => void>(
|
|
148
|
+
fn: T,
|
|
149
|
+
ms: number,
|
|
150
|
+
): T & { flush: () => void } {
|
|
151
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
152
|
+
const wrapped = ((...args: any[]) => {
|
|
153
|
+
if (timer) clearTimeout(timer);
|
|
154
|
+
timer = setTimeout(() => {
|
|
155
|
+
timer = null;
|
|
156
|
+
fn(...args);
|
|
157
|
+
}, ms);
|
|
158
|
+
}) as T & { flush: () => void };
|
|
159
|
+
wrapped.flush = () => {
|
|
160
|
+
if (timer) {
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
timer = null;
|
|
163
|
+
fn();
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
return wrapped;
|
|
167
|
+
}
|
package/provider/wasm.ts
CHANGED
|
@@ -1,124 +1,124 @@
|
|
|
1
|
-
import { AxiosAdapter, AxiosError, AxiosResponse } from "axios";
|
|
2
|
-
import { RadEngine, RadEngineLoader, RadWasmModule } from "./types";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Default WASM loader: import the bundle `rad wasm` generates
|
|
6
|
-
* (`rastack/wasm/rad_wasm.js`) and run its init. The indirection through
|
|
7
|
-
* `new Function` keeps a genuine dynamic `import()` at runtime even when the
|
|
8
|
-
* package is transpiled to CommonJS, so the consumer's bundler resolves and
|
|
9
|
-
* code-splits the `.wasm`.
|
|
10
|
-
*/
|
|
11
|
-
const runtimeImport: (specifier: string) => Promise<any> =
|
|
12
|
-
// eslint-disable-next-line no-new-func
|
|
13
|
-
new Function("s", "return import(s)") as any;
|
|
14
|
-
|
|
15
|
-
export const defaultEngineLoader: RadEngineLoader = async () => {
|
|
16
|
-
const mod: RadWasmModule = await runtimeImport("rastack/wasm/rad_wasm.js");
|
|
17
|
-
const init = mod.default || mod.init;
|
|
18
|
-
if (typeof init === "function") {
|
|
19
|
-
await init();
|
|
20
|
-
}
|
|
21
|
-
return mod;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Instantiate the engine from a loaded module and the compiled schema. The
|
|
26
|
-
* manifest/openapi are passed as JSON strings — the same `schema.rad.json` /
|
|
27
|
-
* `openapi.json` the Rust server reads.
|
|
28
|
-
*/
|
|
29
|
-
export function createEngine(
|
|
30
|
-
mod: RadWasmModule,
|
|
31
|
-
manifest: object | string,
|
|
32
|
-
openapi?: object | string,
|
|
33
|
-
): RadEngine {
|
|
34
|
-
const manifestJson =
|
|
35
|
-
typeof manifest === "string" ? manifest : JSON.stringify(manifest);
|
|
36
|
-
const openapiJson =
|
|
37
|
-
openapi === undefined
|
|
38
|
-
? undefined
|
|
39
|
-
: typeof openapi === "string"
|
|
40
|
-
? openapi
|
|
41
|
-
: JSON.stringify(openapi);
|
|
42
|
-
return new mod.RadApi(manifestJson, openapiJson);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const MUTATING = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* An axios adapter that dispatches requests into the in-browser WASM engine
|
|
49
|
-
* instead of over the network. Translates an axios request into the engine's
|
|
50
|
-
* `handle(method, path, query, body)` and maps the `{ status, body }` result
|
|
51
|
-
* back to an `AxiosResponse` — rejecting on non-2xx exactly as HTTP axios does,
|
|
52
|
-
* so `react-query` error handling is unchanged.
|
|
53
|
-
*
|
|
54
|
-
* `onWrite` fires after each successful mutation so the provider can debounce a
|
|
55
|
-
* persist of the warehouse.
|
|
56
|
-
*/
|
|
57
|
-
export function createWasmAdapter(
|
|
58
|
-
engine: RadEngine,
|
|
59
|
-
onWrite?: () => void,
|
|
60
|
-
): AxiosAdapter {
|
|
61
|
-
return async (config: any): Promise<AxiosResponse> => {
|
|
62
|
-
const method = String(config.method || "get").toUpperCase();
|
|
63
|
-
|
|
64
|
-
const rawUrl = config.url || "";
|
|
65
|
-
const [rawPath, rawQuery] = rawUrl.split("?");
|
|
66
|
-
// Strip any origin so only the `/api/...` path reaches the engine.
|
|
67
|
-
const path = rawPath.replace(/^https?:\/\/[^/]+/i, "") || "/";
|
|
68
|
-
|
|
69
|
-
const query: Record<string, string> = {};
|
|
70
|
-
if (rawQuery) {
|
|
71
|
-
new URLSearchParams(rawQuery).forEach((value, key) => {
|
|
72
|
-
query[key] = value;
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
if (config.params && typeof config.params === "object") {
|
|
76
|
-
for (const [key, value] of Object.entries(config.params)) {
|
|
77
|
-
if (value !== undefined && value !== null) query[key] = String(value);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// axios has already run transformRequest, so JSON bodies arrive as strings.
|
|
82
|
-
let body: unknown;
|
|
83
|
-
if (typeof config.data === "string" && config.data.length > 0) {
|
|
84
|
-
try {
|
|
85
|
-
body = JSON.parse(config.data);
|
|
86
|
-
} catch {
|
|
87
|
-
body = config.data;
|
|
88
|
-
}
|
|
89
|
-
} else if (config.data && typeof config.data === "object") {
|
|
90
|
-
body = config.data;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const result = await engine.handle(
|
|
94
|
-
method,
|
|
95
|
-
path,
|
|
96
|
-
Object.keys(query).length ? query : undefined,
|
|
97
|
-
body,
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
const response: AxiosResponse = {
|
|
101
|
-
data: result.body,
|
|
102
|
-
status: result.status,
|
|
103
|
-
statusText: String(result.status),
|
|
104
|
-
headers: {},
|
|
105
|
-
config,
|
|
106
|
-
request: null,
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const validate =
|
|
110
|
-
config.validateStatus || ((s: number) => s >= 200 && s < 300);
|
|
111
|
-
if (!validate(result.status)) {
|
|
112
|
-
throw new AxiosError(
|
|
113
|
-
`Request failed with status code ${result.status}`,
|
|
114
|
-
result.status >= 500 ? "ERR_BAD_RESPONSE" : "ERR_BAD_REQUEST",
|
|
115
|
-
config,
|
|
116
|
-
null,
|
|
117
|
-
response,
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (MUTATING.has(method) && onWrite) onWrite();
|
|
122
|
-
return response;
|
|
123
|
-
};
|
|
124
|
-
}
|
|
1
|
+
import { AxiosAdapter, AxiosError, AxiosResponse } from "axios";
|
|
2
|
+
import { RadEngine, RadEngineLoader, RadWasmModule } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default WASM loader: import the bundle `rad wasm` generates
|
|
6
|
+
* (`rastack/wasm/rad_wasm.js`) and run its init. The indirection through
|
|
7
|
+
* `new Function` keeps a genuine dynamic `import()` at runtime even when the
|
|
8
|
+
* package is transpiled to CommonJS, so the consumer's bundler resolves and
|
|
9
|
+
* code-splits the `.wasm`.
|
|
10
|
+
*/
|
|
11
|
+
const runtimeImport: (specifier: string) => Promise<any> =
|
|
12
|
+
// eslint-disable-next-line no-new-func
|
|
13
|
+
new Function("s", "return import(s)") as any;
|
|
14
|
+
|
|
15
|
+
export const defaultEngineLoader: RadEngineLoader = async () => {
|
|
16
|
+
const mod: RadWasmModule = await runtimeImport("rastack/wasm/rad_wasm.js");
|
|
17
|
+
const init = mod.default || mod.init;
|
|
18
|
+
if (typeof init === "function") {
|
|
19
|
+
await init();
|
|
20
|
+
}
|
|
21
|
+
return mod;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Instantiate the engine from a loaded module and the compiled schema. The
|
|
26
|
+
* manifest/openapi are passed as JSON strings — the same `schema.rad.json` /
|
|
27
|
+
* `openapi.json` the Rust server reads.
|
|
28
|
+
*/
|
|
29
|
+
export function createEngine(
|
|
30
|
+
mod: RadWasmModule,
|
|
31
|
+
manifest: object | string,
|
|
32
|
+
openapi?: object | string,
|
|
33
|
+
): RadEngine {
|
|
34
|
+
const manifestJson =
|
|
35
|
+
typeof manifest === "string" ? manifest : JSON.stringify(manifest);
|
|
36
|
+
const openapiJson =
|
|
37
|
+
openapi === undefined
|
|
38
|
+
? undefined
|
|
39
|
+
: typeof openapi === "string"
|
|
40
|
+
? openapi
|
|
41
|
+
: JSON.stringify(openapi);
|
|
42
|
+
return new mod.RadApi(manifestJson, openapiJson);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const MUTATING = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* An axios adapter that dispatches requests into the in-browser WASM engine
|
|
49
|
+
* instead of over the network. Translates an axios request into the engine's
|
|
50
|
+
* `handle(method, path, query, body)` and maps the `{ status, body }` result
|
|
51
|
+
* back to an `AxiosResponse` — rejecting on non-2xx exactly as HTTP axios does,
|
|
52
|
+
* so `react-query` error handling is unchanged.
|
|
53
|
+
*
|
|
54
|
+
* `onWrite` fires after each successful mutation so the provider can debounce a
|
|
55
|
+
* persist of the warehouse.
|
|
56
|
+
*/
|
|
57
|
+
export function createWasmAdapter(
|
|
58
|
+
engine: RadEngine,
|
|
59
|
+
onWrite?: () => void,
|
|
60
|
+
): AxiosAdapter {
|
|
61
|
+
return async (config: any): Promise<AxiosResponse> => {
|
|
62
|
+
const method = String(config.method || "get").toUpperCase();
|
|
63
|
+
|
|
64
|
+
const rawUrl = config.url || "";
|
|
65
|
+
const [rawPath, rawQuery] = rawUrl.split("?");
|
|
66
|
+
// Strip any origin so only the `/api/...` path reaches the engine.
|
|
67
|
+
const path = rawPath.replace(/^https?:\/\/[^/]+/i, "") || "/";
|
|
68
|
+
|
|
69
|
+
const query: Record<string, string> = {};
|
|
70
|
+
if (rawQuery) {
|
|
71
|
+
new URLSearchParams(rawQuery).forEach((value, key) => {
|
|
72
|
+
query[key] = value;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (config.params && typeof config.params === "object") {
|
|
76
|
+
for (const [key, value] of Object.entries(config.params)) {
|
|
77
|
+
if (value !== undefined && value !== null) query[key] = String(value);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// axios has already run transformRequest, so JSON bodies arrive as strings.
|
|
82
|
+
let body: unknown;
|
|
83
|
+
if (typeof config.data === "string" && config.data.length > 0) {
|
|
84
|
+
try {
|
|
85
|
+
body = JSON.parse(config.data);
|
|
86
|
+
} catch {
|
|
87
|
+
body = config.data;
|
|
88
|
+
}
|
|
89
|
+
} else if (config.data && typeof config.data === "object") {
|
|
90
|
+
body = config.data;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const result = await engine.handle(
|
|
94
|
+
method,
|
|
95
|
+
path,
|
|
96
|
+
Object.keys(query).length ? query : undefined,
|
|
97
|
+
body,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const response: AxiosResponse = {
|
|
101
|
+
data: result.body,
|
|
102
|
+
status: result.status,
|
|
103
|
+
statusText: String(result.status),
|
|
104
|
+
headers: {},
|
|
105
|
+
config,
|
|
106
|
+
request: null,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const validate =
|
|
110
|
+
config.validateStatus || ((s: number) => s >= 200 && s < 300);
|
|
111
|
+
if (!validate(result.status)) {
|
|
112
|
+
throw new AxiosError(
|
|
113
|
+
`Request failed with status code ${result.status}`,
|
|
114
|
+
result.status >= 500 ? "ERR_BAD_RESPONSE" : "ERR_BAD_REQUEST",
|
|
115
|
+
config,
|
|
116
|
+
null,
|
|
117
|
+
response,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (MUTATING.has(method) && onWrite) onWrite();
|
|
122
|
+
return response;
|
|
123
|
+
};
|
|
124
|
+
}
|
package/runtime.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from "./hooks";
|
|
2
|
-
export * from "./types";
|
|
3
|
-
export * from "./provider";
|
|
1
|
+
export * from "./hooks";
|
|
2
|
+
export * from "./types";
|
|
3
|
+
export * from "./provider";
|
package/src/.prettierrc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
{
|
|
2
|
-
"tabWidth": 2,
|
|
3
|
-
"semi": true,
|
|
4
|
-
"bracketSpacing": true,
|
|
5
|
-
"arrowParens": "always",
|
|
6
|
-
"printWidth": 80
|
|
7
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"tabWidth": 2,
|
|
3
|
+
"semi": true,
|
|
4
|
+
"bracketSpacing": true,
|
|
5
|
+
"arrowParens": "always",
|
|
6
|
+
"printWidth": 80
|
|
7
|
+
}
|