@colixsystems/widget-sdk 0.16.0 → 0.17.0
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/README.md +9 -2
- package/dist/contract.cjs +31 -3
- package/dist/contract.js +28 -3
- package/dist/hooks.js +75 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,7 +6,14 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
6
6
|
|
|
7
7
|
## Status
|
|
8
8
|
|
|
9
|
-
`v0.
|
|
9
|
+
`v0.17.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
|
|
10
|
+
|
|
11
|
+
### What's new in 0.17.0
|
|
12
|
+
|
|
13
|
+
REQ-WBLT-FORMBUILDER — a runtime schema resolver so widgets can render by column type.
|
|
14
|
+
|
|
15
|
+
- **`useDatastoreSchema(tableId)` is wired** + the new `WidgetContext.datastore.schema` slice. Returns `{ schema, loading, error, refetch }` where `schema` is `{ id, name, columns: [{ id, name, dataType, required, relationType, targetTableId, isIdentification }] }` (`null` until loaded). It reads the **existing** ACL-gated `GET /api/v1/tables/:id` — structure only, never row data — so a public-grant table resolves for anonymous visitors exactly like a record read. Use it to resolve a stored `columnId` to its column name / dataType / relation target at runtime (the built-in Form Builder uses it to render an input per column type). Reads need the `datastore.read:<table>` scope. Backed by a new `datastore.schema(tableId)` host facade (web: `widgetHostDatastore`; native: `hostDatastore` in the export's `widgetHost.js`). Additive.
|
|
16
|
+
- **`CONTRACT.version` → `1.7.0`** (additive: one new hook, one new `datastore.schema` context field). No existing export changed signature.
|
|
10
17
|
|
|
11
18
|
### What's new in 0.16.0
|
|
12
19
|
|
|
@@ -125,7 +132,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
|
|
|
125
132
|
|
|
126
133
|
- `defineWidget({ manifest, component })` — validates the manifest and produces a widget module the host can register.
|
|
127
134
|
- `validateManifest(m)` / `validatePropertySchema(s)` / `validateProps(schema, props)` — shape validation; no third-party deps.
|
|
128
|
-
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useFile`, `useWidgetEvent`, `usePayments`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useChildRenderer`, `useClipboard`, `useToast` — hooks that read from the host-provided `WidgetContext` (or, for `useClipboard`, the platform clipboard API directly). `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope. `useUsers(query?)` returns `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` and requires `users.read:*` (mutations also need `users.write:*`); rejections are a `DirectoryError`. `useGroups(query?)` returns `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` and requires `groups.read:*` (mutations also need `groups.write:*`). `usePayments()` returns `{ requestPayment, getPayment }` and requires the `payments.charge:appUser` scope; `requestPayment(...)` rejects with a `PaymentError`. `useUser()` returns the active end-user identity `{ id, email, displayName, roles, groupIds }` (`id` is `null` for anonymous / preview). `useNavigation()` returns `{ goTo, goBack, push, replace, back, currentRoute }` for internal page navigation — for external URLs use the `Linking` primitive (`Linking.openURL(url)`). `useDatastoreRecord(tableId, recordId)` returns `{ data, loading, error, refetch }` for a single record (data is one row or null). `useFile(fileId)` returns `{ url, file, loading, error, refetch }` — the `url` is an absolute URL composed against the host's API base. `useChildRenderer()` returns `{ renderNode(node) }` — container widgets call it to render arbitrary child page-tree nodes (prefer the `WidgetTree` component for the common case).
|
|
135
|
+
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useFile`, `useWidgetEvent`, `usePayments`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useChildRenderer`, `useClipboard`, `useToast` — hooks that read from the host-provided `WidgetContext` (or, for `useClipboard`, the platform clipboard API directly). `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope. `useUsers(query?)` returns `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` and requires `users.read:*` (mutations also need `users.write:*`); rejections are a `DirectoryError`. `useGroups(query?)` returns `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` and requires `groups.read:*` (mutations also need `groups.write:*`). `usePayments()` returns `{ requestPayment, getPayment }` and requires the `payments.charge:appUser` scope; `requestPayment(...)` rejects with a `PaymentError`. `useUser()` returns the active end-user identity `{ id, email, displayName, roles, groupIds }` (`id` is `null` for anonymous / preview). `useNavigation()` returns `{ goTo, goBack, push, replace, back, currentRoute }` for internal page navigation — for external URLs use the `Linking` primitive (`Linking.openURL(url)`). `useDatastoreRecord(tableId, recordId)` returns `{ data, loading, error, refetch }` for a single record (data is one row or null). `useDatastoreSchema(tableId)` returns `{ schema, loading, error, refetch }` where `schema` is `{ id, name, columns: [{ id, name, dataType, required, relationType, targetTableId, isIdentification }] }` (structure only, no row data) — use it to resolve a stored `columnId` to its column type at runtime; requires the `datastore.read:<table>` scope. `useFile(fileId)` returns `{ url, file, loading, error, refetch }` — the `url` is an absolute URL composed against the host's API base. `useChildRenderer()` returns `{ renderNode(node) }` — container widgets call it to render arbitrary child page-tree nodes (prefer the `WidgetTree` component for the common case).
|
|
129
136
|
- `WidgetTree({ node })` — component that renders an author-authored child node through the host's renderer; used by Tabs / Card / custom containers to host arbitrary child widgets.
|
|
130
137
|
- `Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`, `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, `StyleSheet`, `Linking`, `Icon`, `DateTimePicker` — re-exported from `react-native` (the RN primitives) or implemented in the SDK (`Icon` wraps `lucide-react-native`, `DateTimePicker` wraps `@react-native-community/datetimepicker`). The web build aliases `react-native` to `react-native-web` so widgets render in the browser without any per-platform code; the exported Expo app's Metro bundler resolves the real `react-native` library. `Linking` is a static API (`Linking.openURL(url)`) — use it for external URLs, and use `useNavigation().goTo(pageId)` for internal page navigation. See https://reactnative.dev/docs/ for per-component props.
|
|
131
138
|
- `WidgetContextProvider` — React context provider that the host (Studio, Player, exported app) wraps widgets with.
|
package/dist/contract.cjs
CHANGED
|
@@ -130,6 +130,28 @@ const HOOKS = [
|
|
|
130
130
|
requiredContextSlice: ["datastore.records"],
|
|
131
131
|
scopes: ["datastore.read:*"],
|
|
132
132
|
},
|
|
133
|
+
{
|
|
134
|
+
name: "useDatastoreSchema",
|
|
135
|
+
signature: "useDatastoreSchema(tableId)",
|
|
136
|
+
description:
|
|
137
|
+
"Reads a table's structural schema — its columns and their types — so a " +
|
|
138
|
+
"widget can resolve a stored columnId to the column's name / dataType / " +
|
|
139
|
+
"relation target at runtime (e.g. a form that renders inputs by column " +
|
|
140
|
+
"type). Returns { schema, loading, error, refetch } where schema is " +
|
|
141
|
+
"{ id, name, columns: [{ id, name, dataType, required, relationType, " +
|
|
142
|
+
"targetTableId, isIdentification }] } (null until loaded). Reads only the " +
|
|
143
|
+
"structure, never row data; gated by the same datastore.read ACL as " +
|
|
144
|
+
"record reads, so a public-grant table resolves for anonymous visitors.",
|
|
145
|
+
returnShape: {
|
|
146
|
+
schema:
|
|
147
|
+
"{ id, name, columns: [{ id, name, dataType, required, relationType, targetTableId, isIdentification }] } | null",
|
|
148
|
+
loading: "boolean",
|
|
149
|
+
error: "DatastoreError | null",
|
|
150
|
+
refetch: "() => Promise<void>",
|
|
151
|
+
},
|
|
152
|
+
requiredContextSlice: ["datastore.schema"],
|
|
153
|
+
scopes: ["datastore.read:<table>"],
|
|
154
|
+
},
|
|
133
155
|
{
|
|
134
156
|
name: "useDatastoreMutation",
|
|
135
157
|
signature: "useDatastoreMutation(tableId)",
|
|
@@ -524,9 +546,9 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
524
546
|
},
|
|
525
547
|
datastore: {
|
|
526
548
|
description:
|
|
527
|
-
"{ records(table) -> { list(query?), get(id), create(values), update(id, values), delete(id) } }.",
|
|
549
|
+
"{ records(table) -> { list(query?), get(id), create(values), update(id, values), delete(id) }, schema(table) -> Promise<{ id, name, columns: [...] }> }. `records` backs the query/record/mutation hooks; `schema` backs useDatastoreSchema() and resolves a table's column structure (no row data).",
|
|
528
550
|
required: true,
|
|
529
|
-
fields: { records: "function" },
|
|
551
|
+
fields: { records: "function", schema: "function" },
|
|
530
552
|
},
|
|
531
553
|
directory: {
|
|
532
554
|
description:
|
|
@@ -837,7 +859,13 @@ const CONTRACT = deepFreeze({
|
|
|
837
859
|
// so the tenant's Theme Settings "Secondary Color" flows through
|
|
838
860
|
// `useTheme().colors.secondary` (Button secondary variant + any widget
|
|
839
861
|
// that wants the brand's second accent).
|
|
840
|
-
|
|
862
|
+
//
|
|
863
|
+
// 1.7.0: additive — new `useDatastoreSchema(tableId)` hook + the
|
|
864
|
+
// `datastore.schema` host-context slice it reads. Lets a widget resolve a
|
|
865
|
+
// stored columnId to its column name / dataType / relation target at
|
|
866
|
+
// runtime (Form Builder renders inputs by column type). Reads the existing
|
|
867
|
+
// ACL-gated `GET /tables/:id` — structure only, no row data.
|
|
868
|
+
version: "1.7.0",
|
|
841
869
|
hooks: HOOKS,
|
|
842
870
|
primitives: PRIMITIVES,
|
|
843
871
|
manifestSchema: MANIFEST_SCHEMA,
|
package/dist/contract.js
CHANGED
|
@@ -130,6 +130,28 @@ const HOOKS = [
|
|
|
130
130
|
requiredContextSlice: ["datastore.records"],
|
|
131
131
|
scopes: ["datastore.read:*"],
|
|
132
132
|
},
|
|
133
|
+
{
|
|
134
|
+
name: "useDatastoreSchema",
|
|
135
|
+
signature: "useDatastoreSchema(tableId)",
|
|
136
|
+
description:
|
|
137
|
+
"Reads a table's structural schema — its columns and their types — so a " +
|
|
138
|
+
"widget can resolve a stored columnId to the column's name / dataType / " +
|
|
139
|
+
"relation target at runtime (e.g. a form that renders inputs by column " +
|
|
140
|
+
"type). Returns { schema, loading, error, refetch } where schema is " +
|
|
141
|
+
"{ id, name, columns: [{ id, name, dataType, required, relationType, " +
|
|
142
|
+
"targetTableId, isIdentification }] } (null until loaded). Reads only the " +
|
|
143
|
+
"structure, never row data; gated by the same datastore.read ACL as " +
|
|
144
|
+
"record reads, so a public-grant table resolves for anonymous visitors.",
|
|
145
|
+
returnShape: {
|
|
146
|
+
schema:
|
|
147
|
+
"{ id, name, columns: [{ id, name, dataType, required, relationType, targetTableId, isIdentification }] } | null",
|
|
148
|
+
loading: "boolean",
|
|
149
|
+
error: "DatastoreError | null",
|
|
150
|
+
refetch: "() => Promise<void>",
|
|
151
|
+
},
|
|
152
|
+
requiredContextSlice: ["datastore.schema"],
|
|
153
|
+
scopes: ["datastore.read:<table>"],
|
|
154
|
+
},
|
|
133
155
|
{
|
|
134
156
|
name: "useDatastoreMutation",
|
|
135
157
|
signature: "useDatastoreMutation(tableId)",
|
|
@@ -518,9 +540,9 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
518
540
|
},
|
|
519
541
|
datastore: {
|
|
520
542
|
description:
|
|
521
|
-
"{ records(table) -> { list(query?), get(id), create(values), update(id, values), delete(id) } }.",
|
|
543
|
+
"{ records(table) -> { list(query?), get(id), create(values), update(id, values), delete(id) }, schema(table) -> Promise<{ id, name, columns: [...] }> }. `records` backs the query/record/mutation hooks; `schema` backs useDatastoreSchema() and resolves a table's column structure (no row data).",
|
|
522
544
|
required: true,
|
|
523
|
-
fields: { records: "function" },
|
|
545
|
+
fields: { records: "function", schema: "function" },
|
|
524
546
|
},
|
|
525
547
|
directory: {
|
|
526
548
|
description:
|
|
@@ -788,7 +810,10 @@ function deepFreeze(value) {
|
|
|
788
810
|
}
|
|
789
811
|
|
|
790
812
|
const CONTRACT = deepFreeze({
|
|
791
|
-
|
|
813
|
+
// 1.7.0: additive — new useDatastoreSchema(tableId) hook + the
|
|
814
|
+
// datastore.schema host-context slice it reads (resolves a table's column
|
|
815
|
+
// structure at runtime via the existing ACL-gated GET /tables/:id).
|
|
816
|
+
version: "1.7.0",
|
|
792
817
|
hooks: HOOKS,
|
|
793
818
|
primitives: PRIMITIVES,
|
|
794
819
|
manifestSchema: MANIFEST_SCHEMA,
|
package/dist/hooks.js
CHANGED
|
@@ -297,6 +297,81 @@ export function useDatastoreRecord(table, recordId) {
|
|
|
297
297
|
return { data, loading, error, refetch };
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
/**
|
|
301
|
+
* Stateful table-schema resolver hook. Returns { schema, loading, error,
|
|
302
|
+
* refetch } where `schema` is `{ id, name, columns: [{ id, name, dataType,
|
|
303
|
+
* required, relationType, targetTableId, isIdentification }] }` (`null` until
|
|
304
|
+
* loaded).
|
|
305
|
+
*
|
|
306
|
+
* The host's datastore client exposes `schema(table)` which resolves to the
|
|
307
|
+
* table's structural metadata — columns and their types, NOT row data. Use it
|
|
308
|
+
* to resolve a stored `columnId` to its column name / dataType / relation
|
|
309
|
+
* target at runtime (e.g. a form that renders an input per column type). It
|
|
310
|
+
* reads the existing ACL-gated `GET /tables/:id`, so a public-grant table
|
|
311
|
+
* resolves for anonymous visitors exactly like a record read.
|
|
312
|
+
*
|
|
313
|
+
* When `tableId` is falsy (the author hasn't bound a `tableRef` yet) the hook
|
|
314
|
+
* collapses to { schema: null, loading: false, error: null, refetch } without
|
|
315
|
+
* a network round-trip. A 404 (table absent / not readable / cross-tenant)
|
|
316
|
+
* surfaces as a DatastoreError with `code: "NOT_FOUND"`.
|
|
317
|
+
*/
|
|
318
|
+
export function useDatastoreSchema(tableId) {
|
|
319
|
+
const ctx = useWidgetContextOrThrow("useDatastoreSchema");
|
|
320
|
+
if (!ctx.datastore || typeof ctx.datastore.schema !== "function") {
|
|
321
|
+
throw new Error(
|
|
322
|
+
"useDatastoreSchema: host did not inject a datastore schema client",
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const ready = Boolean(tableId);
|
|
326
|
+
const [schema, setSchema] = useState(null);
|
|
327
|
+
const [loading, setLoading] = useState(ready);
|
|
328
|
+
const [error, setError] = useState(null);
|
|
329
|
+
|
|
330
|
+
// Same ref discipline as useDatastoreRecord — `ctx` is a fresh object
|
|
331
|
+
// identity on every host render, so we hold the live inputs in refs to
|
|
332
|
+
// keep `refetch` a stable callback.
|
|
333
|
+
const tableIdRef = useRef(tableId);
|
|
334
|
+
const schemaFnRef = useRef(ctx.datastore.schema);
|
|
335
|
+
tableIdRef.current = tableId;
|
|
336
|
+
schemaFnRef.current = ctx.datastore.schema;
|
|
337
|
+
|
|
338
|
+
const runRef = useRef(0);
|
|
339
|
+
|
|
340
|
+
const doFetch = useCallback(async () => {
|
|
341
|
+
const myRun = ++runRef.current;
|
|
342
|
+
const t = tableIdRef.current;
|
|
343
|
+
if (!t) {
|
|
344
|
+
setLoading(false);
|
|
345
|
+
setError(null);
|
|
346
|
+
setSchema(null);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
setLoading(true);
|
|
350
|
+
setError(null);
|
|
351
|
+
try {
|
|
352
|
+
const result = await schemaFnRef.current(t);
|
|
353
|
+
if (runRef.current !== myRun) return;
|
|
354
|
+
setSchema(result || null);
|
|
355
|
+
setLoading(false);
|
|
356
|
+
} catch (err) {
|
|
357
|
+
if (runRef.current !== myRun) return;
|
|
358
|
+
setError(toDatastoreError(err));
|
|
359
|
+
setLoading(false);
|
|
360
|
+
}
|
|
361
|
+
}, []);
|
|
362
|
+
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
doFetch();
|
|
365
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
366
|
+
}, [tableId]);
|
|
367
|
+
|
|
368
|
+
const refetch = useCallback(async () => {
|
|
369
|
+
await doFetch();
|
|
370
|
+
}, [doFetch]);
|
|
371
|
+
|
|
372
|
+
return { schema, loading, error, refetch };
|
|
373
|
+
}
|
|
374
|
+
|
|
300
375
|
/**
|
|
301
376
|
* Stateful file-asset resolver hook. Returns { url, file, loading, error,
|
|
302
377
|
* refetch }.
|
package/dist/index.d.ts
CHANGED
|
@@ -383,6 +383,59 @@ export function useDatastoreRecord(
|
|
|
383
383
|
refetch(): Promise<void>;
|
|
384
384
|
};
|
|
385
385
|
|
|
386
|
+
/**
|
|
387
|
+
* One column in a table's schema, as returned by `useDatastoreSchema`.
|
|
388
|
+
* Structural metadata only — never row data.
|
|
389
|
+
*/
|
|
390
|
+
export interface DatastoreSchemaColumn {
|
|
391
|
+
id: string;
|
|
392
|
+
name: string;
|
|
393
|
+
dataType:
|
|
394
|
+
| "STRING"
|
|
395
|
+
| "TEXT"
|
|
396
|
+
| "NUMBER"
|
|
397
|
+
| "FLOAT"
|
|
398
|
+
| "BOOL"
|
|
399
|
+
| "DATE"
|
|
400
|
+
| "FILE"
|
|
401
|
+
| "STRING_ARRAY"
|
|
402
|
+
| "INT_ARRAY"
|
|
403
|
+
| "RELATION"
|
|
404
|
+
| "USER"
|
|
405
|
+
| "USER_GROUP";
|
|
406
|
+
required: boolean;
|
|
407
|
+
/** For RELATION columns only. */
|
|
408
|
+
relationType?: "ONE_TO_ONE" | "ONE_TO_MANY" | "MANY_TO_MANY" | null;
|
|
409
|
+
/** For RELATION columns only — the id of the table this column points at. */
|
|
410
|
+
targetTableId?: string | null;
|
|
411
|
+
/** True when this column is the table's display/identification column. */
|
|
412
|
+
isIdentification?: boolean;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export interface DatastoreSchema {
|
|
416
|
+
id: string;
|
|
417
|
+
name: string;
|
|
418
|
+
columns: DatastoreSchemaColumn[];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export interface SchemaResult {
|
|
422
|
+
schema: DatastoreSchema | null;
|
|
423
|
+
loading: boolean;
|
|
424
|
+
error: DatastoreError | null;
|
|
425
|
+
refetch(): Promise<void>;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Stateful table-schema resolver hook. Returns `{ schema, loading, error,
|
|
430
|
+
* refetch }` where `schema` is the bound table's column structure (`null`
|
|
431
|
+
* until loaded). Reads the existing ACL-gated `GET /tables/:id` — structure
|
|
432
|
+
* only, no row data. Use it to resolve a stored `columnId` to its column
|
|
433
|
+
* name / dataType / relation target at runtime.
|
|
434
|
+
*/
|
|
435
|
+
export function useDatastoreSchema(
|
|
436
|
+
tableId: string | null | undefined,
|
|
437
|
+
): SchemaResult;
|
|
438
|
+
|
|
386
439
|
/**
|
|
387
440
|
* Stateful file-asset resolver hook. Returns `{ url, file, loading, error,
|
|
388
441
|
* refetch }`. The `url` is an absolute URL composed against the host's API
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
],
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "node scripts/build.js",
|
|
38
|
-
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/linter-users-scope.test.js"
|
|
38
|
+
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/linter-users-scope.test.js"
|
|
39
39
|
},
|
|
40
40
|
"engines": {
|
|
41
41
|
"node": ">=18"
|