@colixsystems/widget-sdk 0.18.0 → 0.21.1
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 +129 -29
- package/dist/contract.cjs +228 -113
- package/dist/contract.js +238 -122
- package/dist/hooks.js +781 -570
- package/dist/index.d.ts +257 -65
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/dist/linter.cjs +56 -0
- package/dist/linter.js +57 -0
- package/dist/manifest.cjs +157 -2
- package/dist/manifest.js +157 -2
- package/package.json +2 -2
package/dist/contract.js
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
const DEFAULT_THEME_TOKENS = Object.freeze({
|
|
12
12
|
colors: Object.freeze({
|
|
13
|
-
primary: "#
|
|
13
|
+
primary: "#3b82f6",
|
|
14
14
|
onPrimary: "#ffffff",
|
|
15
|
-
secondary: "#
|
|
15
|
+
secondary: "#10b981",
|
|
16
16
|
onSecondary: "#ffffff",
|
|
17
17
|
surface: "#ffffff",
|
|
18
18
|
onSurface: "#111827",
|
|
@@ -70,6 +70,26 @@ const HOOKS = [
|
|
|
70
70
|
requiredContextSlice: ["user"],
|
|
71
71
|
scopes: null,
|
|
72
72
|
},
|
|
73
|
+
{
|
|
74
|
+
name: "useFill",
|
|
75
|
+
signature: "useFill()",
|
|
76
|
+
description:
|
|
77
|
+
"Returns true when the host has sized this widget to fill its layout " +
|
|
78
|
+
'slot\'s available height — a page-grid tile whose author chose "Fill ' +
|
|
79
|
+
'tile height", or a widget type that fills by default (containers and ' +
|
|
80
|
+
"media). When true, a widget that has a meaningful filled form (Image, " +
|
|
81
|
+
"Chart, Map, Video) should switch from its intrinsic height to a stretch " +
|
|
82
|
+
'style (flex: 1 / height: "100%"); widgets with no useful filled form ' +
|
|
83
|
+
"may ignore it. Defaults to false wherever the host has not opted the " +
|
|
84
|
+
"widget into filling, so calling it is always safe. The SAME value is " +
|
|
85
|
+
"injected by the web Player and the native export (CLAUDE.md §3), so a " +
|
|
86
|
+
"widget's fill behaviour is identical on both platforms.",
|
|
87
|
+
returnShape: {
|
|
88
|
+
"(returns)": "boolean",
|
|
89
|
+
},
|
|
90
|
+
requiredContextSlice: ["fill"],
|
|
91
|
+
scopes: null,
|
|
92
|
+
},
|
|
73
93
|
{
|
|
74
94
|
name: "useChildRenderer",
|
|
75
95
|
signature: "useChildRenderer()",
|
|
@@ -168,12 +188,12 @@ const HOOKS = [
|
|
|
168
188
|
name: "useDirectory",
|
|
169
189
|
signature: "useDirectory(query?)",
|
|
170
190
|
returnShape: {
|
|
171
|
-
users: "Array<{ id, name, role }>",
|
|
191
|
+
users: "Array<{ id, name, role }> // snake_case rows; unwrapped from { data, meta }",
|
|
172
192
|
loading: "boolean",
|
|
173
193
|
error: "DatastoreError | null",
|
|
174
194
|
refetch: "() => Promise<void>",
|
|
175
195
|
},
|
|
176
|
-
requiredContextSlice: ["directory.
|
|
196
|
+
requiredContextSlice: ["directory.users"],
|
|
177
197
|
scopes: ["directory.read:users"],
|
|
178
198
|
},
|
|
179
199
|
{
|
|
@@ -207,28 +227,26 @@ const HOOKS = [
|
|
|
207
227
|
name: "useUsers",
|
|
208
228
|
signature: "useUsers(query?)",
|
|
209
229
|
description:
|
|
210
|
-
"AppUser administration
|
|
211
|
-
"deactivate,
|
|
212
|
-
"
|
|
213
|
-
"
|
|
230
|
+
"AppUser administration via the injected directory-client at " +
|
|
231
|
+
"ctx.directory.users.{list,get,invite,deactivate,reactivate}. Returns " +
|
|
232
|
+
"{ users, loading, error, refetch, invite, deactivate, reactivate, remove }. " +
|
|
233
|
+
"list returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
234
|
+
"res.data; rows are snake_case (is_active, …). Reads need users.read:* " +
|
|
235
|
+
"scope; mutations need users.write:*. The `invite` call accepts " +
|
|
236
|
+
"{ email, name, group_ids? } and returns the resulting AppUserInvite row " +
|
|
237
|
+
"(the email is sent by the host).",
|
|
214
238
|
returnShape: {
|
|
215
|
-
users: "Array<{ id, name, email?, role,
|
|
239
|
+
users: "Array<{ id, name, email?, role, is_active }> // snake_case rows; unwrapped from { data, meta }",
|
|
216
240
|
loading: "boolean",
|
|
217
241
|
error: "DirectoryError | null",
|
|
218
242
|
refetch: "() => Promise<void>",
|
|
219
243
|
invite:
|
|
220
|
-
"({ email, name,
|
|
244
|
+
"({ email, name, group_ids? }) => Promise<Invite> // rejects with DirectoryError",
|
|
221
245
|
deactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
222
246
|
reactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
223
247
|
remove: "(userId) => Promise<void> // rejects with DirectoryError",
|
|
224
248
|
},
|
|
225
|
-
requiredContextSlice: [
|
|
226
|
-
"users.listUsers",
|
|
227
|
-
"users.invite",
|
|
228
|
-
"users.deactivate",
|
|
229
|
-
"users.reactivate",
|
|
230
|
-
"users.remove",
|
|
231
|
-
],
|
|
249
|
+
requiredContextSlice: ["directory.users"],
|
|
232
250
|
scopes: ["users.read:*"],
|
|
233
251
|
},
|
|
234
252
|
// REQ-USERMGMT / REQ-ACL-SYS M3 — AppUserGroup administration. Returns
|
|
@@ -240,11 +258,14 @@ const HOOKS = [
|
|
|
240
258
|
name: "useGroups",
|
|
241
259
|
signature: "useGroups(query?)",
|
|
242
260
|
description:
|
|
243
|
-
"AppUserGroup administration
|
|
244
|
-
"create,
|
|
261
|
+
"AppUserGroup administration via the injected directory-client at " +
|
|
262
|
+
"ctx.directory.groups.{list,create,remove,addMember,removeMember,listMine}. " +
|
|
263
|
+
"Returns { groups, loading, error, refetch, create, remove, addMember, " +
|
|
264
|
+
"removeMember }. list returns the { data, meta } envelope verbatim — the " +
|
|
265
|
+
"hook unwraps res.data; rows are snake_case. Reads need groups.read:*; " +
|
|
245
266
|
"mutations need groups.write:*.",
|
|
246
267
|
returnShape: {
|
|
247
|
-
groups: "Array<{ id, name,
|
|
268
|
+
groups: "Array<{ id, name, member_count }> // snake_case rows; unwrapped from { data, meta }",
|
|
248
269
|
loading: "boolean",
|
|
249
270
|
error: "DirectoryError | null",
|
|
250
271
|
refetch: "() => Promise<void>",
|
|
@@ -256,54 +277,48 @@ const HOOKS = [
|
|
|
256
277
|
removeMember:
|
|
257
278
|
"(groupId, userId) => Promise<void> // rejects with DirectoryError",
|
|
258
279
|
},
|
|
259
|
-
requiredContextSlice: [
|
|
260
|
-
"groups.listGroups",
|
|
261
|
-
"groups.create",
|
|
262
|
-
"groups.remove",
|
|
263
|
-
"groups.addMember",
|
|
264
|
-
"groups.removeMember",
|
|
265
|
-
],
|
|
280
|
+
requiredContextSlice: ["directory.groups"],
|
|
266
281
|
scopes: ["groups.read:*"],
|
|
267
282
|
},
|
|
268
283
|
// REQ-ACL-06 / REQ-ACL-RELINHERIT-05 — per-record VirtualPermission
|
|
269
|
-
// management for a single record. Reads
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
284
|
+
// management for a single record. Reads the injected datastore-client at
|
|
285
|
+
// ctx.datastore.records(tableId).permissions(recordId) (the recordPermissions
|
|
286
|
+
// facade was folded into the datastore-client). The backend gates the call
|
|
287
|
+
// on `can_grant` for the target record (Studio owners short-circuit;
|
|
288
|
+
// APP_USER actors must hold `can_grant` via REQ-ACL-05 / REQ-ACL-06). A
|
|
289
|
+
// widget that declares the scope but whose caller lacks the grant receives
|
|
290
|
+
// `PermissionError { code: 'FORBIDDEN' }`.
|
|
276
291
|
{
|
|
277
292
|
name: "useRecordPermissions",
|
|
278
293
|
signature: "useRecordPermissions(tableId, recordId)",
|
|
279
294
|
description:
|
|
280
|
-
"Manage per-record VirtualPermission grants on a single record
|
|
281
|
-
"
|
|
282
|
-
"permissions
|
|
283
|
-
"
|
|
284
|
-
"
|
|
285
|
-
"(
|
|
286
|
-
"
|
|
287
|
-
"
|
|
295
|
+
"Manage per-record VirtualPermission grants on a single record via the " +
|
|
296
|
+
"injected datastore-client at " +
|
|
297
|
+
"ctx.datastore.records(tableId).permissions(recordId).{list,grant,update,revoke}. " +
|
|
298
|
+
"Returns { permissions, loading, error, grant, revoke, update, refetch } " +
|
|
299
|
+
"where permissions is Array<{ id, user_id, group_id, can_read, can_write, " +
|
|
300
|
+
"can_delete, can_grant }> (snake_case rows verbatim; list() returns the " +
|
|
301
|
+
"{ data, meta } envelope and the hook unwraps res.data). grant/update " +
|
|
302
|
+
"bodies are snake_case verbatim ({ user_id | group_id, can_read, " +
|
|
303
|
+
"can_write, can_delete, can_grant }). Mutating requires acl.write:records " +
|
|
304
|
+
"scope AND can_grant on the target record (REQ-ACL-RELINHERIT-05: " +
|
|
305
|
+
"APP_USER actors with can_grant are accepted, not only Studio owners). " +
|
|
306
|
+
"When tableId or recordId is null/empty the hook collapses to an empty " +
|
|
307
|
+
"no-op result without a network round-trip.",
|
|
288
308
|
returnShape: {
|
|
289
309
|
permissions:
|
|
290
|
-
"Array<{ id,
|
|
310
|
+
"Array<{ id, user_id, group_id, can_read, can_write, can_delete, can_grant }> // snake_case rows; unwrapped from { data, meta }",
|
|
291
311
|
loading: "boolean",
|
|
292
312
|
error: "PermissionError | null",
|
|
293
313
|
grant:
|
|
294
|
-
"({
|
|
314
|
+
"({ user_id?, group_id?, can_read?, can_write?, can_delete?, can_grant? }) => Promise<RecordPermission> // rejects with PermissionError",
|
|
295
315
|
revoke:
|
|
296
316
|
"(permissionId) => Promise<void> // rejects with PermissionError",
|
|
297
317
|
update:
|
|
298
|
-
"(permissionId, {
|
|
318
|
+
"(permissionId, { can_read?, can_write?, can_delete?, can_grant? }) => Promise<RecordPermission> // rejects with PermissionError",
|
|
299
319
|
refetch: "() => Promise<void>",
|
|
300
320
|
},
|
|
301
|
-
requiredContextSlice: [
|
|
302
|
-
"recordPermissions.list",
|
|
303
|
-
"recordPermissions.grant",
|
|
304
|
-
"recordPermissions.revoke",
|
|
305
|
-
"recordPermissions.update",
|
|
306
|
-
],
|
|
321
|
+
requiredContextSlice: ["datastore.records"],
|
|
307
322
|
scopes: ["acl.write:records"],
|
|
308
323
|
},
|
|
309
324
|
// REQ-WSDK-PLATFORM §6 — Tier A SDK hooks.
|
|
@@ -455,10 +470,40 @@ const CATEGORIES = [
|
|
|
455
470
|
"DATA",
|
|
456
471
|
"MEDIA",
|
|
457
472
|
"COMMUNICATION",
|
|
473
|
+
// REQ-USERMGMT-06: app-administration widgets (User Management, …) the
|
|
474
|
+
// published app embeds for its own member management — its own palette
|
|
475
|
+
// section, distinct from COMMUNICATION.
|
|
476
|
+
"ADMINISTRATION",
|
|
458
477
|
"CUSTOM",
|
|
459
478
|
];
|
|
460
479
|
const PLATFORMS = ["web", "native"];
|
|
461
480
|
|
|
481
|
+
// REQ-WIDGET-ACTION — server-side actions a widget may declare in its
|
|
482
|
+
// manifest. Each runs in the shared isolated-vm action runner (see backend
|
|
483
|
+
// action-runner.service.js) on a cron schedule or in response to a record
|
|
484
|
+
// CRUD event — NEVER in the rendered app, so they never affect Player ↔
|
|
485
|
+
// export parity. The trigger vocabulary mirrors the backend Action model.
|
|
486
|
+
const ACTION_TRIGGER_TYPES = [
|
|
487
|
+
"schedule",
|
|
488
|
+
"record_created",
|
|
489
|
+
"record_updated",
|
|
490
|
+
"record_deleted",
|
|
491
|
+
];
|
|
492
|
+
// Globals the action script runs against (the runner's surface) — distinct
|
|
493
|
+
// from the React/SDK widget surface, so the component import/banned-API
|
|
494
|
+
// linter does NOT scan action scripts.
|
|
495
|
+
const ACTION_SCRIPT_GLOBALS = [
|
|
496
|
+
"datastore",
|
|
497
|
+
"fetch",
|
|
498
|
+
"console",
|
|
499
|
+
"record",
|
|
500
|
+
"tenantId",
|
|
501
|
+
"triggerType",
|
|
502
|
+
"triggerTableId",
|
|
503
|
+
];
|
|
504
|
+
// Mirrors action.service.js SCRIPT_MAX_BYTES.
|
|
505
|
+
const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
|
|
506
|
+
|
|
462
507
|
// Reverse-DNS-ish manifest id, e.g. "com.acme.charts.barchart". Two or
|
|
463
508
|
// more labels, lowercase alnum + hyphen, label starts with a letter. The
|
|
464
509
|
// analyzer + the SDK validator both read this from the contract so a
|
|
@@ -544,6 +589,24 @@ const MANIFEST_SCHEMA = {
|
|
|
544
589
|
description:
|
|
545
590
|
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (must be declared earlier in the array). Tables persist across uninstalls.",
|
|
546
591
|
},
|
|
592
|
+
actions: {
|
|
593
|
+
type: "object[]",
|
|
594
|
+
required: false,
|
|
595
|
+
description:
|
|
596
|
+
"Optional. Server-side actions the widget declares. Each runs in the shared isolated-vm action runner (cron- or record-triggered) — NEVER in the rendered app. Operators enable them per tenant from the Properties Panel; the action materialises DISABLED until they bind an integration API key (and, for record_* triggers, a target table) in the Actions admin page. Each entry: { key (stable, unique within the manifest), name, description?, triggerType (one of " +
|
|
597
|
+
ACTION_TRIGGER_TYPES.join(", ") +
|
|
598
|
+
"), scheduleCron? (required iff triggerType=='schedule'; node-cron syntax), timeoutMs? (100–300000), scriptSource (≤200 KiB; runs against " +
|
|
599
|
+
ACTION_SCRIPT_GLOBALS.join(", ") +
|
|
600
|
+
" — NOT the React surface, so SDK imports/hooks are unavailable) }. Do NOT include triggerTableId or apiKeyId — those are tenant-local and bound after install.",
|
|
601
|
+
default: [],
|
|
602
|
+
},
|
|
603
|
+
translations: {
|
|
604
|
+
type: "object",
|
|
605
|
+
required: false,
|
|
606
|
+
description:
|
|
607
|
+
"Optional (REQ-L10N-WIDGET). Translation strings the widget ships. Shape { <key>: { en: string, <locale>?: string, ... } } — `en` is REQUIRED per key. At install the host merges these into the tenant's localization dictionary under a per-widget namespace (`widget.<manifest.id>.<key>`); `useI18n().t(\"<key>\")` resolves the namespaced key automatically, so authors never type the prefix. Non-destructive: an existing admin-edited value is never overwritten, and a language the tenant has not added is never created (values for absent locales are dropped; the widget's `en` seeds the tenant's base language so every key renders). Keys persist across uninstalls; admins prune them from the Translations admin (bulk delete by `widget.<id>.` prefix). Limits: ≤100 keys; each key matches /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/; each value ≤1 KB; the namespaced key must fit the dictionary key cap (128 chars).",
|
|
608
|
+
default: {},
|
|
609
|
+
},
|
|
547
610
|
};
|
|
548
611
|
|
|
549
612
|
const WIDGET_CONTEXT_SHAPE = {
|
|
@@ -558,10 +621,24 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
558
621
|
required: true,
|
|
559
622
|
fields: { id: "manifest.id", version: "manifest.version" },
|
|
560
623
|
},
|
|
624
|
+
// REQ-LAY-08 — optional host layout hint backing useFill(). True when the
|
|
625
|
+
// host sized this widget to fill its layout slot's available height (a
|
|
626
|
+
// page-grid tile set to "Fill tile height", or a default-fill widget type).
|
|
627
|
+
// Optional: a host that never fills a widget simply omits it and useFill()
|
|
628
|
+
// returns false, so existing hosts need no change.
|
|
629
|
+
fill: {
|
|
630
|
+
description:
|
|
631
|
+
"Optional host layout hint (boolean). True when the host sized this " +
|
|
632
|
+
"widget to fill its layout slot's available height (page-grid tile " +
|
|
633
|
+
"fill). Backs useFill(); defaults to false when absent.",
|
|
634
|
+
required: false,
|
|
635
|
+
fields: {},
|
|
636
|
+
},
|
|
561
637
|
user: {
|
|
562
|
-
description:
|
|
638
|
+
description:
|
|
639
|
+
"Signed-in user, host-provided VERBATIM (snake_case: { id, email, display_name, roles, group_ids }). Not a data-client.",
|
|
563
640
|
required: true,
|
|
564
|
-
fields: { id: "string", email: "string",
|
|
641
|
+
fields: { id: "string", email: "string", display_name: "string" },
|
|
565
642
|
},
|
|
566
643
|
workspace: {
|
|
567
644
|
description:
|
|
@@ -581,19 +658,30 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
581
658
|
},
|
|
582
659
|
datastore: {
|
|
583
660
|
description:
|
|
584
|
-
"
|
|
661
|
+
"Injected @colixsystems/datastore-client instance. " +
|
|
662
|
+
"{ tables: { list(), get(idOrName) }, schema(tableId) -> Promise<{ id, name, columns: [...] }>, " +
|
|
663
|
+
"records(tableId) -> { list(query) -> Promise<{ data, meta }>, get(id), create(values), update(id, values), delete(id), aggregate(spec), " +
|
|
664
|
+
"permissions(recordId) -> { list() -> Promise<{ data, meta }>, grant(body), update(permId, patch), revoke(permId) } } }. " +
|
|
665
|
+
"`records` backs the query/record/mutation hooks; `records(t).permissions(r)` backs useRecordPermissions(); `schema` backs useDatastoreSchema(). " +
|
|
666
|
+
"List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case (author column values keep their author-given names).",
|
|
585
667
|
required: true,
|
|
586
|
-
fields: { records: "function", schema: "function" },
|
|
668
|
+
fields: { records: "function", schema: "function", tables: "object" },
|
|
587
669
|
},
|
|
588
670
|
directory: {
|
|
589
671
|
description:
|
|
590
|
-
"
|
|
672
|
+
"Injected @colixsystems/directory-client instance. " +
|
|
673
|
+
"{ me(), users: { list(query?) -> Promise<{ data, meta }>, get(id), invite(body), deactivate(id), reactivate(id) }, " +
|
|
674
|
+
"groups: { list(query?) -> Promise<{ data, meta }>, create(body), remove(id), addMember(groupId, userId), removeMember(groupId, userId), listMine() }, " +
|
|
675
|
+
"invites: { list(), revoke(id), resend(id) } }. " +
|
|
676
|
+
"users backs useDirectory() + useUsers(); groups backs useGroups(). List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case. Reads gated by directory.read:users / users.read:* / groups.read:*; mutations by users.write:* / groups.write:*.",
|
|
591
677
|
required: true,
|
|
592
|
-
fields: {
|
|
678
|
+
fields: { users: "object", groups: "object" },
|
|
593
679
|
},
|
|
594
680
|
files: {
|
|
595
681
|
description:
|
|
596
|
-
"
|
|
682
|
+
"Injected @colixsystems/files-client instance, FLATTENED so file ops are top-level. " +
|
|
683
|
+
"{ get(id) -> Promise<{ id, url, ... }>, list(query) -> Promise<{ data, meta }>, upload(formData), folders: { list, create }, shares: { list, create, remove } }. " +
|
|
684
|
+
"Backs useFile(); the returned file already carries an absolute url the widget can drop into an <Image source>.",
|
|
597
685
|
required: true,
|
|
598
686
|
fields: { get: "function" },
|
|
599
687
|
},
|
|
@@ -610,64 +698,17 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
610
698
|
},
|
|
611
699
|
payments: {
|
|
612
700
|
description:
|
|
613
|
-
"
|
|
701
|
+
"Injected @colixsystems/payments-client instance (REQ-BILL-07-WIDGETPAY). { requestPayment(body) -> Promise<{ id, status, checkoutUrl? }>, getPayment(id) -> Promise<payment> }. Backs usePayments(); requires the payments.charge:appUser scope. The host opens hosted Checkout (or auto-confirms under the mock provider); the charge settles to the workspace owner.",
|
|
614
702
|
required: true,
|
|
615
703
|
fields: { requestPayment: "function", getPayment: "function" },
|
|
616
704
|
},
|
|
617
|
-
// REQ-
|
|
618
|
-
//
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
//
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
"AppUser administration. { listUsers(query?) -> Promise<User[]>, invite({ email, name, groupIds? }) -> Promise<Invite>, deactivate(userId) -> Promise<User>, reactivate(userId) -> Promise<User>, remove(userId) -> Promise<void> }. Backs useUsers(); reads require users.read:*, mutations require users.write:*.",
|
|
625
|
-
required: true,
|
|
626
|
-
fields: {
|
|
627
|
-
listUsers: "function",
|
|
628
|
-
invite: "function",
|
|
629
|
-
deactivate: "function",
|
|
630
|
-
reactivate: "function",
|
|
631
|
-
remove: "function",
|
|
632
|
-
},
|
|
633
|
-
},
|
|
634
|
-
// REQ-ACL-06 / REQ-ACL-RELINHERIT-05 — per-record VirtualPermission
|
|
635
|
-
// management facade backing useRecordPermissions(). Hits the existing
|
|
636
|
-
// /api/v1/tables/:tableId/records/:recordId/permissions REST surface;
|
|
637
|
-
// the host normalises the wire shape (userId / groupId / both-null =
|
|
638
|
-
// public) into the principalType+principalId pair widgets read.
|
|
639
|
-
recordPermissions: {
|
|
640
|
-
description:
|
|
641
|
-
"Per-record VirtualPermission management. " +
|
|
642
|
-
"{ list(tableId, recordId) -> Promise<RecordPermission[]>, " +
|
|
643
|
-
"grant(tableId, recordId, body) -> Promise<RecordPermission>, " +
|
|
644
|
-
"revoke(tableId, recordId, permissionId) -> Promise<void>, " +
|
|
645
|
-
"update(tableId, recordId, permissionId, body) -> Promise<RecordPermission> }. " +
|
|
646
|
-
"Backs useRecordPermissions(); requires acl.write:records scope AND " +
|
|
647
|
-
"canGrant on the target record.",
|
|
648
|
-
required: true,
|
|
649
|
-
fields: {
|
|
650
|
-
list: "function",
|
|
651
|
-
grant: "function",
|
|
652
|
-
revoke: "function",
|
|
653
|
-
update: "function",
|
|
654
|
-
},
|
|
655
|
-
},
|
|
656
|
-
// REQ-USERMGMT / REQ-ACL-SYS M3 — AppUserGroup administration facade
|
|
657
|
-
// backing useGroups(). Reads gated by `groups.read:*`; mutations by
|
|
658
|
-
// `groups.write:*`. Same X-Widget-Scopes + SystemAcl gating as users.
|
|
659
|
-
groups: {
|
|
660
|
-
description:
|
|
661
|
-
"AppUserGroup administration. { listGroups(query?) -> Promise<Group[]>, create({ name }) -> Promise<Group>, remove(groupId) -> Promise<void>, addMember(groupId, userId) -> Promise<void>, removeMember(groupId, userId) -> Promise<void> }. Backs useGroups(); reads require groups.read:*, mutations require groups.write:*.",
|
|
662
|
-
required: true,
|
|
663
|
-
fields: {
|
|
664
|
-
listGroups: "function",
|
|
665
|
-
create: "function",
|
|
666
|
-
remove: "function",
|
|
667
|
-
addMember: "function",
|
|
668
|
-
removeMember: "function",
|
|
669
|
-
},
|
|
670
|
-
},
|
|
705
|
+
// REQ-WSDK-DOMAIN-CLIENTS — the AppUser administration, AppUserGroup
|
|
706
|
+
// administration, and per-record VirtualPermission facades that used to
|
|
707
|
+
// live here (`users`, `groups`, `recordPermissions`) were folded into the
|
|
708
|
+
// injected domain-client instances: useUsers()/useGroups() read
|
|
709
|
+
// ctx.directory.{users,groups}; useRecordPermissions() reads
|
|
710
|
+
// ctx.datastore.records(table).permissions(record). See the `directory`
|
|
711
|
+
// and `datastore` slices above.
|
|
671
712
|
i18n: {
|
|
672
713
|
description: "{ t(key, fallback?), locale }.",
|
|
673
714
|
required: true,
|
|
@@ -872,20 +913,89 @@ function deepFreeze(value) {
|
|
|
872
913
|
return Object.freeze(value);
|
|
873
914
|
}
|
|
874
915
|
|
|
916
|
+
// REQ-L10N-WIDGET — a widget's manifest `translations` are merged into the
|
|
917
|
+
// tenant dictionary under a per-widget namespace so they never collide with
|
|
918
|
+
// app keys or another widget's keys. The prefix is DERIVED from the widget id
|
|
919
|
+
// (it "follows the widget"), so authors call `t("greeting")` and the host
|
|
920
|
+
// resolves `widget.<id>.greeting`. This is the ONE definition of the key
|
|
921
|
+
// format: the SDK `useI18n` hook (lookup) and the backend seeder (write) both
|
|
922
|
+
// call it, so the wire key and the lookup key can never drift.
|
|
923
|
+
function widgetTranslationPrefix(id) {
|
|
924
|
+
return `widget.${id}.`;
|
|
925
|
+
}
|
|
926
|
+
function widgetTranslationKey(id, key) {
|
|
927
|
+
return `widget.${id}.${key}`;
|
|
928
|
+
}
|
|
929
|
+
|
|
875
930
|
const CONTRACT = deepFreeze({
|
|
876
|
-
// 1.
|
|
877
|
-
//
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
//
|
|
881
|
-
//
|
|
882
|
-
// the
|
|
883
|
-
|
|
931
|
+
// 1.7.0: additive — new useDatastoreSchema(tableId) hook + the
|
|
932
|
+
// datastore.schema host-context slice it reads (resolves a table's column
|
|
933
|
+
// structure at runtime via the existing ACL-gated GET /tables/:id).
|
|
934
|
+
//
|
|
935
|
+
// 1.8.0: two additive features.
|
|
936
|
+
// - REQ-WIDGET-ACTION — manifests may declare an optional `actions` array:
|
|
937
|
+
// server-side cron/record-triggered scripts the operator enables per
|
|
938
|
+
// tenant, run in the existing isolated-vm action runner (never in the
|
|
939
|
+
// rendered app). New fields actionTriggerTypes / actionScriptGlobals /
|
|
940
|
+
// actionScriptMaxBytes feed the docs + agent prompt. New ADMINISTRATION
|
|
941
|
+
// manifest category (REQ-USERMGMT-06).
|
|
942
|
+
// - REQ-ACL-06 — new useRecordPermissions(tableId, recordId) hook + the
|
|
943
|
+
// recordPermissions host-context slice it reads + the acl.write:records
|
|
944
|
+
// scope it gates on. Hits the existing REQ-ACL-06
|
|
945
|
+
// /api/v1/tables/:tableId/records/:recordId/permissions REST surface;
|
|
946
|
+
// the host normalises the wire shape into a single principalType+
|
|
947
|
+
// principalId pair widgets branch on. Caller still needs canGrant on
|
|
948
|
+
// the target record (REQ-ACL-RELINHERIT-05).
|
|
949
|
+
//
|
|
950
|
+
// 1.9.0: host-contract change (REQ-WSDK-DOMAIN-CLIENTS) — the host now
|
|
951
|
+
// injects domain-client INSTANCES on the WidgetContext rather than
|
|
952
|
+
// bespoke per-hook facades. ctx.datastore is a @colixsystems/datastore-client
|
|
953
|
+
// (records(t).list now returns the { data, meta } envelope verbatim;
|
|
954
|
+
// records(t).permissions(r) replaces the deleted ctx.recordPermissions),
|
|
955
|
+
// ctx.directory is a @colixsystems/directory-client (users/groups
|
|
956
|
+
// namespaces replace the deleted ctx.users / ctx.groups; list returns
|
|
957
|
+
// envelopes), ctx.files is a flattened @colixsystems/files-client,
|
|
958
|
+
// ctx.payments is a @colixsystems/payments-client. All rows/bodies are
|
|
959
|
+
// snake_case. Hooks now unwrap the list envelope (res.data ?? []). The
|
|
960
|
+
// SDK stays duck-typed — it imports none of the four data SDKs. Minor
|
|
961
|
+
// bump on the contract's pre-1.0 versioning (the breaking channel).
|
|
962
|
+
//
|
|
963
|
+
// 1.10.0: additive (REQ-L10N-WIDGET) — manifests may declare an optional
|
|
964
|
+
// `translations` map ({ <key>: { en, <locale>? } }, en required). The
|
|
965
|
+
// host seeds them into the tenant l10n dictionary under a per-widget
|
|
966
|
+
// namespace at install; `useI18n().t(key)` now auto-prefixes the lookup
|
|
967
|
+
// with `widget.<ctx.widget.id>.` (then falls back to the raw key, so
|
|
968
|
+
// existing widgets and shared app keys keep resolving). New exported
|
|
969
|
+
// helpers widgetTranslationPrefix / widgetTranslationKey are the single
|
|
970
|
+
// source of the key format, shared by the hook and the backend seeder.
|
|
971
|
+
//
|
|
972
|
+
// 1.11.0: additive (REQ-LAY-08) — new useFill() hook + the optional `fill`
|
|
973
|
+
// host-context slice it reads. The host sets ctx.fill=true when it has
|
|
974
|
+
// sized a widget to fill its layout slot's available height (a page-grid
|
|
975
|
+
// tile whose author chose "Fill tile height", or a default-fill widget
|
|
976
|
+
// type — containers + media). Widgets that can stretch (Image, Chart,
|
|
977
|
+
// Map, Video) switch to flex:1/height:"100%" when useFill() is true; all
|
|
978
|
+
// others ignore it. The slice is OPTIONAL (defaults false) so existing
|
|
979
|
+
// hosts need no change, and the same value is injected on web and native
|
|
980
|
+
// so fill behaviour cannot diverge between the Player and the export.
|
|
981
|
+
//
|
|
982
|
+
// 1.11.1: fix — the canonical default `themeTokens.colors` now match the
|
|
983
|
+
// product's advertised default brand (primary #3b82f6 blue, secondary
|
|
984
|
+
// #10b981 green) instead of the stale coral/slate that no other surface
|
|
985
|
+
// used. The Theme Settings tab and Player chrome already defaulted to
|
|
986
|
+
// these values, so a tenant that never customised its theme rendered
|
|
987
|
+
// widgets in coral until a first save persisted the blue — a divergence
|
|
988
|
+
// between the unsaved and saved-defaults render. Aligning the canonical
|
|
989
|
+
// tokens removes it (no shape/signature change — default-value fix only).
|
|
990
|
+
version: "1.11.1",
|
|
884
991
|
hooks: HOOKS,
|
|
885
992
|
primitives: PRIMITIVES,
|
|
886
993
|
manifestSchema: MANIFEST_SCHEMA,
|
|
887
994
|
manifestCategories: CATEGORIES,
|
|
888
995
|
manifestPlatforms: PLATFORMS,
|
|
996
|
+
actionTriggerTypes: ACTION_TRIGGER_TYPES,
|
|
997
|
+
actionScriptGlobals: ACTION_SCRIPT_GLOBALS,
|
|
998
|
+
actionScriptMaxBytes: ACTION_SCRIPT_MAX_BYTES,
|
|
889
999
|
themeTokens: DEFAULT_THEME_TOKENS,
|
|
890
1000
|
widgetContextShape: WIDGET_CONTEXT_SHAPE,
|
|
891
1001
|
bundleExportContract: BUNDLE_EXPORT_CONTRACT,
|
|
@@ -909,4 +1019,10 @@ function requiredContextKeys() {
|
|
|
909
1019
|
return [...keys];
|
|
910
1020
|
}
|
|
911
1021
|
|
|
912
|
-
export {
|
|
1022
|
+
export {
|
|
1023
|
+
CONTRACT,
|
|
1024
|
+
isHookAllowed,
|
|
1025
|
+
requiredContextKeys,
|
|
1026
|
+
widgetTranslationPrefix,
|
|
1027
|
+
widgetTranslationKey,
|
|
1028
|
+
};
|