@colixsystems/widget-sdk 0.18.0 → 0.19.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 +87 -29
- package/dist/contract.cjs +137 -110
- package/dist/contract.js +147 -119
- package/dist/hooks.js +734 -571
- package/dist/index.d.ts +228 -65
- package/dist/linter.cjs +56 -0
- package/dist/linter.js +57 -0
- package/dist/manifest.cjs +75 -2
- package/dist/manifest.js +75 -2
- package/package.json +2 -2
package/dist/manifest.cjs
CHANGED
|
@@ -5,11 +5,78 @@
|
|
|
5
5
|
// implementation.
|
|
6
6
|
|
|
7
7
|
const VALID_CATEGORIES = new Set([
|
|
8
|
-
"input", "display", "layout", "data", "media", "communication", "custom",
|
|
9
|
-
"INPUT", "DISPLAY", "LAYOUT", "DATA", "MEDIA", "COMMUNICATION", "CUSTOM",
|
|
8
|
+
"input", "display", "layout", "data", "media", "communication", "administration", "custom",
|
|
9
|
+
"INPUT", "DISPLAY", "LAYOUT", "DATA", "MEDIA", "COMMUNICATION", "ADMINISTRATION", "CUSTOM",
|
|
10
10
|
]);
|
|
11
11
|
const VALID_PLATFORMS = new Set(["web", "native"]);
|
|
12
12
|
|
|
13
|
+
// REQ-WIDGET-ACTION — structural validation for manifest-declared server
|
|
14
|
+
// actions. Mirrors the backend action.service caps; the runner re-validates
|
|
15
|
+
// the cron expression (node-cron) and binds the tenant-local API key + table
|
|
16
|
+
// at enable time, so we only check shape + size here (cross-env: no Buffer).
|
|
17
|
+
const VALID_ACTION_TRIGGERS = new Set([
|
|
18
|
+
"schedule",
|
|
19
|
+
"record_created",
|
|
20
|
+
"record_updated",
|
|
21
|
+
"record_deleted",
|
|
22
|
+
]);
|
|
23
|
+
const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
|
|
24
|
+
const ACTION_TIMEOUT_MIN_MS = 100;
|
|
25
|
+
const ACTION_TIMEOUT_MAX_MS = 5 * 60 * 1000;
|
|
26
|
+
|
|
27
|
+
function utf8ByteLength(s) {
|
|
28
|
+
return new TextEncoder().encode(s).length;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function validateManifestActions(actions, errors) {
|
|
32
|
+
if (!Array.isArray(actions)) {
|
|
33
|
+
errors.push("manifest.actions must be an array (omit it or use [] for none)");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const seenKeys = new Set();
|
|
37
|
+
for (const a of actions) {
|
|
38
|
+
if (a === null || typeof a !== "object") {
|
|
39
|
+
errors.push("manifest.actions entries must be objects");
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
if (!isNonEmptyString(a.key)) {
|
|
43
|
+
errors.push("manifest.actions[].key must be a non-empty string");
|
|
44
|
+
} else if (seenKeys.has(a.key)) {
|
|
45
|
+
errors.push(`manifest.actions[].key "${a.key}" is duplicated`);
|
|
46
|
+
} else {
|
|
47
|
+
seenKeys.add(a.key);
|
|
48
|
+
}
|
|
49
|
+
pushIf(errors, isNonEmptyString(a.name), "manifest.actions[].name must be a non-empty string");
|
|
50
|
+
if (!VALID_ACTION_TRIGGERS.has(a.triggerType)) {
|
|
51
|
+
errors.push(
|
|
52
|
+
`manifest.actions[].triggerType must be one of ${[...VALID_ACTION_TRIGGERS].join(", ")}`,
|
|
53
|
+
);
|
|
54
|
+
} else if (a.triggerType === "schedule") {
|
|
55
|
+
pushIf(
|
|
56
|
+
errors,
|
|
57
|
+
isNonEmptyString(a.scheduleCron),
|
|
58
|
+
"manifest.actions[].scheduleCron is required when triggerType is 'schedule'",
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (!isNonEmptyString(a.scriptSource)) {
|
|
62
|
+
errors.push("manifest.actions[].scriptSource must be a non-empty string");
|
|
63
|
+
} else if (utf8ByteLength(a.scriptSource) > ACTION_SCRIPT_MAX_BYTES) {
|
|
64
|
+
errors.push("manifest.actions[].scriptSource exceeds 200 KiB");
|
|
65
|
+
}
|
|
66
|
+
if (a.timeoutMs !== undefined) {
|
|
67
|
+
const t = Number(a.timeoutMs);
|
|
68
|
+
if (!Number.isFinite(t) || t < ACTION_TIMEOUT_MIN_MS || t > ACTION_TIMEOUT_MAX_MS) {
|
|
69
|
+
errors.push("manifest.actions[].timeoutMs must be between 100 and 300000");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (a.triggerTableId !== undefined || a.apiKeyId !== undefined) {
|
|
73
|
+
errors.push(
|
|
74
|
+
"manifest.actions[] must not include triggerTableId or apiKeyId — those are tenant-local and bound after install",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
13
80
|
function canonicalCategory(c) {
|
|
14
81
|
if (typeof c !== "string") return null;
|
|
15
82
|
const upper = c.toUpperCase();
|
|
@@ -20,6 +87,7 @@ function canonicalCategory(c) {
|
|
|
20
87
|
"DATA",
|
|
21
88
|
"MEDIA",
|
|
22
89
|
"COMMUNICATION",
|
|
90
|
+
"ADMINISTRATION",
|
|
23
91
|
"CUSTOM",
|
|
24
92
|
].includes(upper)
|
|
25
93
|
? upper
|
|
@@ -135,6 +203,11 @@ function validateManifest(m) {
|
|
|
135
203
|
}
|
|
136
204
|
}
|
|
137
205
|
|
|
206
|
+
// `actions` is optional (additive in SDK 1.8.0) — only validate when present.
|
|
207
|
+
if (manifest.actions !== undefined) {
|
|
208
|
+
validateManifestActions(manifest.actions, errors);
|
|
209
|
+
}
|
|
210
|
+
|
|
138
211
|
return errors.length === 0 ? { ok: true } : { ok: false, errors };
|
|
139
212
|
}
|
|
140
213
|
|
package/dist/manifest.js
CHANGED
|
@@ -5,11 +5,78 @@
|
|
|
5
5
|
// implementation.
|
|
6
6
|
|
|
7
7
|
const VALID_CATEGORIES = new Set([
|
|
8
|
-
"input", "display", "layout", "data", "media", "communication", "custom",
|
|
9
|
-
"INPUT", "DISPLAY", "LAYOUT", "DATA", "MEDIA", "COMMUNICATION", "CUSTOM",
|
|
8
|
+
"input", "display", "layout", "data", "media", "communication", "administration", "custom",
|
|
9
|
+
"INPUT", "DISPLAY", "LAYOUT", "DATA", "MEDIA", "COMMUNICATION", "ADMINISTRATION", "CUSTOM",
|
|
10
10
|
]);
|
|
11
11
|
const VALID_PLATFORMS = new Set(["web", "native"]);
|
|
12
12
|
|
|
13
|
+
// REQ-WIDGET-ACTION — structural validation for manifest-declared server
|
|
14
|
+
// actions. Mirrors the backend action.service caps; the runner re-validates
|
|
15
|
+
// the cron expression (node-cron) and binds the tenant-local API key + table
|
|
16
|
+
// at enable time, so we only check shape + size here (cross-env: no Buffer).
|
|
17
|
+
const VALID_ACTION_TRIGGERS = new Set([
|
|
18
|
+
"schedule",
|
|
19
|
+
"record_created",
|
|
20
|
+
"record_updated",
|
|
21
|
+
"record_deleted",
|
|
22
|
+
]);
|
|
23
|
+
const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
|
|
24
|
+
const ACTION_TIMEOUT_MIN_MS = 100;
|
|
25
|
+
const ACTION_TIMEOUT_MAX_MS = 5 * 60 * 1000;
|
|
26
|
+
|
|
27
|
+
function utf8ByteLength(s) {
|
|
28
|
+
return new TextEncoder().encode(s).length;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function validateManifestActions(actions, errors) {
|
|
32
|
+
if (!Array.isArray(actions)) {
|
|
33
|
+
errors.push("manifest.actions must be an array (omit it or use [] for none)");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const seenKeys = new Set();
|
|
37
|
+
for (const a of actions) {
|
|
38
|
+
if (a === null || typeof a !== "object") {
|
|
39
|
+
errors.push("manifest.actions entries must be objects");
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
if (!isNonEmptyString(a.key)) {
|
|
43
|
+
errors.push("manifest.actions[].key must be a non-empty string");
|
|
44
|
+
} else if (seenKeys.has(a.key)) {
|
|
45
|
+
errors.push(`manifest.actions[].key "${a.key}" is duplicated`);
|
|
46
|
+
} else {
|
|
47
|
+
seenKeys.add(a.key);
|
|
48
|
+
}
|
|
49
|
+
pushIf(errors, isNonEmptyString(a.name), "manifest.actions[].name must be a non-empty string");
|
|
50
|
+
if (!VALID_ACTION_TRIGGERS.has(a.triggerType)) {
|
|
51
|
+
errors.push(
|
|
52
|
+
`manifest.actions[].triggerType must be one of ${[...VALID_ACTION_TRIGGERS].join(", ")}`,
|
|
53
|
+
);
|
|
54
|
+
} else if (a.triggerType === "schedule") {
|
|
55
|
+
pushIf(
|
|
56
|
+
errors,
|
|
57
|
+
isNonEmptyString(a.scheduleCron),
|
|
58
|
+
"manifest.actions[].scheduleCron is required when triggerType is 'schedule'",
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (!isNonEmptyString(a.scriptSource)) {
|
|
62
|
+
errors.push("manifest.actions[].scriptSource must be a non-empty string");
|
|
63
|
+
} else if (utf8ByteLength(a.scriptSource) > ACTION_SCRIPT_MAX_BYTES) {
|
|
64
|
+
errors.push("manifest.actions[].scriptSource exceeds 200 KiB");
|
|
65
|
+
}
|
|
66
|
+
if (a.timeoutMs !== undefined) {
|
|
67
|
+
const t = Number(a.timeoutMs);
|
|
68
|
+
if (!Number.isFinite(t) || t < ACTION_TIMEOUT_MIN_MS || t > ACTION_TIMEOUT_MAX_MS) {
|
|
69
|
+
errors.push("manifest.actions[].timeoutMs must be between 100 and 300000");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (a.triggerTableId !== undefined || a.apiKeyId !== undefined) {
|
|
73
|
+
errors.push(
|
|
74
|
+
"manifest.actions[] must not include triggerTableId or apiKeyId — those are tenant-local and bound after install",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
13
80
|
function canonicalCategory(c) {
|
|
14
81
|
if (typeof c !== "string") return null;
|
|
15
82
|
const upper = c.toUpperCase();
|
|
@@ -20,6 +87,7 @@ function canonicalCategory(c) {
|
|
|
20
87
|
"DATA",
|
|
21
88
|
"MEDIA",
|
|
22
89
|
"COMMUNICATION",
|
|
90
|
+
"ADMINISTRATION",
|
|
23
91
|
"CUSTOM",
|
|
24
92
|
].includes(upper)
|
|
25
93
|
? upper
|
|
@@ -135,6 +203,11 @@ function validateManifest(m) {
|
|
|
135
203
|
}
|
|
136
204
|
}
|
|
137
205
|
|
|
206
|
+
// `actions` is optional (additive in SDK 1.8.0) — only validate when present.
|
|
207
|
+
if (manifest.actions !== undefined) {
|
|
208
|
+
validateManifestActions(manifest.actions, errors);
|
|
209
|
+
}
|
|
210
|
+
|
|
138
211
|
return errors.length === 0 ? { ok: true } : { ok: false, errors };
|
|
139
212
|
}
|
|
140
213
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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__/hooks-schema.test.js src/__tests__/hooks-record-permissions.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__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/linter-users-scope.test.js src/__tests__/manifest-actions.test.js"
|
|
39
39
|
},
|
|
40
40
|
"engines": {
|
|
41
41
|
"node": ">=18"
|