@friggframework/core 2.0.0--canary.696.5d9f1be.0 → 2.0.0--canary.698.b1a2d30.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/integrations/data-defined/README.md +31 -1
- package/integrations/data-defined/capabilities/AUDIT.md +451 -0
- package/integrations/data-defined/capabilities/asana.capabilities.json +22 -13
- package/integrations/data-defined/capabilities/fathom.capabilities.json +16 -6
- package/integrations/data-defined/capabilities/hubspot.capabilities.json +78 -27
- package/integrations/data-defined/capabilities/linear.capabilities.json +30 -10
- package/integrations/data-defined/capabilities/phone.capabilities.json +24 -7
- package/integrations/data-defined/capabilities/pipedrive.capabilities.json +26 -11
- package/integrations/data-defined/capabilities/salesforce.capabilities.json +64 -18
- package/integrations/data-defined/capabilities/slack.capabilities.json +12 -5
- package/integrations/data-defined/capabilities/stripe.capabilities.json +28 -10
- package/integrations/data-defined/data-defined-integration.js +2 -2
- package/integrations/data-defined/examples/asana-slack.definition.json +4 -5
- package/integrations/data-defined/examples/hubspot-slack.definition.json +4 -4
- package/integrations/data-defined/examples/klaviyo-hubspot.definition.json +1 -4
- package/integrations/data-defined/examples/phone-hubspot.definition.json +1 -5
- package/integrations/data-defined/expressions.js +2 -0
- package/integrations/data-defined/index.js +6 -0
- package/integrations/data-defined/manifests.js +18 -0
- package/integrations/data-defined/renderers/common.js +67 -0
- package/integrations/data-defined/renderers/html.js +129 -0
- package/integrations/data-defined/renderers/hubspot.js +54 -0
- package/integrations/data-defined/renderers/preview.js +154 -0
- package/integrations/data-defined/renderers/slack.js +146 -0
- package/integrations/data-defined/runtime.js +17 -2
- package/integrations/data-defined/schema/capability-manifest.v0.1.schema.json +40 -0
- package/integrations/data-defined/scripts/check-manifests.js +220 -0
- package/integrations/data-defined/tests/helpers-fake-crm.js +11 -6
- package/integrations/data-defined/validate.js +6 -0
- package/package.json +5 -5
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Check capability manifests against the real API modules they describe (ADR-063 phase 4: real manifests).
|
|
4
|
+
*
|
|
5
|
+
* node scripts/check-manifests.js <modules-dir> [<modules-dir> ...]
|
|
6
|
+
* node scripts/check-manifests.js --manifests capabilities/ --json ~/api-module-library/packages
|
|
7
|
+
* node scripts/check-manifests.js --stub-missing --strict ~/api-module-library/packages
|
|
8
|
+
*
|
|
9
|
+
* For every manifest it finds the module (a directory named <module> or api-module-<module>, or whose package.json
|
|
10
|
+
* name ends in that, up to three levels under each given directory; the first one that loads wins), loads its Api
|
|
11
|
+
* class and checks, with no network and no instance, on the prototype:
|
|
12
|
+
* - every capability's `method` exists (a capability marked `status: "unimplemented"` must NOT have it: the
|
|
13
|
+
* manifest would be stale);
|
|
14
|
+
* - the arguments fit the method's declared parameters: `args` no longer than the parameter list, and a
|
|
15
|
+
* capability with no `args` (called with one object) is not a method that needs two or more positional ones;
|
|
16
|
+
* - `webhook.verify` names a method that exists.
|
|
17
|
+
*
|
|
18
|
+
* Exit codes: 0 every method found (unimplemented capabilities and modules with no source are reported, not failed,
|
|
19
|
+
* unless --strict) · 1 a mismatch · 2 could not run (bad arguments, no manifests).
|
|
20
|
+
*
|
|
21
|
+
* --manifests <dir> manifests to check (default: ../capabilities)
|
|
22
|
+
* --json machine-readable result on stdout
|
|
23
|
+
* --strict unimplemented capabilities, unloadable modules and modules not found fail too
|
|
24
|
+
* --stub-missing load modules whose own dependencies are not installed (jsforce, stripe, @linear/sdk ...) by
|
|
25
|
+
* standing in for the missing packages; enough to read the Api's methods, never to call them
|
|
26
|
+
*/
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const Module = require('module');
|
|
30
|
+
|
|
31
|
+
const FOUND = 'ok';
|
|
32
|
+
|
|
33
|
+
/** Parameters of a function as written: [{ name, optional }]. Reads the source, so it is a heuristic for the arity check. */
|
|
34
|
+
function parametersOf(fn) {
|
|
35
|
+
const src = Function.prototype.toString.call(fn);
|
|
36
|
+
const bare = /^(?:async\s+)?([\w$]+)\s*=>/.exec(src);
|
|
37
|
+
if (bare) return [{ text: bare[1], optional: false }];
|
|
38
|
+
const open = src.indexOf('(');
|
|
39
|
+
if (open < 0) return [];
|
|
40
|
+
let depth = 0, i = open, cur = '';
|
|
41
|
+
const parts = [];
|
|
42
|
+
for (; i < src.length; i++) {
|
|
43
|
+
const c = src[i];
|
|
44
|
+
if ('([{'.includes(c)) { depth++; if (depth === 1) continue; }
|
|
45
|
+
if (')]}'.includes(c)) { depth--; if (depth === 0) break; }
|
|
46
|
+
if (c === ',' && depth === 1) { parts.push(cur); cur = ''; } else cur += c;
|
|
47
|
+
}
|
|
48
|
+
if (cur.trim()) parts.push(cur);
|
|
49
|
+
return parts.map((p) => p.replace(/\/\*[\s\S]*?\*\//g, '').trim()).filter(Boolean).map((p) => ({ text: p.replace(/\s+/g, ' '), optional: /^\.\.\./.test(p) || p.includes('=') || /^[\w$]+\s*\?/.test(p) }));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function methodOf(Api, name) {
|
|
53
|
+
for (let p = Api && Api.prototype; p && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
|
|
54
|
+
const d = Object.getOwnPropertyDescriptor(p, name);
|
|
55
|
+
if (d) return typeof d.value === 'function' ? d.value : null;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const signatureOf = (fn) => `(${parametersOf(fn).map((p) => p.text).join(', ')})`;
|
|
61
|
+
|
|
62
|
+
const looksLikeModule = (dir) => ['package.json', 'index.js', 'api.js'].some((f) => fs.existsSync(path.join(dir, f)));
|
|
63
|
+
|
|
64
|
+
/** Directories that could be the module: name match, up to `depth` levels below root. */
|
|
65
|
+
function candidates(root, moduleName, depth = 3) {
|
|
66
|
+
const found = [];
|
|
67
|
+
const isMatch = (dir) => {
|
|
68
|
+
const base = path.basename(dir);
|
|
69
|
+
if (base === moduleName || base === `api-module-${moduleName}`) return true;
|
|
70
|
+
try { return JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).name?.endsWith(`api-module-${moduleName}`); } catch { return false; }
|
|
71
|
+
};
|
|
72
|
+
const walk = (dir, level) => {
|
|
73
|
+
if (level > depth) return;
|
|
74
|
+
let entries;
|
|
75
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
76
|
+
for (const e of entries) {
|
|
77
|
+
if (!e.isDirectory() || e.name === 'node_modules' || e.name.startsWith('.')) continue;
|
|
78
|
+
const full = path.join(dir, e.name);
|
|
79
|
+
if (isMatch(full) && looksLikeModule(full)) found.push(full);
|
|
80
|
+
else walk(full, level + 1);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
if (isMatch(root) && looksLikeModule(root)) found.push(root);
|
|
84
|
+
walk(root, 1);
|
|
85
|
+
return found;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Stand-ins for dependencies that are not installed, so an Api's methods can be read. Restored by the caller. */
|
|
89
|
+
function stubMissingDependencies() {
|
|
90
|
+
const original = Module._load;
|
|
91
|
+
const stub = () => new Proxy(function stubbed() {}, { get: (t, k) => (k === '__esModule' ? false : k === 'prototype' ? t.prototype : t[k] || (t[k] = stub())), construct: () => ({}), apply: () => stub() });
|
|
92
|
+
Module._load = function load(request, ...rest) {
|
|
93
|
+
try { return original.call(this, request, ...rest); } catch (e) {
|
|
94
|
+
if (e && e.code === 'MODULE_NOT_FOUND' && !request.startsWith('.') && !path.isAbsolute(request)) return stub();
|
|
95
|
+
throw e;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
return () => { Module._load = original; };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function loadApi(dir) {
|
|
102
|
+
const attempts = [dir, path.join(dir, 'api.js'), path.join(dir, 'src', 'api.js'), path.join(dir, 'dist', 'api.js')];
|
|
103
|
+
let firstError;
|
|
104
|
+
for (const target of attempts) {
|
|
105
|
+
try {
|
|
106
|
+
const mod = require(target);
|
|
107
|
+
const Api = mod.Api || mod.API || mod.Definition?.API || (typeof mod === 'function' ? mod : null);
|
|
108
|
+
if (typeof Api === 'function' && Api.prototype) return { Api, from: path.relative(dir, require.resolve(target)) || '.' };
|
|
109
|
+
firstError = firstError || new Error('exports no Api class');
|
|
110
|
+
} catch (e) { firstError = firstError || e; } // the first failure is the module's own; later ones are only missing fallbacks
|
|
111
|
+
}
|
|
112
|
+
return { error: (firstError?.message || 'cannot load').split('\n')[0] };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {Record<string, object>} manifests module -> manifest
|
|
117
|
+
* @param {string[]} roots directories to look for the modules in
|
|
118
|
+
* @returns {Array<{module, package?, source?, state, message?, rows: Array<{capability, method, state, signature?, args?, note?}>}>}
|
|
119
|
+
*/
|
|
120
|
+
function checkManifests(manifests, roots, { stubMissing = false } = {}) {
|
|
121
|
+
const restore = stubMissing ? stubMissingDependencies() : null;
|
|
122
|
+
try {
|
|
123
|
+
return Object.values(manifests).map((m) => {
|
|
124
|
+
const result = { module: m.module, package: m.package, state: FOUND, rows: [] };
|
|
125
|
+
const dirs = roots.flatMap((r) => candidates(path.resolve(r), m.module));
|
|
126
|
+
if (!dirs.length) return { ...result, state: 'no-module', message: `No directory named ${m.module} or api-module-${m.module} under ${roots.join(', ')}` };
|
|
127
|
+
let loaded, failures = [];
|
|
128
|
+
for (const dir of dirs) {
|
|
129
|
+
const l = loadApi(dir);
|
|
130
|
+
if (l.Api) { loaded = { ...l, dir }; break; }
|
|
131
|
+
failures.push(`${dir}: ${l.error}`);
|
|
132
|
+
}
|
|
133
|
+
if (!loaded) return { ...result, state: 'unloadable', message: failures.join('; ') };
|
|
134
|
+
result.source = loaded.dir;
|
|
135
|
+
const { Api } = loaded;
|
|
136
|
+
for (const [name, c] of Object.entries(m.capabilities || {})) {
|
|
137
|
+
const fn = methodOf(Api, c.method);
|
|
138
|
+
const row = { capability: name, method: c.method };
|
|
139
|
+
if (c.status === 'unimplemented') {
|
|
140
|
+
row.state = fn ? 'stale-unimplemented' : 'unimplemented';
|
|
141
|
+
row.note = fn ? `Marked unimplemented, but ${c.method}${signatureOf(fn)} exists: ${c.note || 'no note says why it cannot be used'}` : c.note;
|
|
142
|
+
if (fn) row.signature = signatureOf(fn);
|
|
143
|
+
} else if (!fn) {
|
|
144
|
+
row.state = 'missing';
|
|
145
|
+
} else {
|
|
146
|
+
row.signature = signatureOf(fn);
|
|
147
|
+
const params = parametersOf(fn);
|
|
148
|
+
const required = params.filter((p) => !p.optional).length;
|
|
149
|
+
if (c.args) {
|
|
150
|
+
row.args = c.args.map((a) => (typeof a === 'string' ? a : `=${JSON.stringify(a.value)}`)).join(', ');
|
|
151
|
+
row.state = c.args.length > params.length && !params.some((p) => p.text.startsWith('...')) ? 'arity' : 'ok';
|
|
152
|
+
if (row.state === 'arity') row.note = `Passes ${c.args.length} arguments; ${c.method} declares ${params.length}`;
|
|
153
|
+
} else {
|
|
154
|
+
row.state = required > 1 ? 'arity' : 'ok';
|
|
155
|
+
if (row.state === 'arity') row.note = `Called with one object; ${c.method} needs ${required} positional arguments (add args)`;
|
|
156
|
+
}
|
|
157
|
+
if (c.note) row.note = [row.note, c.note].filter(Boolean).join('. ');
|
|
158
|
+
}
|
|
159
|
+
result.rows.push(row);
|
|
160
|
+
}
|
|
161
|
+
if (m.webhook?.verify) {
|
|
162
|
+
const fn = methodOf(Api, m.webhook.verify);
|
|
163
|
+
const marked = m.webhook.status === 'unimplemented';
|
|
164
|
+
result.rows.push({ capability: 'webhook.verify', method: m.webhook.verify, state: fn ? (marked ? 'stale-unimplemented' : 'ok') : marked ? 'unimplemented' : 'missing', ...(fn ? { signature: signatureOf(fn) } : {}), ...(m.webhook.note ? { note: m.webhook.note } : {}) });
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
});
|
|
168
|
+
} finally { if (restore) restore(); }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// stale-unimplemented is soft: the method exists but the note may say why it cannot be used as the capability is shaped.
|
|
172
|
+
const BAD = new Set(['missing', 'arity']);
|
|
173
|
+
const SOFT = new Set(['unimplemented', 'stale-unimplemented', 'unloadable', 'no-module']);
|
|
174
|
+
|
|
175
|
+
function summarise(results) {
|
|
176
|
+
const counts = {};
|
|
177
|
+
for (const r of results) {
|
|
178
|
+
counts[r.state] = (counts[r.state] || 0) + (r.state === FOUND ? 0 : 1);
|
|
179
|
+
for (const row of r.rows) counts[row.state] = (counts[row.state] || 0) + 1;
|
|
180
|
+
}
|
|
181
|
+
const mismatches = results.flatMap((r) => r.rows.filter((x) => BAD.has(x.state)));
|
|
182
|
+
const soft = results.filter((r) => SOFT.has(r.state)).length + results.flatMap((r) => r.rows).filter((x) => SOFT.has(x.state)).length;
|
|
183
|
+
return { counts, mismatches: mismatches.length, soft };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function main(argv) {
|
|
187
|
+
const args = [...argv];
|
|
188
|
+
const flag = (n) => { const i = args.indexOf(n); if (i < 0) return false; args.splice(i, 1); return true; };
|
|
189
|
+
const option = (n) => { const i = args.indexOf(n); if (i < 0) return null; const v = args[i + 1]; args.splice(i, 2); return v; };
|
|
190
|
+
const strict = flag('--strict'), asJson = flag('--json'), stubMissing = flag('--stub-missing');
|
|
191
|
+
const manifestDir = option('--manifests') || path.join(__dirname, '..', 'capabilities');
|
|
192
|
+
const unknown = args.find((a) => a.startsWith('--'));
|
|
193
|
+
if (unknown) { console.error(`Unknown option ${unknown}`); return 2; }
|
|
194
|
+
if (!args.length) { console.error('Usage: check-manifests.js [--manifests dir] [--json] [--strict] [--stub-missing] <modules-dir> [...]'); return 2; }
|
|
195
|
+
for (const r of args) if (!fs.existsSync(r)) { console.error(`No such directory: ${r}`); return 2; }
|
|
196
|
+
if (!fs.existsSync(manifestDir)) { console.error(`No such directory: ${manifestDir}`); return 2; }
|
|
197
|
+
const manifests = {};
|
|
198
|
+
for (const f of fs.readdirSync(manifestDir).filter((x) => x.endsWith('.capabilities.json'))) {
|
|
199
|
+
try { const m = JSON.parse(fs.readFileSync(path.join(manifestDir, f), 'utf8')); manifests[m.module] = m; } catch (e) { console.error(`${f}: not valid JSON (${e.message})`); return 2; }
|
|
200
|
+
}
|
|
201
|
+
if (!Object.keys(manifests).length) { console.error(`No *.capabilities.json in ${manifestDir}`); return 2; }
|
|
202
|
+
|
|
203
|
+
const results = checkManifests(manifests, args, { stubMissing });
|
|
204
|
+
const sum = summarise(results);
|
|
205
|
+
const failed = sum.mismatches > 0 || (strict && sum.soft > 0);
|
|
206
|
+
if (asJson) process.stdout.write(`${JSON.stringify({ valid: !failed, strict, summary: sum, results }, null, 2)}\n`);
|
|
207
|
+
else {
|
|
208
|
+
for (const r of results) {
|
|
209
|
+
if (r.state !== FOUND) { console.log(`${strict ? 'FAIL' : 'skip'} ${r.module.padEnd(12)} ${r.state}: ${r.message}`); continue; }
|
|
210
|
+
const bad = r.rows.filter((x) => BAD.has(x.state)).length;
|
|
211
|
+
console.log(`${bad ? 'FAIL' : 'ok '} ${r.module.padEnd(12)} ${r.rows.filter((x) => x.state === 'ok').length}/${r.rows.length} ok (${path.relative(process.cwd(), r.source) || '.'})`);
|
|
212
|
+
for (const x of r.rows.filter((y) => y.state !== 'ok')) console.log(` ${x.state.padEnd(20)} ${x.capability} -> ${x.method}${x.note ? ` ${x.note}` : ''}`);
|
|
213
|
+
}
|
|
214
|
+
console.log(`\n${results.length} manifests: ${sum.mismatches} mismatches, ${sum.soft} unverifiable or unimplemented${strict ? ' (strict)' : ''}`);
|
|
215
|
+
}
|
|
216
|
+
return failed ? 1 : 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (require.main === module) process.exit(main(process.argv.slice(2)));
|
|
220
|
+
module.exports = { checkManifests, summarise, parametersOf, methodOf, candidates };
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* In-memory HubSpot and Phone APIs with the real Api classes' call shapes (the manifests' `args`): HubSpot's
|
|
3
|
+
* getContactById(id), createContact(properties), updateContact(id, properties); Phone's getContact(id) and
|
|
4
|
+
* updateContact(id, { defaultFields }). searchContacts is on both only because the sync tests need a search; neither
|
|
5
|
+
* real Api has one (the manifests mark contact.search unimplemented). Test helper.
|
|
6
|
+
*/
|
|
2
7
|
/** A fake API: records by id, a write log, and the methods the manifest names. */
|
|
3
8
|
function hubspotApi() {
|
|
4
9
|
const records = new Map();
|
|
@@ -6,7 +11,7 @@ function hubspotApi() {
|
|
|
6
11
|
const api = {
|
|
7
12
|
records, writes: [],
|
|
8
13
|
seed(props, extra = {}) { const id = String(++n); records.set(id, { id, properties: { ...props }, updatedAt: extra.updatedAt || '2026-10-01T00:00:00Z' }); return id; },
|
|
9
|
-
async getContactById(
|
|
14
|
+
async getContactById(id) { return records.get(String(id)) || null; },
|
|
10
15
|
async searchContacts({ email, phone }) { return [...records.values()].filter((r) => (email && r.properties.email === email) || (phone && r.properties.phone === phone)); },
|
|
11
16
|
async listContacts({ after, limit = 100, updatedSince }) {
|
|
12
17
|
const all = [...records.values()].filter((r) => !updatedSince || Date.parse(r.updatedAt) > Date.parse(updatedSince));
|
|
@@ -14,8 +19,8 @@ function hubspotApi() {
|
|
|
14
19
|
const page = all.slice(start, start + limit);
|
|
15
20
|
return { results: page, ...(start + limit < all.length ? { paging: { next: { after: String(start + limit) } } } : {}) };
|
|
16
21
|
},
|
|
17
|
-
async createContact(
|
|
18
|
-
async updateContact(
|
|
22
|
+
async createContact(properties) { api.writes.push(['create', properties]); const id = api.seed(properties, { updatedAt: '2026-10-02T00:00:00Z' }); return records.get(id); },
|
|
23
|
+
async updateContact(id, properties) { api.writes.push(['update', id, properties]); const r = records.get(String(id)); Object.assign(r.properties, properties); r.updatedAt = '2026-10-03T00:00:00Z'; return r; },
|
|
19
24
|
};
|
|
20
25
|
return api;
|
|
21
26
|
}
|
|
@@ -25,7 +30,7 @@ function phoneApi() {
|
|
|
25
30
|
const api = {
|
|
26
31
|
records, writes: [], searches: [],
|
|
27
32
|
seed(fields, extra = {}) { const id = `CT${++n}`; records.set(id, { id, defaultFields: JSON.parse(JSON.stringify(fields)), ...extra }); return id; },
|
|
28
|
-
async getContact(
|
|
33
|
+
async getContact(id) { return records.get(id) || null; },
|
|
29
34
|
async searchContacts(args) {
|
|
30
35
|
api.searches.push(args);
|
|
31
36
|
return [...records.values()].filter((r) => (args.phone && r.defaultFields.phoneNumbers?.[0]?.value === args.phone) || (args.email && r.defaultFields.emails?.[0]?.value === args.email));
|
|
@@ -36,7 +41,7 @@ function phoneApi() {
|
|
|
36
41
|
return { data: all.slice(start, start + maxResults), ...(start + maxResults < all.length ? { nextPageToken: String(start + maxResults) } : {}) };
|
|
37
42
|
},
|
|
38
43
|
async createContact({ defaultFields, externalId }) { api.writes.push(['create', defaultFields, externalId]); const id = api.seed(defaultFields, { externalId }); return records.get(id); },
|
|
39
|
-
async updateContact(
|
|
44
|
+
async updateContact(id, { defaultFields }) { api.writes.push(['update', id, defaultFields]); const r = records.get(id); r.defaultFields = { ...r.defaultFields, ...defaultFields }; return r; },
|
|
40
45
|
};
|
|
41
46
|
return api;
|
|
42
47
|
}
|
|
@@ -100,6 +100,7 @@ function validate(def, manifests = {}, { codeMethods } = {}) {
|
|
|
100
100
|
const list = manifestOf[mk].options?.[ok];
|
|
101
101
|
if (!list) { err(p, `Module "${mk}" has no option list "${ok}"`); return; }
|
|
102
102
|
const inputs = list.inputs || {};
|
|
103
|
+
if (manifestOf[mk].capabilities?.[list.capability]?.status === 'unimplemented') warn(p, `Option list ${ref.source} reads ${list.capability}, which is unimplemented in the ${mk} Api; the dropdown will not load`);
|
|
103
104
|
for (const [name, spec] of Object.entries(inputs)) if (spec.required && !(name in (ref.with || {}))) err(`${p}.with.${name}`, `Option list ${ref.source} needs input "${name}"`);
|
|
104
105
|
for (const [name, v] of Object.entries(ref.with || {})) {
|
|
105
106
|
if (!(name in inputs)) { err(`${p}.with.${name}`, `Option list ${ref.source} takes no input "${name}"`); continue; }
|
|
@@ -177,6 +178,9 @@ function validate(def, manifests = {}, { codeMethods } = {}) {
|
|
|
177
178
|
const toName = `${to.k}.${to.o}`;
|
|
178
179
|
if (!fs) { err(`${p}.${from === src ? 'source' : 'target'}`, `${fromName} declares no sync block (objects.${from.o}.sync), so it cannot be read`); continue; }
|
|
179
180
|
if (!ts) { err(`${p}.${to === tgt ? 'target' : 'source'}`, `${toName} declares no sync block (objects.${to.o}.sync), so it cannot be written`); continue; }
|
|
181
|
+
for (const [spec, man, nm, where] of [[fs, manifestOf[from.k], fromName, from === src ? 'source' : 'target'], [ts, manifestOf[to.k], toName, to === tgt ? 'target' : 'source']]) {
|
|
182
|
+
for (const op of Object.values(spec)) if (op?.capability && man.capabilities?.[op.capability]?.status === 'unimplemented') warn(`${p}.${where}`, `${nm} syncs through ${op.capability}, which is unimplemented in the ${man.module} Api`);
|
|
183
|
+
}
|
|
180
184
|
if (!ts.create && !ts.update) err(`${p}.${to === tgt ? 'target' : 'source'}`, `${toName} has neither sync.create nor sync.update`);
|
|
181
185
|
if ((s.backfill?.trigger || 'none') !== 'none' && !fs.list && from === src) err(`${p}.backfill`, `A backfill pages through ${fromName}, which has no sync.list`);
|
|
182
186
|
// A two-way backfill also reads the target when it can list; when it cannot, it reads only the source.
|
|
@@ -305,6 +309,7 @@ function validate(def, manifests = {}, { codeMethods } = {}) {
|
|
|
305
309
|
const c = man.capabilities?.[cap.capability];
|
|
306
310
|
if (!c) err(`${p}.use`, `Module "${cap.module}" (${man.module}) has no capability "${cap.capability}"`);
|
|
307
311
|
else {
|
|
312
|
+
if (c.status === 'unimplemented') warn(`${p}.use`, `${st.use} is unimplemented in the ${man.module} Api${c.note ? ` (${c.note})` : ''}; this step will fail at run time until the module adds it`);
|
|
308
313
|
for (const [name, spec] of Object.entries(c.input || {})) if (spec.required && !(name in args)) err(`${p}.with.${name}`, `Required by ${st.use}`);
|
|
309
314
|
for (const name of Object.keys(args)) if (c.input && !(name in c.input)) err(`${p}.with.${name}`, `${st.use} takes no argument "${name}"`);
|
|
310
315
|
}
|
|
@@ -329,6 +334,7 @@ function validate(def, manifests = {}, { codeMethods } = {}) {
|
|
|
329
334
|
if (t.type === 'webhook') {
|
|
330
335
|
if (!manifestOf[t.module]) err(`${p}.trigger.module`, `Unknown module key "${t.module}"`);
|
|
331
336
|
else if (!manifestOf[t.module].events?.[t.topic]) err(`${p}.trigger.topic`, `Module "${t.module}" emits no event "${t.topic}"`);
|
|
337
|
+
else if (manifestOf[t.module].webhook?.status === 'unimplemented' && (manifestOf[t.module].events[t.topic].delivery || 'webhook') === 'webhook') warn(`${p}.trigger`, `${t.module} events arrive by webhook, but the ${manifestOf[t.module].module} Api has no ${manifestOf[t.module].webhook.verify || 'verify'} method, so every delivery fails signature verification`);
|
|
332
338
|
}
|
|
333
339
|
if (t.type === 'schedule') {
|
|
334
340
|
if ((t.cron === undefined) === (t.every === undefined)) err(`${p}.trigger`, 'Give exactly one of cron or every');
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.
|
|
4
|
+
"version": "2.0.0--canary.698.b1a2d30.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@friggframework/eslint-config": "2.0.0--canary.
|
|
51
|
-
"@friggframework/prettier-config": "2.0.0--canary.
|
|
52
|
-
"@friggframework/test": "2.0.0--canary.
|
|
50
|
+
"@friggframework/eslint-config": "2.0.0--canary.698.b1a2d30.0",
|
|
51
|
+
"@friggframework/prettier-config": "2.0.0--canary.698.b1a2d30.0",
|
|
52
|
+
"@friggframework/test": "2.0.0--canary.698.b1a2d30.0",
|
|
53
53
|
"@prisma/client": "^6.19.3",
|
|
54
54
|
"@types/lodash": "4.17.15",
|
|
55
55
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -90,5 +90,5 @@
|
|
|
90
90
|
"publishConfig": {
|
|
91
91
|
"access": "public"
|
|
92
92
|
},
|
|
93
|
-
"gitHead": "
|
|
93
|
+
"gitHead": "b1a2d30f3296992d698742a0c0a99b916c7fbfbe"
|
|
94
94
|
}
|