khotan-data 0.1.0 → 0.2.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 +89 -6
- package/dist/cli.js +405 -35
- package/dist/factory.cjs +1160 -106
- package/dist/factory.cjs.map +1 -1
- package/dist/factory.d.cts +262 -38
- package/dist/factory.d.ts +262 -38
- package/dist/factory.js +1158 -108
- package/dist/factory.js.map +1 -1
- package/dist/templates/api-state.tsx +249 -0
- package/dist/templates/cache.example.ts +11 -0
- package/dist/templates/cache.ts +58 -0
- package/dist/templates/catch.ts +13 -1
- package/dist/templates/debug-index-page.tsx +56 -36
- package/dist/templates/hub.tsx +9 -23
- package/dist/templates/inflow.ts +5 -6
- package/dist/templates/khotan-config.ts +30 -4
- package/dist/templates/mapping-browser.tsx +773 -0
- package/dist/templates/mappings-page.tsx +9 -0
- package/dist/templates/outflow.ts +5 -5
- package/dist/templates/pass.ts +10 -0
- package/dist/templates/plug-debugger.tsx +15 -7
- package/dist/templates/relay.example.ts +11 -1
- package/dist/templates/relay.ts +16 -7
- package/dist/templates/runs-table.tsx +133 -130
- package/dist/templates/schema.ts +81 -0
- package/dist/templates/skill-plug.md +38 -15
- package/dist/templates/skill-setup.md +80 -3
- package/dist/templates/topology-canvas.tsx +19 -30
- package/dist/templates/var-panel.tsx +33 -10
- package/dist/templates/webhook-events-table.tsx +105 -102
- package/dist/templates/wire-panel.tsx +30 -8
- package/package.json +1 -1
package/dist/factory.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { eq, desc, sql, inArray, count } from 'drizzle-orm';
|
|
1
|
+
import { eq, desc, sql, inArray, count, and } from 'drizzle-orm';
|
|
2
2
|
import { pgTable, timestamp, text, boolean, unique, index, jsonb, integer } from 'drizzle-orm/pg-core';
|
|
3
3
|
|
|
4
4
|
// src/factory.ts
|
|
@@ -162,7 +162,7 @@ var khotanRuns = pgTable(
|
|
|
162
162
|
)
|
|
163
163
|
]
|
|
164
164
|
);
|
|
165
|
-
var
|
|
165
|
+
var khotanMappingsTable = pgTable(
|
|
166
166
|
"khotan_mappings",
|
|
167
167
|
{
|
|
168
168
|
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
@@ -182,6 +182,39 @@ var khotanMappings = pgTable(
|
|
|
182
182
|
index("khotan_mappings_refs_gin_idx").using("gin", table.refs)
|
|
183
183
|
]
|
|
184
184
|
);
|
|
185
|
+
var khotanCaches = pgTable(
|
|
186
|
+
"khotan_caches",
|
|
187
|
+
{
|
|
188
|
+
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
189
|
+
name: text("name").notNull().unique(),
|
|
190
|
+
scope: jsonb("scope").$type(),
|
|
191
|
+
ttlSeconds: integer("ttl_seconds"),
|
|
192
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
193
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
|
|
194
|
+
},
|
|
195
|
+
(table) => [index("khotan_caches_name_idx").on(table.name)]
|
|
196
|
+
);
|
|
197
|
+
var khotanCacheEntries = pgTable(
|
|
198
|
+
"khotan_cache_entries",
|
|
199
|
+
{
|
|
200
|
+
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
201
|
+
cacheId: text("cache_id").notNull(),
|
|
202
|
+
key: text("key").notNull(),
|
|
203
|
+
value: jsonb("value").notNull().$type(),
|
|
204
|
+
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
|
205
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
206
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
|
|
207
|
+
},
|
|
208
|
+
(table) => [
|
|
209
|
+
unique("khotan_cache_entries_cache_id_key_unique").on(
|
|
210
|
+
table.cacheId,
|
|
211
|
+
table.key
|
|
212
|
+
),
|
|
213
|
+
index("khotan_cache_entries_cache_id_idx").on(table.cacheId),
|
|
214
|
+
index("khotan_cache_entries_cache_id_key_idx").on(table.cacheId, table.key),
|
|
215
|
+
index("khotan_cache_entries_expires_at_idx").on(table.expiresAt)
|
|
216
|
+
]
|
|
217
|
+
);
|
|
185
218
|
async function deriveKey(secret) {
|
|
186
219
|
const encoded = new TextEncoder().encode(secret);
|
|
187
220
|
const hash = await crypto.subtle.digest("SHA-256", encoded);
|
|
@@ -226,6 +259,104 @@ function hexToBytes(hex) {
|
|
|
226
259
|
function bytesToHex(bytes) {
|
|
227
260
|
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
228
261
|
}
|
|
262
|
+
var CLI_TOKEN_SCHEME = "KhotanCLI";
|
|
263
|
+
var CLI_TOKEN_WINDOW_MS = 6e4;
|
|
264
|
+
async function deriveCliToken(secret, timestamp2) {
|
|
265
|
+
const key = await crypto.subtle.importKey(
|
|
266
|
+
"raw",
|
|
267
|
+
new TextEncoder().encode(secret),
|
|
268
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
269
|
+
false,
|
|
270
|
+
["sign"]
|
|
271
|
+
);
|
|
272
|
+
const sig = await crypto.subtle.sign(
|
|
273
|
+
"HMAC",
|
|
274
|
+
key,
|
|
275
|
+
new TextEncoder().encode(`khotan-cli:${timestamp2}`)
|
|
276
|
+
);
|
|
277
|
+
return bytesToHex(new Uint8Array(sig));
|
|
278
|
+
}
|
|
279
|
+
function timingSafeEqualHex(a, b) {
|
|
280
|
+
if (a.length !== b.length) return false;
|
|
281
|
+
let diff = 0;
|
|
282
|
+
for (let i = 0; i < a.length; i++) {
|
|
283
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
284
|
+
}
|
|
285
|
+
return diff === 0;
|
|
286
|
+
}
|
|
287
|
+
async function isCliRequestAuthorized(request, secret) {
|
|
288
|
+
if (process.env["NODE_ENV"] === "production") return false;
|
|
289
|
+
if (!secret) return false;
|
|
290
|
+
const header = request.headers.get("authorization");
|
|
291
|
+
if (!header?.startsWith(`${CLI_TOKEN_SCHEME} `)) return false;
|
|
292
|
+
const token = header.slice(CLI_TOKEN_SCHEME.length + 1).trim();
|
|
293
|
+
const dotIdx = token.indexOf(".");
|
|
294
|
+
if (dotIdx === -1) return false;
|
|
295
|
+
const timestamp2 = token.slice(0, dotIdx);
|
|
296
|
+
const provided = token.slice(dotIdx + 1);
|
|
297
|
+
const ts = Number.parseInt(timestamp2, 10);
|
|
298
|
+
if (!Number.isFinite(ts)) return false;
|
|
299
|
+
if (Math.abs(Date.now() - ts) > CLI_TOKEN_WINDOW_MS) return false;
|
|
300
|
+
const expected = await deriveCliToken(secret, timestamp2);
|
|
301
|
+
return timingSafeEqualHex(provided, expected);
|
|
302
|
+
}
|
|
303
|
+
function bindPlugWithVars(plug, vars, setVars) {
|
|
304
|
+
const opts = (extra) => ({
|
|
305
|
+
...extra,
|
|
306
|
+
vars,
|
|
307
|
+
...setVars ? { _setVars: setVars } : {}
|
|
308
|
+
});
|
|
309
|
+
return {
|
|
310
|
+
get(path, extra) {
|
|
311
|
+
return plug.get(path, opts(extra));
|
|
312
|
+
},
|
|
313
|
+
post(path, extra) {
|
|
314
|
+
return plug.post(path, opts(extra));
|
|
315
|
+
},
|
|
316
|
+
put(path, extra) {
|
|
317
|
+
return plug.put(path, opts(extra));
|
|
318
|
+
},
|
|
319
|
+
patch(path, extra) {
|
|
320
|
+
return plug.patch(path, opts(extra));
|
|
321
|
+
},
|
|
322
|
+
delete(path, extra) {
|
|
323
|
+
return plug.delete(path, opts(extra));
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function bindWorkflowPlug(plug, ctx, plugName = ctx.flow.plugName) {
|
|
328
|
+
const vars = plugName === ctx.flow.plugName ? ctx.vars : ctx.plugVarsByName?.[plugName] ?? {};
|
|
329
|
+
if (plugName !== ctx.flow.plugName) {
|
|
330
|
+
ctx.plugVarsByName ??= {};
|
|
331
|
+
ctx.plugVarsByName[plugName] = vars;
|
|
332
|
+
}
|
|
333
|
+
return bindPlugWithVars(plug, vars, async (updates) => {
|
|
334
|
+
Object.assign(vars, updates);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
var khotanRuntimeRegistry = /* @__PURE__ */ new Map();
|
|
338
|
+
function getWorkflowRuntimeHelpers(ctx) {
|
|
339
|
+
const helpers = khotanRuntimeRegistry.get(ctx.khotanInstanceId);
|
|
340
|
+
if (!helpers) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
`Khotan runtime helpers for instance "${ctx.khotanInstanceId}" are not registered`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
return helpers;
|
|
346
|
+
}
|
|
347
|
+
function khotanCache(ctx, cacheName) {
|
|
348
|
+
return getWorkflowRuntimeHelpers(ctx).cache(cacheName);
|
|
349
|
+
}
|
|
350
|
+
function khotanMappings(ctx) {
|
|
351
|
+
const helpers = getWorkflowRuntimeHelpers(ctx);
|
|
352
|
+
return {
|
|
353
|
+
list: helpers.listMappings,
|
|
354
|
+
lookup: helpers.lookupMapping,
|
|
355
|
+
upsert: helpers.upsertMapping,
|
|
356
|
+
update: helpers.updateMapping,
|
|
357
|
+
delete: helpers.deleteMapping
|
|
358
|
+
};
|
|
359
|
+
}
|
|
229
360
|
function drizzleAdapter(db) {
|
|
230
361
|
return {
|
|
231
362
|
async upsertPlug(plug) {
|
|
@@ -374,27 +505,93 @@ function drizzleAdapter(db) {
|
|
|
374
505
|
async upsertResource(resource) {
|
|
375
506
|
const rows = await db.insert(khotanResources).values({
|
|
376
507
|
name: resource.name,
|
|
377
|
-
connectField: resource.connectField,
|
|
508
|
+
connectField: serializeConnectField(resource.connectField),
|
|
378
509
|
description: resource.description ?? null
|
|
379
510
|
}).onConflictDoUpdate({
|
|
380
511
|
target: khotanResources.name,
|
|
381
512
|
set: {
|
|
382
|
-
connectField: resource.connectField,
|
|
513
|
+
connectField: serializeConnectField(resource.connectField),
|
|
383
514
|
description: resource.description ?? null,
|
|
384
515
|
updatedAt: /* @__PURE__ */ new Date()
|
|
385
516
|
}
|
|
386
517
|
}).returning({ id: khotanResources.id });
|
|
387
518
|
return { id: rows[0].id };
|
|
388
519
|
},
|
|
520
|
+
async upsertCache(cache) {
|
|
521
|
+
const rows = await db.insert(khotanCaches).values({
|
|
522
|
+
name: cache.name,
|
|
523
|
+
scope: cache.scope ?? null,
|
|
524
|
+
ttlSeconds: cache.ttlSeconds ?? null
|
|
525
|
+
}).onConflictDoUpdate({
|
|
526
|
+
target: khotanCaches.name,
|
|
527
|
+
set: {
|
|
528
|
+
scope: cache.scope ?? null,
|
|
529
|
+
ttlSeconds: cache.ttlSeconds ?? null,
|
|
530
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
531
|
+
}
|
|
532
|
+
}).returning({ id: khotanCaches.id });
|
|
533
|
+
return { id: rows[0].id };
|
|
534
|
+
},
|
|
535
|
+
async getCacheByName(name) {
|
|
536
|
+
const rows = await db.select().from(khotanCaches).where(eq(khotanCaches.name, name)).limit(1);
|
|
537
|
+
return rows[0] ?? null;
|
|
538
|
+
},
|
|
539
|
+
async getCacheEntry(cacheId, key) {
|
|
540
|
+
const rows = await db.select({
|
|
541
|
+
id: khotanCacheEntries.id,
|
|
542
|
+
cacheId: khotanCacheEntries.cacheId,
|
|
543
|
+
key: khotanCacheEntries.key,
|
|
544
|
+
value: khotanCacheEntries.value,
|
|
545
|
+
expiresAt: khotanCacheEntries.expiresAt,
|
|
546
|
+
createdAt: khotanCacheEntries.createdAt,
|
|
547
|
+
updatedAt: khotanCacheEntries.updatedAt
|
|
548
|
+
}).from(khotanCacheEntries).where(
|
|
549
|
+
and(
|
|
550
|
+
eq(khotanCacheEntries.cacheId, cacheId),
|
|
551
|
+
eq(khotanCacheEntries.key, key)
|
|
552
|
+
)
|
|
553
|
+
).limit(1);
|
|
554
|
+
return rows[0] ?? null;
|
|
555
|
+
},
|
|
556
|
+
async upsertCacheEntry(entry) {
|
|
557
|
+
const existing = await db.select({ id: khotanCacheEntries.id }).from(khotanCacheEntries).where(
|
|
558
|
+
and(
|
|
559
|
+
eq(khotanCacheEntries.cacheId, entry.cacheId),
|
|
560
|
+
eq(khotanCacheEntries.key, entry.key)
|
|
561
|
+
)
|
|
562
|
+
).limit(1);
|
|
563
|
+
const rows = await db.insert(khotanCacheEntries).values({
|
|
564
|
+
cacheId: entry.cacheId,
|
|
565
|
+
key: entry.key,
|
|
566
|
+
value: entry.value,
|
|
567
|
+
expiresAt: entry.expiresAt ?? null
|
|
568
|
+
}).onConflictDoUpdate({
|
|
569
|
+
target: [khotanCacheEntries.cacheId, khotanCacheEntries.key],
|
|
570
|
+
set: {
|
|
571
|
+
value: entry.value,
|
|
572
|
+
expiresAt: entry.expiresAt ?? null,
|
|
573
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
574
|
+
}
|
|
575
|
+
}).returning({ id: khotanCacheEntries.id });
|
|
576
|
+
return { id: rows[0].id, created: existing.length === 0 };
|
|
577
|
+
},
|
|
578
|
+
async deleteCacheEntry(cacheId, key) {
|
|
579
|
+
await db.delete(khotanCacheEntries).where(
|
|
580
|
+
and(
|
|
581
|
+
eq(khotanCacheEntries.cacheId, cacheId),
|
|
582
|
+
eq(khotanCacheEntries.key, key)
|
|
583
|
+
)
|
|
584
|
+
);
|
|
585
|
+
},
|
|
389
586
|
async listResources() {
|
|
390
587
|
const flowCounts = db.select({
|
|
391
588
|
resourceId: khotanFlows.resourceId,
|
|
392
589
|
flowCount: count(khotanFlows.id).as("flow_count")
|
|
393
590
|
}).from(khotanFlows).where(sql`${khotanFlows.resourceId} is not null`).groupBy(khotanFlows.resourceId).as("flow_counts");
|
|
394
591
|
const mappingCounts = db.select({
|
|
395
|
-
resourceId:
|
|
396
|
-
mappingCount: count(
|
|
397
|
-
}).from(
|
|
592
|
+
resourceId: khotanMappingsTable.resourceId,
|
|
593
|
+
mappingCount: count(khotanMappingsTable.id).as("mapping_count")
|
|
594
|
+
}).from(khotanMappingsTable).groupBy(khotanMappingsTable.resourceId).as("mapping_counts");
|
|
398
595
|
const rows = await db.select({
|
|
399
596
|
id: khotanResources.id,
|
|
400
597
|
name: khotanResources.name,
|
|
@@ -408,57 +605,79 @@ function drizzleAdapter(db) {
|
|
|
408
605
|
mappingCounts,
|
|
409
606
|
eq(khotanResources.id, mappingCounts.resourceId)
|
|
410
607
|
);
|
|
411
|
-
return rows
|
|
608
|
+
return rows.map((row) => ({
|
|
609
|
+
...row,
|
|
610
|
+
connectField: deserializeConnectField(row.connectField)
|
|
611
|
+
}));
|
|
412
612
|
},
|
|
413
613
|
async getResource(id) {
|
|
414
614
|
const rows = await db.select().from(khotanResources).where(eq(khotanResources.id, id)).limit(1);
|
|
415
|
-
|
|
615
|
+
if (!rows[0]) return null;
|
|
616
|
+
return {
|
|
617
|
+
...rows[0],
|
|
618
|
+
connectField: deserializeConnectField(rows[0].connectField)
|
|
619
|
+
};
|
|
416
620
|
},
|
|
417
621
|
async getResourceFlows(resourceId) {
|
|
418
622
|
return db.select().from(khotanFlows).where(eq(khotanFlows.resourceId, resourceId));
|
|
419
623
|
},
|
|
420
624
|
async upsertMapping(mapping) {
|
|
421
625
|
if (mapping.id) {
|
|
422
|
-
const rows2 = await db.update(
|
|
626
|
+
const rows2 = await db.update(khotanMappingsTable).set({
|
|
423
627
|
resourceId: mapping.resourceId,
|
|
424
628
|
connectValue: mapping.connectValue,
|
|
425
629
|
refs: mapping.refs,
|
|
426
630
|
metadata: mapping.metadata ?? null,
|
|
427
631
|
updatedAt: /* @__PURE__ */ new Date()
|
|
428
|
-
}).where(eq(
|
|
632
|
+
}).where(eq(khotanMappingsTable.id, mapping.id)).returning({ id: khotanMappingsTable.id });
|
|
429
633
|
return { id: rows2[0].id, created: false };
|
|
430
634
|
}
|
|
431
|
-
const existing = await db.select({ id:
|
|
432
|
-
sql`${
|
|
635
|
+
const existing = await db.select({ id: khotanMappingsTable.id }).from(khotanMappingsTable).where(
|
|
636
|
+
sql`${khotanMappingsTable.resourceId} = ${mapping.resourceId} and ${khotanMappingsTable.connectValue} = ${mapping.connectValue}`
|
|
433
637
|
).limit(1);
|
|
434
|
-
const rows = await db.insert(
|
|
638
|
+
const rows = await db.insert(khotanMappingsTable).values({
|
|
435
639
|
resourceId: mapping.resourceId,
|
|
436
640
|
connectValue: mapping.connectValue,
|
|
437
641
|
refs: mapping.refs,
|
|
438
642
|
metadata: mapping.metadata ?? null
|
|
439
643
|
}).onConflictDoUpdate({
|
|
440
|
-
target: [
|
|
644
|
+
target: [khotanMappingsTable.resourceId, khotanMappingsTable.connectValue],
|
|
441
645
|
set: {
|
|
442
|
-
refs: sql`${
|
|
646
|
+
refs: sql`${khotanMappingsTable.refs} || ${JSON.stringify(mapping.refs)}::jsonb`,
|
|
443
647
|
metadata: mapping.metadata ?? null,
|
|
444
648
|
updatedAt: /* @__PURE__ */ new Date()
|
|
445
649
|
}
|
|
446
|
-
}).returning({ id:
|
|
650
|
+
}).returning({ id: khotanMappingsTable.id });
|
|
447
651
|
return { id: rows[0].id, created: existing.length === 0 };
|
|
448
652
|
},
|
|
449
653
|
async getMapping(id) {
|
|
450
|
-
const rows = await db.select().from(
|
|
654
|
+
const rows = await db.select().from(khotanMappingsTable).where(eq(khotanMappingsTable.id, id)).limit(1);
|
|
451
655
|
return rows[0] ?? null;
|
|
452
656
|
},
|
|
453
|
-
async listMappings(resourceId) {
|
|
454
|
-
|
|
657
|
+
async listMappings({ resourceId, limit, offset, search }) {
|
|
658
|
+
const normalizedSearch = search?.trim();
|
|
659
|
+
const searchPattern = normalizedSearch ? `%${normalizedSearch.replace(/[%_]/g, "\\$&")}%` : null;
|
|
660
|
+
const filters = searchPattern ? sql`${khotanMappingsTable.resourceId} = ${resourceId} and (${khotanMappingsTable.connectValue} ilike ${searchPattern} escape '\\' or ${khotanMappingsTable.refs}::text ilike ${searchPattern} escape '\\' or ${khotanMappingsTable.metadata}::text ilike ${searchPattern} escape '\\')` : sql`${khotanMappingsTable.resourceId} = ${resourceId}`;
|
|
661
|
+
const totalRows = await db.select({ total: count(khotanMappingsTable.id) }).from(khotanMappingsTable).where(filters);
|
|
662
|
+
const rows = await db.select().from(khotanMappingsTable).where(filters).orderBy(khotanMappingsTable.connectValue, khotanMappingsTable.id).limit(limit + 1).offset(offset);
|
|
663
|
+
return {
|
|
664
|
+
items: rows.slice(0, limit),
|
|
665
|
+
hasMore: rows.length > limit,
|
|
666
|
+
total: totalRows[0]?.total ?? 0
|
|
667
|
+
};
|
|
455
668
|
},
|
|
456
669
|
async deleteMapping(id) {
|
|
457
|
-
await db.delete(
|
|
670
|
+
await db.delete(khotanMappingsTable).where(eq(khotanMappingsTable.id, id));
|
|
458
671
|
},
|
|
459
|
-
async lookupMapping(
|
|
460
|
-
|
|
461
|
-
|
|
672
|
+
async lookupMapping(params) {
|
|
673
|
+
if ("connectValue" in params) {
|
|
674
|
+
const rows2 = await db.select().from(khotanMappingsTable).where(
|
|
675
|
+
sql`${khotanMappingsTable.resourceId} = ${params.resourceId} and ${khotanMappingsTable.connectValue} = ${params.connectValue}`
|
|
676
|
+
).limit(1);
|
|
677
|
+
return rows2[0] ?? null;
|
|
678
|
+
}
|
|
679
|
+
const rows = await db.select().from(khotanMappingsTable).where(
|
|
680
|
+
sql`${khotanMappingsTable.resourceId} = ${params.resourceId} and ${khotanMappingsTable.refs}->>${params.plugName} = ${params.ref}`
|
|
462
681
|
).limit(1);
|
|
463
682
|
return rows[0] ?? null;
|
|
464
683
|
},
|
|
@@ -791,6 +1010,149 @@ function getErrorMessage(error) {
|
|
|
791
1010
|
if (error instanceof Error) return error.message;
|
|
792
1011
|
return "Unknown error";
|
|
793
1012
|
}
|
|
1013
|
+
var CRON_MONTH_ALIASES = {
|
|
1014
|
+
jan: 1,
|
|
1015
|
+
feb: 2,
|
|
1016
|
+
mar: 3,
|
|
1017
|
+
apr: 4,
|
|
1018
|
+
may: 5,
|
|
1019
|
+
jun: 6,
|
|
1020
|
+
jul: 7,
|
|
1021
|
+
aug: 8,
|
|
1022
|
+
sep: 9,
|
|
1023
|
+
oct: 10,
|
|
1024
|
+
nov: 11,
|
|
1025
|
+
dec: 12
|
|
1026
|
+
};
|
|
1027
|
+
var CRON_DAY_ALIASES = {
|
|
1028
|
+
sun: 0,
|
|
1029
|
+
mon: 1,
|
|
1030
|
+
tue: 2,
|
|
1031
|
+
wed: 3,
|
|
1032
|
+
thu: 4,
|
|
1033
|
+
fri: 5,
|
|
1034
|
+
sat: 6
|
|
1035
|
+
};
|
|
1036
|
+
function parseCronValue(token, spec) {
|
|
1037
|
+
const normalized = token.trim().toLowerCase();
|
|
1038
|
+
if (normalized === "") {
|
|
1039
|
+
throw new Error("Cron token cannot be empty");
|
|
1040
|
+
}
|
|
1041
|
+
if (spec.aliases?.[normalized] !== void 0) {
|
|
1042
|
+
return spec.aliases[normalized];
|
|
1043
|
+
}
|
|
1044
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
1045
|
+
if (!Number.isFinite(parsed)) {
|
|
1046
|
+
throw new Error(`Invalid cron token: "${token}"`);
|
|
1047
|
+
}
|
|
1048
|
+
if (spec.aliases === CRON_DAY_ALIASES && parsed === 7) {
|
|
1049
|
+
return 0;
|
|
1050
|
+
}
|
|
1051
|
+
if (parsed < spec.min || parsed > spec.max) {
|
|
1052
|
+
throw new Error(
|
|
1053
|
+
`Cron value "${token}" is out of range ${spec.min}-${spec.max}`
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
return parsed;
|
|
1057
|
+
}
|
|
1058
|
+
function cronFieldIsWildcard(field) {
|
|
1059
|
+
return field.trim() === "*";
|
|
1060
|
+
}
|
|
1061
|
+
function matchesCronField(field, value, spec) {
|
|
1062
|
+
return field.split(",").map((part) => part.trim()).filter(Boolean).some((part) => {
|
|
1063
|
+
if (part === "*") return true;
|
|
1064
|
+
const [baseRaw, stepRaw] = part.split("/");
|
|
1065
|
+
const base = baseRaw?.trim() ?? "";
|
|
1066
|
+
const step = stepRaw ? Number.parseInt(stepRaw.trim(), 10) : 1;
|
|
1067
|
+
if (!Number.isFinite(step) || step <= 0) {
|
|
1068
|
+
throw new Error(`Invalid cron step: "${part}"`);
|
|
1069
|
+
}
|
|
1070
|
+
let start;
|
|
1071
|
+
let end;
|
|
1072
|
+
if (base === "*" || base === "") {
|
|
1073
|
+
start = spec.min;
|
|
1074
|
+
end = spec.max;
|
|
1075
|
+
} else if (base.includes("-")) {
|
|
1076
|
+
const [startRaw, endRaw] = base.split("-");
|
|
1077
|
+
start = parseCronValue(startRaw ?? "", spec);
|
|
1078
|
+
end = parseCronValue(endRaw ?? "", spec);
|
|
1079
|
+
if (end < start) {
|
|
1080
|
+
throw new Error(`Invalid cron range: "${part}"`);
|
|
1081
|
+
}
|
|
1082
|
+
} else {
|
|
1083
|
+
start = parseCronValue(base, spec);
|
|
1084
|
+
end = stepRaw ? spec.max : start;
|
|
1085
|
+
}
|
|
1086
|
+
if (value < start || value > end) return false;
|
|
1087
|
+
return (value - start) % step === 0;
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
function matchesCronSchedule(schedule, now) {
|
|
1091
|
+
const parts = schedule.trim().split(/\s+/);
|
|
1092
|
+
if (parts.length !== 5) {
|
|
1093
|
+
throw new Error(`Cron schedule must have 5 fields: "${schedule}"`);
|
|
1094
|
+
}
|
|
1095
|
+
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
|
1096
|
+
const minuteMatch = matchesCronField(minute, now.getUTCMinutes(), {
|
|
1097
|
+
min: 0,
|
|
1098
|
+
max: 59
|
|
1099
|
+
});
|
|
1100
|
+
const hourMatch = matchesCronField(hour, now.getUTCHours(), {
|
|
1101
|
+
min: 0,
|
|
1102
|
+
max: 23
|
|
1103
|
+
});
|
|
1104
|
+
const monthMatch = matchesCronField(month, now.getUTCMonth() + 1, {
|
|
1105
|
+
min: 1,
|
|
1106
|
+
max: 12,
|
|
1107
|
+
aliases: CRON_MONTH_ALIASES
|
|
1108
|
+
});
|
|
1109
|
+
const dayOfMonthMatch = matchesCronField(dayOfMonth, now.getUTCDate(), {
|
|
1110
|
+
min: 1,
|
|
1111
|
+
max: 31
|
|
1112
|
+
});
|
|
1113
|
+
const dayOfWeekMatch = matchesCronField(dayOfWeek, now.getUTCDay(), {
|
|
1114
|
+
min: 0,
|
|
1115
|
+
max: 6,
|
|
1116
|
+
aliases: CRON_DAY_ALIASES
|
|
1117
|
+
});
|
|
1118
|
+
const dayOfMonthWildcard = cronFieldIsWildcard(dayOfMonth);
|
|
1119
|
+
const dayOfWeekWildcard = cronFieldIsWildcard(dayOfWeek);
|
|
1120
|
+
const dayMatches = dayOfMonthWildcard && dayOfWeekWildcard ? true : dayOfMonthWildcard ? dayOfWeekMatch : dayOfWeekWildcard ? dayOfMonthMatch : dayOfMonthMatch || dayOfWeekMatch;
|
|
1121
|
+
return minuteMatch && hourMatch && monthMatch && dayMatches;
|
|
1122
|
+
}
|
|
1123
|
+
function startOfUtcMinute(date) {
|
|
1124
|
+
return new Date(
|
|
1125
|
+
Date.UTC(
|
|
1126
|
+
date.getUTCFullYear(),
|
|
1127
|
+
date.getUTCMonth(),
|
|
1128
|
+
date.getUTCDate(),
|
|
1129
|
+
date.getUTCHours(),
|
|
1130
|
+
date.getUTCMinutes(),
|
|
1131
|
+
0,
|
|
1132
|
+
0
|
|
1133
|
+
)
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
function coerceDate(value) {
|
|
1137
|
+
if (value instanceof Date && Number.isFinite(value.getTime())) {
|
|
1138
|
+
return value;
|
|
1139
|
+
}
|
|
1140
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
1141
|
+
const parsed = new Date(value);
|
|
1142
|
+
if (Number.isFinite(parsed.getTime())) return parsed;
|
|
1143
|
+
}
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
function isCronRequestAuthorized(request) {
|
|
1147
|
+
const secret = process.env["CRON_SECRET"]?.trim();
|
|
1148
|
+
if (!secret) {
|
|
1149
|
+
return process.env["NODE_ENV"] !== "production";
|
|
1150
|
+
}
|
|
1151
|
+
return request.headers.get("authorization") === `Bearer ${secret}`;
|
|
1152
|
+
}
|
|
1153
|
+
function isDebugEnabled() {
|
|
1154
|
+
return Boolean(process?.env?.["KHOTAN_DEBUG"]) && process?.env?.["NODE_ENV"] !== "production";
|
|
1155
|
+
}
|
|
794
1156
|
function isWorkflowCancelledError(error) {
|
|
795
1157
|
if (!error || typeof error !== "object") return false;
|
|
796
1158
|
const record = error;
|
|
@@ -903,31 +1265,263 @@ function serializeEndpoints(endpoints) {
|
|
|
903
1265
|
}
|
|
904
1266
|
return result;
|
|
905
1267
|
}
|
|
1268
|
+
function isPlainObject(value) {
|
|
1269
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1270
|
+
}
|
|
1271
|
+
function serializeConnectField(connectField) {
|
|
1272
|
+
return Array.isArray(connectField) ? JSON.stringify(connectField) : connectField;
|
|
1273
|
+
}
|
|
1274
|
+
function deserializeConnectField(connectField) {
|
|
1275
|
+
if (Array.isArray(connectField)) {
|
|
1276
|
+
return connectField;
|
|
1277
|
+
}
|
|
1278
|
+
if (typeof connectField !== "string") {
|
|
1279
|
+
throw new Error("Resource connectField must be a string or string array");
|
|
1280
|
+
}
|
|
1281
|
+
const trimmed = connectField.trim();
|
|
1282
|
+
if (!trimmed.startsWith("[")) {
|
|
1283
|
+
return connectField;
|
|
1284
|
+
}
|
|
1285
|
+
try {
|
|
1286
|
+
const parsed = JSON.parse(trimmed);
|
|
1287
|
+
if (Array.isArray(parsed) && parsed.length > 0 && parsed.every((value) => typeof value === "string" && value.length > 0)) {
|
|
1288
|
+
return parsed;
|
|
1289
|
+
}
|
|
1290
|
+
} catch {
|
|
1291
|
+
}
|
|
1292
|
+
return connectField;
|
|
1293
|
+
}
|
|
1294
|
+
function validateConnectField(resourceName, connectField) {
|
|
1295
|
+
if (typeof connectField === "string") {
|
|
1296
|
+
if (!connectField.trim()) {
|
|
1297
|
+
throw new Error(
|
|
1298
|
+
`Resource "${resourceName}" must declare a non-empty connectField`
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
if (!Array.isArray(connectField) || connectField.length === 0) {
|
|
1304
|
+
throw new Error(
|
|
1305
|
+
`Resource "${resourceName}" must declare connectField as a string or non-empty ordered string array`
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
for (const field of connectField) {
|
|
1309
|
+
if (typeof field !== "string" || !field.trim()) {
|
|
1310
|
+
throw new Error(
|
|
1311
|
+
`Resource "${resourceName}" has an invalid composite connectField entry`
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
function validateResourcePlugs(resource, plugNames) {
|
|
1317
|
+
if (resource.mapping.plugs === void 0) {
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
if (!isPlainObject(resource.mapping.plugs)) {
|
|
1321
|
+
throw new Error(
|
|
1322
|
+
`Resource "${resource.name}" must declare mapping.plugs as an object keyed by plug name`
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
for (const [plugName, declaration] of Object.entries(resource.mapping.plugs)) {
|
|
1326
|
+
if (!plugNames.has(plugName)) {
|
|
1327
|
+
throw new Error(
|
|
1328
|
+
`Resource "${resource.name}" references unknown plug: "${plugName}"`
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
if (!isPlainObject(declaration)) {
|
|
1332
|
+
throw new Error(
|
|
1333
|
+
`Resource "${resource.name}" has an invalid plug declaration for "${plugName}"`
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
const keys = Object.keys(declaration);
|
|
1337
|
+
if (keys.length !== 1 || typeof declaration["uniqueIdentifier"] !== "string" || !declaration["uniqueIdentifier"].trim()) {
|
|
1338
|
+
throw new Error(
|
|
1339
|
+
`Resource "${resource.name}" must declare exactly one uniqueIdentifier for plug "${plugName}"`
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
function normalizeCacheScope(cacheName, scope) {
|
|
1345
|
+
if (scope === void 0) {
|
|
1346
|
+
return void 0;
|
|
1347
|
+
}
|
|
1348
|
+
if (!isPlainObject(scope)) {
|
|
1349
|
+
throw new Error(`Cache "${cacheName}" must declare scope as an object`);
|
|
1350
|
+
}
|
|
1351
|
+
const normalized = {};
|
|
1352
|
+
for (const key of ["plug", "resource", "flow"]) {
|
|
1353
|
+
const value = scope[key];
|
|
1354
|
+
if (value === void 0) {
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1357
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1358
|
+
throw new Error(`Cache "${cacheName}" has an invalid scope.${key} value`);
|
|
1359
|
+
}
|
|
1360
|
+
normalized[key] = value.trim();
|
|
1361
|
+
}
|
|
1362
|
+
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
1363
|
+
}
|
|
1364
|
+
function parseCacheTtlSeconds(cacheName, ttl) {
|
|
1365
|
+
if (ttl === void 0) {
|
|
1366
|
+
return null;
|
|
1367
|
+
}
|
|
1368
|
+
if (typeof ttl === "number") {
|
|
1369
|
+
if (!Number.isFinite(ttl) || ttl <= 0) {
|
|
1370
|
+
throw new Error(`Cache "${cacheName}" must declare a positive ttl`);
|
|
1371
|
+
}
|
|
1372
|
+
return Math.ceil(ttl);
|
|
1373
|
+
}
|
|
1374
|
+
if (typeof ttl !== "string") {
|
|
1375
|
+
throw new Error(`Cache "${cacheName}" must declare ttl as a string or number`);
|
|
1376
|
+
}
|
|
1377
|
+
const normalized = ttl.trim().toLowerCase();
|
|
1378
|
+
const match = /^(\d+)\s*(ms|s|m|h|d)$/.exec(normalized);
|
|
1379
|
+
if (!match) {
|
|
1380
|
+
throw new Error(
|
|
1381
|
+
`Cache "${cacheName}" has an invalid ttl "${ttl}". Use values like "30s", "15m", or "6h"`
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
const amount = Number.parseInt(match[1], 10);
|
|
1385
|
+
const unit = match[2];
|
|
1386
|
+
const milliseconds = unit === "ms" ? amount : unit === "s" ? amount * 1e3 : unit === "m" ? amount * 6e4 : unit === "h" ? amount * 36e5 : amount * 864e5;
|
|
1387
|
+
return Math.max(1, Math.ceil(milliseconds / 1e3));
|
|
1388
|
+
}
|
|
1389
|
+
function validateCacheKey(key) {
|
|
1390
|
+
if (typeof key !== "string" || !key.trim()) {
|
|
1391
|
+
throw new Error("Cache key must be a non-empty string");
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
function coerceCacheEntryRecord(row) {
|
|
1395
|
+
if (typeof row["id"] !== "string" || typeof row["cacheId"] !== "string" || typeof row["key"] !== "string") {
|
|
1396
|
+
return null;
|
|
1397
|
+
}
|
|
1398
|
+
return {
|
|
1399
|
+
id: row["id"],
|
|
1400
|
+
cacheId: row["cacheId"],
|
|
1401
|
+
key: row["key"],
|
|
1402
|
+
value: row["value"],
|
|
1403
|
+
expiresAt: coerceDate(row["expiresAt"]),
|
|
1404
|
+
createdAt: coerceDate(row["createdAt"]) ?? void 0,
|
|
1405
|
+
updatedAt: coerceDate(row["updatedAt"]) ?? void 0
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
function isCacheEntryExpired(entry, now = /* @__PURE__ */ new Date()) {
|
|
1409
|
+
return entry.expiresAt !== null && entry.expiresAt.getTime() <= now.getTime();
|
|
1410
|
+
}
|
|
1411
|
+
function canonicalizeConnectValue(resource, connectValue) {
|
|
1412
|
+
const { connectField } = resource.mapping;
|
|
1413
|
+
if (Array.isArray(connectField)) {
|
|
1414
|
+
if (typeof connectValue === "string") {
|
|
1415
|
+
return connectValue;
|
|
1416
|
+
}
|
|
1417
|
+
if (!Array.isArray(connectValue)) {
|
|
1418
|
+
throw new Error(
|
|
1419
|
+
`Resource "${resource.name}" expects composite connectValue input matching connectField order`
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
if (connectValue.length !== connectField.length) {
|
|
1423
|
+
throw new Error(
|
|
1424
|
+
`Resource "${resource.name}" expects ${String(connectField.length)} connectValue parts in declared order`
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
const parts = connectValue.map((part) => {
|
|
1428
|
+
if (typeof part === "string") return part;
|
|
1429
|
+
if (typeof part === "number" || typeof part === "boolean") {
|
|
1430
|
+
return String(part);
|
|
1431
|
+
}
|
|
1432
|
+
throw new Error(
|
|
1433
|
+
`Resource "${resource.name}" connectValue parts must be strings, numbers, or booleans`
|
|
1434
|
+
);
|
|
1435
|
+
});
|
|
1436
|
+
return JSON.stringify(parts);
|
|
1437
|
+
}
|
|
1438
|
+
if (typeof connectValue === "string") {
|
|
1439
|
+
return connectValue;
|
|
1440
|
+
}
|
|
1441
|
+
if (typeof connectValue === "number" || typeof connectValue === "boolean") {
|
|
1442
|
+
return String(connectValue);
|
|
1443
|
+
}
|
|
1444
|
+
throw new Error(
|
|
1445
|
+
`Resource "${resource.name}" expects connectValue to be a string, number, or boolean`
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
906
1448
|
function khotan(config) {
|
|
907
|
-
const { adapter, plugs, resources = [] } = config;
|
|
1449
|
+
const { adapter, plugs, resources = [], caches = [], authorize } = config;
|
|
1450
|
+
const instanceId = crypto.randomUUID();
|
|
1451
|
+
if (!authorize) {
|
|
1452
|
+
console.warn(
|
|
1453
|
+
"[khotan] No `authorize` hook configured: the management API (/api/khotan/*) is publicly accessible. Pass `authorize` to gate it behind your auth layer (e.g. better-auth). This is required for any deployed environment."
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
if (!(config.secret ?? process.env["KHOTAN_SECRET"])) {
|
|
1457
|
+
console.warn(
|
|
1458
|
+
"[khotan] No `secret`/`KHOTAN_SECRET` configured: plug credentials and wire metadata will not be encrypted at rest. Set KHOTAN_SECRET to a high-entropy value."
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
const plugNames = /* @__PURE__ */ new Set();
|
|
1462
|
+
for (const plug of plugs) {
|
|
1463
|
+
if (plugNames.has(plug.name)) {
|
|
1464
|
+
throw new Error(`Duplicate plug name: "${plug.name}"`);
|
|
1465
|
+
}
|
|
1466
|
+
plugNames.add(plug.name);
|
|
1467
|
+
}
|
|
908
1468
|
const resourceNames = /* @__PURE__ */ new Set();
|
|
1469
|
+
const resourceConfigByName = /* @__PURE__ */ new Map();
|
|
909
1470
|
for (const resource of resources) {
|
|
910
1471
|
if (resourceNames.has(resource.name)) {
|
|
911
1472
|
throw new Error(`Duplicate resource name: "${resource.name}"`);
|
|
912
1473
|
}
|
|
1474
|
+
validateConnectField(resource.name, resource.mapping.connectField);
|
|
1475
|
+
validateResourcePlugs(resource, plugNames);
|
|
913
1476
|
resourceNames.add(resource.name);
|
|
1477
|
+
resourceConfigByName.set(resource.name, resource);
|
|
914
1478
|
}
|
|
915
|
-
const
|
|
1479
|
+
const registeredFlowNames = /* @__PURE__ */ new Set();
|
|
916
1480
|
for (const plug of plugs) {
|
|
917
|
-
if (
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
throw new Error(
|
|
925
|
-
`Flow "${flow2.name}" references unknown resource: "${flow2.resource}"`
|
|
926
|
-
);
|
|
927
|
-
}
|
|
1481
|
+
if (!plug.flows) continue;
|
|
1482
|
+
for (const flow2 of plug.flows) {
|
|
1483
|
+
registeredFlowNames.add(flow2.name);
|
|
1484
|
+
if (flow2.resource && !resourceNames.has(flow2.resource)) {
|
|
1485
|
+
throw new Error(
|
|
1486
|
+
`Flow "${flow2.name}" references unknown resource: "${flow2.resource}"`
|
|
1487
|
+
);
|
|
928
1488
|
}
|
|
929
1489
|
}
|
|
930
1490
|
}
|
|
1491
|
+
const cacheStateByName = /* @__PURE__ */ new Map();
|
|
1492
|
+
for (const cache of caches) {
|
|
1493
|
+
if (cacheStateByName.has(cache.name)) {
|
|
1494
|
+
throw new Error(`Duplicate cache name: "${cache.name}"`);
|
|
1495
|
+
}
|
|
1496
|
+
if (typeof cache.name !== "string" || !cache.name.trim()) {
|
|
1497
|
+
throw new Error("Cache registrations must declare a non-empty name");
|
|
1498
|
+
}
|
|
1499
|
+
const normalizedScope = normalizeCacheScope(cache.name, cache.scope);
|
|
1500
|
+
if (normalizedScope?.plug && !plugNames.has(normalizedScope.plug)) {
|
|
1501
|
+
throw new Error(
|
|
1502
|
+
`Cache "${cache.name}" references unknown plug: "${normalizedScope.plug}"`
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
if (normalizedScope?.resource && !resourceNames.has(normalizedScope.resource)) {
|
|
1506
|
+
throw new Error(
|
|
1507
|
+
`Cache "${cache.name}" references unknown resource: "${normalizedScope.resource}"`
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
if (normalizedScope?.flow && !registeredFlowNames.has(normalizedScope.flow)) {
|
|
1511
|
+
throw new Error(
|
|
1512
|
+
`Cache "${cache.name}" references unknown flow: "${normalizedScope.flow}"`
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
cacheStateByName.set(cache.name, {
|
|
1516
|
+
id: "",
|
|
1517
|
+
config: {
|
|
1518
|
+
...cache,
|
|
1519
|
+
name: cache.name.trim(),
|
|
1520
|
+
...normalizedScope ? { scope: normalizedScope } : {}
|
|
1521
|
+
},
|
|
1522
|
+
ttlSeconds: parseCacheTtlSeconds(cache.name, cache.ttl)
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
931
1525
|
const registeredFlowKeys = new Set(
|
|
932
1526
|
plugs.flatMap(
|
|
933
1527
|
(plug) => (plug.flows ?? []).map(
|
|
@@ -969,16 +1563,31 @@ function khotan(config) {
|
|
|
969
1563
|
}
|
|
970
1564
|
let initialized = false;
|
|
971
1565
|
let initPromise = null;
|
|
1566
|
+
const resourceIdByName = /* @__PURE__ */ new Map();
|
|
1567
|
+
const resourceConfigById = /* @__PURE__ */ new Map();
|
|
972
1568
|
async function doInit() {
|
|
973
1569
|
if (initialized) return;
|
|
974
|
-
|
|
1570
|
+
resourceIdByName.clear();
|
|
1571
|
+
resourceConfigById.clear();
|
|
975
1572
|
for (const resource of resources) {
|
|
976
1573
|
const { id } = await adapter.upsertResource({
|
|
977
1574
|
name: resource.name,
|
|
978
|
-
connectField: resource.connectField,
|
|
1575
|
+
connectField: resource.mapping.connectField,
|
|
979
1576
|
description: resource.description ?? null
|
|
980
1577
|
});
|
|
981
|
-
|
|
1578
|
+
resourceIdByName.set(resource.name, id);
|
|
1579
|
+
resourceConfigById.set(id, resource);
|
|
1580
|
+
}
|
|
1581
|
+
for (const [cacheName, cacheState] of cacheStateByName) {
|
|
1582
|
+
const { id } = await adapter.upsertCache({
|
|
1583
|
+
name: cacheName,
|
|
1584
|
+
scope: cacheState.config.scope ?? null,
|
|
1585
|
+
ttlSeconds: cacheState.ttlSeconds
|
|
1586
|
+
});
|
|
1587
|
+
cacheStateByName.set(cacheName, {
|
|
1588
|
+
...cacheState,
|
|
1589
|
+
id
|
|
1590
|
+
});
|
|
982
1591
|
}
|
|
983
1592
|
for (const plug of plugs) {
|
|
984
1593
|
const { id: plugId } = await adapter.upsertPlug({
|
|
@@ -996,7 +1605,7 @@ function khotan(config) {
|
|
|
996
1605
|
schedule: flow2.schedule ?? null
|
|
997
1606
|
});
|
|
998
1607
|
if (flow2.resource) {
|
|
999
|
-
const resourceId =
|
|
1608
|
+
const resourceId = resourceIdByName.get(flow2.resource);
|
|
1000
1609
|
await adapter.updateFlowResourceId(flowId, resourceId);
|
|
1001
1610
|
}
|
|
1002
1611
|
}
|
|
@@ -1031,6 +1640,189 @@ function khotan(config) {
|
|
|
1031
1640
|
initPromise ??= doInit();
|
|
1032
1641
|
return initPromise;
|
|
1033
1642
|
}
|
|
1643
|
+
async function getRegisteredResourceById(resourceId) {
|
|
1644
|
+
await init();
|
|
1645
|
+
return resourceConfigById.get(resourceId) ?? null;
|
|
1646
|
+
}
|
|
1647
|
+
async function resolveCacheState(cacheName) {
|
|
1648
|
+
await init();
|
|
1649
|
+
const cacheState = cacheStateByName.get(cacheName);
|
|
1650
|
+
if (!cacheState || !cacheState.id) {
|
|
1651
|
+
throw new Error(`Cache "${cacheName}" is not registered`);
|
|
1652
|
+
}
|
|
1653
|
+
return cacheState;
|
|
1654
|
+
}
|
|
1655
|
+
function createCacheInstance(cacheName) {
|
|
1656
|
+
return {
|
|
1657
|
+
async get(key) {
|
|
1658
|
+
const entry = await readCacheEntry(cacheName, key);
|
|
1659
|
+
return entry ? entry.value : null;
|
|
1660
|
+
},
|
|
1661
|
+
async set(key, value) {
|
|
1662
|
+
validateCacheKey(key);
|
|
1663
|
+
const cacheState = await resolveCacheState(cacheName);
|
|
1664
|
+
const expiresAt = cacheState.ttlSeconds !== null ? new Date(Date.now() + cacheState.ttlSeconds * 1e3) : null;
|
|
1665
|
+
await adapter.upsertCacheEntry({
|
|
1666
|
+
cacheId: cacheState.id,
|
|
1667
|
+
key,
|
|
1668
|
+
value,
|
|
1669
|
+
expiresAt
|
|
1670
|
+
});
|
|
1671
|
+
return value;
|
|
1672
|
+
},
|
|
1673
|
+
async delete(key) {
|
|
1674
|
+
validateCacheKey(key);
|
|
1675
|
+
const cacheState = await resolveCacheState(cacheName);
|
|
1676
|
+
await adapter.deleteCacheEntry(cacheState.id, key);
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
async function readCacheEntry(cacheName, key) {
|
|
1681
|
+
validateCacheKey(key);
|
|
1682
|
+
const cacheState = await resolveCacheState(cacheName);
|
|
1683
|
+
const row = await adapter.getCacheEntry(cacheState.id, key);
|
|
1684
|
+
if (!row) {
|
|
1685
|
+
return null;
|
|
1686
|
+
}
|
|
1687
|
+
const entry = coerceCacheEntryRecord(row);
|
|
1688
|
+
if (!entry || isCacheEntryExpired(entry)) {
|
|
1689
|
+
return null;
|
|
1690
|
+
}
|
|
1691
|
+
return entry;
|
|
1692
|
+
}
|
|
1693
|
+
function decorateResourceRecord(resource) {
|
|
1694
|
+
const { connectField: storedConnectField, ...rest } = resource;
|
|
1695
|
+
const configResource = typeof resource["name"] === "string" ? resourceConfigByName.get(resource["name"]) : void 0;
|
|
1696
|
+
return {
|
|
1697
|
+
...rest,
|
|
1698
|
+
mapping: {
|
|
1699
|
+
connectField: configResource?.mapping.connectField ?? deserializeConnectField(storedConnectField),
|
|
1700
|
+
...configResource?.mapping.plugs ? { plugs: configResource.mapping.plugs } : {}
|
|
1701
|
+
}
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
function buildMappingPage(params) {
|
|
1705
|
+
return {
|
|
1706
|
+
items: params.items,
|
|
1707
|
+
page: {
|
|
1708
|
+
limit: params.limit,
|
|
1709
|
+
offset: params.offset,
|
|
1710
|
+
hasMore: params.hasMore,
|
|
1711
|
+
prevOffset: Math.max(params.offset - params.limit, 0),
|
|
1712
|
+
nextOffset: params.offset + params.limit,
|
|
1713
|
+
total: params.total
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
async function validateMappingPayload(params) {
|
|
1718
|
+
if (!isPlainObject(params.refs)) {
|
|
1719
|
+
throw new Error("Mapping refs must be an object keyed by plug name");
|
|
1720
|
+
}
|
|
1721
|
+
for (const [plugName, ref] of Object.entries(params.refs)) {
|
|
1722
|
+
if (typeof ref !== "string") {
|
|
1723
|
+
throw new Error(`Mapping ref "${plugName}" must be a string`);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
if (params.metadata !== void 0 && params.metadata !== null) {
|
|
1727
|
+
if (!isPlainObject(params.metadata)) {
|
|
1728
|
+
throw new Error("Mapping metadata must be an object when provided");
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
const resource = await getRegisteredResourceById(params.resourceId);
|
|
1732
|
+
if (!resource) {
|
|
1733
|
+
throw new Error(`Resource "${params.resourceId}" is not registered`);
|
|
1734
|
+
}
|
|
1735
|
+
if (resource.mapping.plugs) {
|
|
1736
|
+
const invalidPlugs = Object.keys(params.refs).filter(
|
|
1737
|
+
(plugName) => !resource.mapping.plugs?.[plugName]
|
|
1738
|
+
);
|
|
1739
|
+
if (invalidPlugs.length > 0) {
|
|
1740
|
+
throw new Error(
|
|
1741
|
+
`Resource "${resource.name}" only allows refs for declared plugs. Invalid refs: ${invalidPlugs.join(", ")}`
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
return resource;
|
|
1746
|
+
}
|
|
1747
|
+
async function listMappings(params) {
|
|
1748
|
+
const resource = await getRegisteredResourceById(params.resourceId);
|
|
1749
|
+
if (!resource) {
|
|
1750
|
+
throw new Error(`Resource "${params.resourceId}" is not registered`);
|
|
1751
|
+
}
|
|
1752
|
+
const limit = Math.min(Math.max(params.limit ?? 20, 1), 100);
|
|
1753
|
+
const offset = Math.max(params.offset ?? 0, 0);
|
|
1754
|
+
const page = await adapter.listMappings({
|
|
1755
|
+
resourceId: params.resourceId,
|
|
1756
|
+
limit,
|
|
1757
|
+
offset,
|
|
1758
|
+
...params.search?.trim() ? { search: params.search.trim() } : {}
|
|
1759
|
+
});
|
|
1760
|
+
return buildMappingPage({
|
|
1761
|
+
limit,
|
|
1762
|
+
offset,
|
|
1763
|
+
hasMore: page.hasMore,
|
|
1764
|
+
total: page.total,
|
|
1765
|
+
items: page.items
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
async function lookupMapping(params) {
|
|
1769
|
+
const resource = await getRegisteredResourceById(params.resourceId);
|
|
1770
|
+
if (!resource) {
|
|
1771
|
+
throw new Error(`Resource "${params.resourceId}" is not registered`);
|
|
1772
|
+
}
|
|
1773
|
+
if ("connectValue" in params) {
|
|
1774
|
+
return adapter.lookupMapping({
|
|
1775
|
+
resourceId: params.resourceId,
|
|
1776
|
+
connectValue: canonicalizeConnectValue(resource, params.connectValue)
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
if (resource.mapping.plugs && !resource.mapping.plugs[params.plugName]) {
|
|
1780
|
+
throw new Error(
|
|
1781
|
+
`Resource "${resource.name}" does not declare plug "${params.plugName}"`
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
return adapter.lookupMapping(params);
|
|
1785
|
+
}
|
|
1786
|
+
async function upsertMapping(mapping) {
|
|
1787
|
+
const resource = await validateMappingPayload(mapping);
|
|
1788
|
+
const result = await adapter.upsertMapping({
|
|
1789
|
+
resourceId: mapping.resourceId,
|
|
1790
|
+
connectValue: canonicalizeConnectValue(resource, mapping.connectValue),
|
|
1791
|
+
refs: mapping.refs,
|
|
1792
|
+
metadata: mapping.metadata ?? null
|
|
1793
|
+
});
|
|
1794
|
+
const saved = await adapter.getMapping(result.id);
|
|
1795
|
+
if (!saved) {
|
|
1796
|
+
throw new Error("Mapping was saved but could not be reloaded");
|
|
1797
|
+
}
|
|
1798
|
+
return saved;
|
|
1799
|
+
}
|
|
1800
|
+
async function updateMapping(id, mapping) {
|
|
1801
|
+
const existing = await adapter.getMapping(id);
|
|
1802
|
+
if (!existing) {
|
|
1803
|
+
throw new Error(`Mapping "${id}" not found`);
|
|
1804
|
+
}
|
|
1805
|
+
const resource = await validateMappingPayload(mapping);
|
|
1806
|
+
await adapter.upsertMapping({
|
|
1807
|
+
id,
|
|
1808
|
+
resourceId: mapping.resourceId,
|
|
1809
|
+
connectValue: canonicalizeConnectValue(resource, mapping.connectValue),
|
|
1810
|
+
refs: mapping.refs,
|
|
1811
|
+
metadata: mapping.metadata ?? null
|
|
1812
|
+
});
|
|
1813
|
+
const saved = await adapter.getMapping(id);
|
|
1814
|
+
if (!saved) {
|
|
1815
|
+
throw new Error(`Mapping "${id}" disappeared after update`);
|
|
1816
|
+
}
|
|
1817
|
+
return saved;
|
|
1818
|
+
}
|
|
1819
|
+
async function deleteMapping(id) {
|
|
1820
|
+
const existing = await adapter.getMapping(id);
|
|
1821
|
+
if (!existing) {
|
|
1822
|
+
throw new Error(`Mapping "${id}" not found`);
|
|
1823
|
+
}
|
|
1824
|
+
await adapter.deleteMapping(id);
|
|
1825
|
+
}
|
|
1034
1826
|
function wire(plugName) {
|
|
1035
1827
|
const plugReg = plugs.find((p) => p.name === plugName);
|
|
1036
1828
|
if (!plugReg) {
|
|
@@ -1041,29 +1833,7 @@ function khotan(config) {
|
|
|
1041
1833
|
}
|
|
1042
1834
|
const wireConfig = plugReg.wires[0];
|
|
1043
1835
|
function createBoundPlug(vars, _setVars) {
|
|
1044
|
-
|
|
1045
|
-
const opts = (extra) => ({
|
|
1046
|
-
...extra,
|
|
1047
|
-
vars,
|
|
1048
|
-
..._setVars && { _setVars }
|
|
1049
|
-
});
|
|
1050
|
-
return {
|
|
1051
|
-
get(path, extra) {
|
|
1052
|
-
return plug.get(path, opts(extra));
|
|
1053
|
-
},
|
|
1054
|
-
post(path, extra) {
|
|
1055
|
-
return plug.post(path, opts(extra));
|
|
1056
|
-
},
|
|
1057
|
-
put(path, extra) {
|
|
1058
|
-
return plug.put(path, opts(extra));
|
|
1059
|
-
},
|
|
1060
|
-
patch(path, extra) {
|
|
1061
|
-
return plug.patch(path, opts(extra));
|
|
1062
|
-
},
|
|
1063
|
-
delete(path, extra) {
|
|
1064
|
-
return plug.delete(path, opts(extra));
|
|
1065
|
-
}
|
|
1066
|
-
};
|
|
1836
|
+
return bindPlugWithVars(plugReg.plug, vars, _setVars);
|
|
1067
1837
|
}
|
|
1068
1838
|
async function getWireVars(wireId) {
|
|
1069
1839
|
const raw = await adapter.getWireMetadata(wireId);
|
|
@@ -1253,31 +2023,21 @@ function khotan(config) {
|
|
|
1253
2023
|
const setFlowVars = async (updates) => {
|
|
1254
2024
|
await setVars(plugName, { ...vars, ...updates });
|
|
1255
2025
|
};
|
|
1256
|
-
const
|
|
1257
|
-
|
|
2026
|
+
const boundPlug = bindPlugWithVars(
|
|
2027
|
+
plugReg.plug,
|
|
1258
2028
|
vars,
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
const
|
|
1262
|
-
|
|
1263
|
-
return plugReg.plug.get(path, opts(extra));
|
|
1264
|
-
},
|
|
1265
|
-
post(path, extra) {
|
|
1266
|
-
return plugReg.plug.post(path, opts(extra));
|
|
1267
|
-
},
|
|
1268
|
-
put(path, extra) {
|
|
1269
|
-
return plugReg.plug.put(path, opts(extra));
|
|
1270
|
-
},
|
|
1271
|
-
patch(path, extra) {
|
|
1272
|
-
return plugReg.plug.patch(path, opts(extra));
|
|
1273
|
-
},
|
|
1274
|
-
delete(path, extra) {
|
|
1275
|
-
return plugReg.plug.delete(path, opts(extra));
|
|
1276
|
-
}
|
|
2029
|
+
secret ? setFlowVars : void 0
|
|
2030
|
+
);
|
|
2031
|
+
const plugVarsByName = {
|
|
2032
|
+
[plugName]: vars
|
|
1277
2033
|
};
|
|
2034
|
+
if (flowReg.to && plugNames.has(flowReg.to)) {
|
|
2035
|
+
plugVarsByName[flowReg.to] = secret ? await getVars(flowReg.to).catch(() => ({})) : {};
|
|
2036
|
+
}
|
|
1278
2037
|
const flowContext = {
|
|
1279
2038
|
id: flowId,
|
|
1280
2039
|
name: flowReg.name,
|
|
2040
|
+
plugName,
|
|
1281
2041
|
type: flowReg.type,
|
|
1282
2042
|
resource: flowReg.resource ?? null,
|
|
1283
2043
|
to: flowReg.to ?? null
|
|
@@ -1290,7 +2050,9 @@ function khotan(config) {
|
|
|
1290
2050
|
runType,
|
|
1291
2051
|
body: requestBody["body"],
|
|
1292
2052
|
vars,
|
|
1293
|
-
|
|
2053
|
+
plugVarsByName,
|
|
2054
|
+
khotanRunId: runId,
|
|
2055
|
+
khotanInstanceId: instanceId
|
|
1294
2056
|
}
|
|
1295
2057
|
]);
|
|
1296
2058
|
const workflowRunId = getWorkflowRunId(result2);
|
|
@@ -1315,7 +2077,8 @@ function khotan(config) {
|
|
|
1315
2077
|
runType,
|
|
1316
2078
|
body: requestBody["body"],
|
|
1317
2079
|
vars,
|
|
1318
|
-
setVars: setFlowVars
|
|
2080
|
+
setVars: setFlowVars,
|
|
2081
|
+
cache: createCacheInstance
|
|
1319
2082
|
});
|
|
1320
2083
|
const runResult = toFlowRunResult(result);
|
|
1321
2084
|
const { counters, status } = await completeRunOk(runResult);
|
|
@@ -1336,6 +2099,112 @@ function khotan(config) {
|
|
|
1336
2099
|
);
|
|
1337
2100
|
}
|
|
1338
2101
|
}
|
|
2102
|
+
async function wasFlowTriggeredInMinuteWindow(flowId, slotStart, slotEnd) {
|
|
2103
|
+
const runs = await adapter.listRuns(flowId);
|
|
2104
|
+
return runs.some((run) => {
|
|
2105
|
+
const startedAt = coerceDate(run["startedAt"]);
|
|
2106
|
+
if (!startedAt) return false;
|
|
2107
|
+
const startedAtMs = startedAt.getTime();
|
|
2108
|
+
return startedAtMs >= slotStart.getTime() && startedAtMs < slotEnd.getTime();
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
async function dispatchScheduledFlows(options = {}) {
|
|
2112
|
+
await init();
|
|
2113
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
2114
|
+
const slotStart = startOfUtcMinute(now);
|
|
2115
|
+
const slotEnd = new Date(slotStart.getTime() + 6e4);
|
|
2116
|
+
const runType = options.runType ?? "full";
|
|
2117
|
+
const registeredFlows = (await adapter.listFlows()).filter(
|
|
2118
|
+
(flow2) => isRegisteredFlowRecord(flow2)
|
|
2119
|
+
);
|
|
2120
|
+
const scheduledFlows = registeredFlows.filter(
|
|
2121
|
+
(flow2) => typeof flow2["schedule"] === "string" && flow2["schedule"].trim()
|
|
2122
|
+
);
|
|
2123
|
+
const triggered = [];
|
|
2124
|
+
const skipped = [];
|
|
2125
|
+
for (const flow2 of scheduledFlows) {
|
|
2126
|
+
const flowId = typeof flow2["id"] === "string" ? flow2["id"] : null;
|
|
2127
|
+
const flowName = typeof flow2["name"] === "string" ? flow2["name"] : null;
|
|
2128
|
+
const plugName = typeof flow2["plugName"] === "string" ? flow2["plugName"] : null;
|
|
2129
|
+
const schedule = typeof flow2["schedule"] === "string" ? flow2["schedule"].trim() : "";
|
|
2130
|
+
if (!flowId || !flowName || !plugName || !schedule) continue;
|
|
2131
|
+
if (flow2["enabled"] === false) {
|
|
2132
|
+
skipped.push({
|
|
2133
|
+
flowId,
|
|
2134
|
+
flowName,
|
|
2135
|
+
plugName,
|
|
2136
|
+
schedule,
|
|
2137
|
+
reason: "disabled"
|
|
2138
|
+
});
|
|
2139
|
+
continue;
|
|
2140
|
+
}
|
|
2141
|
+
let isDue = false;
|
|
2142
|
+
try {
|
|
2143
|
+
isDue = matchesCronSchedule(schedule, now);
|
|
2144
|
+
} catch (error) {
|
|
2145
|
+
skipped.push({
|
|
2146
|
+
flowId,
|
|
2147
|
+
flowName,
|
|
2148
|
+
plugName,
|
|
2149
|
+
schedule,
|
|
2150
|
+
reason: "invalid_schedule",
|
|
2151
|
+
detail: getErrorMessage(error)
|
|
2152
|
+
});
|
|
2153
|
+
continue;
|
|
2154
|
+
}
|
|
2155
|
+
if (!isDue) {
|
|
2156
|
+
skipped.push({
|
|
2157
|
+
flowId,
|
|
2158
|
+
flowName,
|
|
2159
|
+
plugName,
|
|
2160
|
+
schedule,
|
|
2161
|
+
reason: "not_due"
|
|
2162
|
+
});
|
|
2163
|
+
continue;
|
|
2164
|
+
}
|
|
2165
|
+
if (await wasFlowTriggeredInMinuteWindow(flowId, slotStart, slotEnd)) {
|
|
2166
|
+
skipped.push({
|
|
2167
|
+
flowId,
|
|
2168
|
+
flowName,
|
|
2169
|
+
plugName,
|
|
2170
|
+
schedule,
|
|
2171
|
+
reason: "already_triggered"
|
|
2172
|
+
});
|
|
2173
|
+
continue;
|
|
2174
|
+
}
|
|
2175
|
+
const response = await triggerFlowRun(flowId, { runType });
|
|
2176
|
+
const payload = await response.json().catch(() => ({}));
|
|
2177
|
+
if (!response.ok) {
|
|
2178
|
+
skipped.push({
|
|
2179
|
+
flowId,
|
|
2180
|
+
flowName,
|
|
2181
|
+
plugName,
|
|
2182
|
+
schedule,
|
|
2183
|
+
reason: "trigger_failed",
|
|
2184
|
+
status: response.status,
|
|
2185
|
+
detail: typeof payload["error"] === "string" ? payload["error"] : response.statusText
|
|
2186
|
+
});
|
|
2187
|
+
continue;
|
|
2188
|
+
}
|
|
2189
|
+
triggered.push({
|
|
2190
|
+
flowId,
|
|
2191
|
+
flowName,
|
|
2192
|
+
plugName,
|
|
2193
|
+
schedule,
|
|
2194
|
+
runId: payload["id"] ?? null,
|
|
2195
|
+
workflowRunId: payload["workflowRunId"] ?? null,
|
|
2196
|
+
status: typeof payload["status"] === "string" ? payload["status"] : "running"
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
return {
|
|
2200
|
+
ok: true,
|
|
2201
|
+
tickAt: slotStart.toISOString(),
|
|
2202
|
+
runType,
|
|
2203
|
+
evaluated: scheduledFlows.length,
|
|
2204
|
+
triggered,
|
|
2205
|
+
skipped
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
1339
2208
|
async function resolveFlowId(flowNameOrId, options = {}) {
|
|
1340
2209
|
await init();
|
|
1341
2210
|
const byId = await adapter.getFlow(flowNameOrId);
|
|
@@ -1407,13 +2276,32 @@ function khotan(config) {
|
|
|
1407
2276
|
const plugsIdx = segments.indexOf("plugs");
|
|
1408
2277
|
const flowsIdx = segments.indexOf("flows");
|
|
1409
2278
|
const resourcesIdx = segments.indexOf("resources");
|
|
2279
|
+
const cachesIdx = segments.indexOf("caches");
|
|
1410
2280
|
const mappingsIdx = segments.indexOf("mappings");
|
|
1411
2281
|
const runsIdx = segments.indexOf("runs");
|
|
1412
2282
|
const wiresIdx = segments.indexOf("wires");
|
|
1413
2283
|
const webhookHandlersIdx = segments.indexOf("webhook-handlers");
|
|
1414
2284
|
const webhookEventsIdx = segments.indexOf("webhook-events");
|
|
1415
2285
|
const variablesIdx = segments.indexOf("variables");
|
|
2286
|
+
const cronIdx = segments.indexOf("cron");
|
|
2287
|
+
const webhookIdx = segments.indexOf("webhook");
|
|
1416
2288
|
const debugIdx = segments.indexOf("debug");
|
|
2289
|
+
const isInboundWebhook = webhookIdx !== -1 && webhookIdx === segments.length - 2;
|
|
2290
|
+
const isCronRoute = cronIdx !== -1 && cronIdx === segments.length - 1;
|
|
2291
|
+
const isDebugRoute = debugIdx !== -1;
|
|
2292
|
+
if (authorize && !isInboundWebhook && !isCronRoute && !isDebugRoute) {
|
|
2293
|
+
let allowed = await isCliRequestAuthorized(request, secret);
|
|
2294
|
+
if (!allowed) {
|
|
2295
|
+
try {
|
|
2296
|
+
allowed = await authorize(request);
|
|
2297
|
+
} catch {
|
|
2298
|
+
allowed = false;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
if (!allowed) {
|
|
2302
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
1417
2305
|
const limit = Math.min(
|
|
1418
2306
|
Math.max(
|
|
1419
2307
|
Number.parseInt(url.searchParams.get("limit") ?? "20", 10) || 20,
|
|
@@ -1425,17 +2313,43 @@ function khotan(config) {
|
|
|
1425
2313
|
Number.parseInt(url.searchParams.get("offset") ?? "0", 10) || 0,
|
|
1426
2314
|
0
|
|
1427
2315
|
);
|
|
2316
|
+
const search = url.searchParams.get("search")?.trim() || void 0;
|
|
2317
|
+
const wantsMappingPage = url.searchParams.has("limit") || url.searchParams.has("offset") || url.searchParams.has("search");
|
|
1428
2318
|
if (request.method === "GET") {
|
|
2319
|
+
if (cachesIdx !== -1 && cachesIdx === segments.length - 3) {
|
|
2320
|
+
const cacheName = decodeURIComponent(segments[cachesIdx + 1]);
|
|
2321
|
+
const key = decodeURIComponent(segments[cachesIdx + 2]);
|
|
2322
|
+
try {
|
|
2323
|
+
const entry = await readCacheEntry(cacheName, key);
|
|
2324
|
+
if (!entry) {
|
|
2325
|
+
return Response.json({ error: "Cache entry not found" }, { status: 404 });
|
|
2326
|
+
}
|
|
2327
|
+
return Response.json({
|
|
2328
|
+
cache: cacheName,
|
|
2329
|
+
key: entry.key,
|
|
2330
|
+
value: entry.value,
|
|
2331
|
+
expiresAt: entry.expiresAt
|
|
2332
|
+
});
|
|
2333
|
+
} catch (error) {
|
|
2334
|
+
const message = error instanceof Error ? error.message : "Invalid cache request";
|
|
2335
|
+
return Response.json({ error: message }, { status: 400 });
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
if (cronIdx !== -1 && cronIdx === segments.length - 1) {
|
|
2339
|
+
if (!isCronRequestAuthorized(request)) {
|
|
2340
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2341
|
+
}
|
|
2342
|
+
const result = await dispatchScheduledFlows();
|
|
2343
|
+
return Response.json(result);
|
|
2344
|
+
}
|
|
1429
2345
|
if (debugIdx !== -1 && debugIdx === segments.length - 1) {
|
|
1430
|
-
|
|
1431
|
-
if (!debugActive) {
|
|
2346
|
+
if (!isDebugEnabled()) {
|
|
1432
2347
|
return Response.json({ error: "Not found" }, { status: 404 });
|
|
1433
2348
|
}
|
|
1434
2349
|
return Response.json({ enabled: true });
|
|
1435
2350
|
}
|
|
1436
2351
|
if (debugIdx !== -1 && debugIdx === segments.length - 2) {
|
|
1437
|
-
|
|
1438
|
-
if (!debugActive) {
|
|
2352
|
+
if (!isDebugEnabled()) {
|
|
1439
2353
|
return Response.json({ error: "Not found" }, { status: 404 });
|
|
1440
2354
|
}
|
|
1441
2355
|
const plugName = segments[debugIdx + 1];
|
|
@@ -1657,12 +2571,27 @@ function khotan(config) {
|
|
|
1657
2571
|
const filtered = data.filter(
|
|
1658
2572
|
(r) => typeof r["name"] === "string" && resourceNames.has(r["name"])
|
|
1659
2573
|
);
|
|
1660
|
-
return Response.json(filtered);
|
|
2574
|
+
return Response.json(filtered.map(decorateResourceRecord));
|
|
1661
2575
|
}
|
|
1662
2576
|
if (resourcesIdx !== -1 && resourcesIdx === segments.length - 3 && segments[resourcesIdx + 2] === "mappings") {
|
|
1663
2577
|
const resourceId = segments[resourcesIdx + 1];
|
|
1664
|
-
const
|
|
1665
|
-
|
|
2578
|
+
const resource = await getRegisteredResourceById(resourceId);
|
|
2579
|
+
if (!resource) {
|
|
2580
|
+
return Response.json(
|
|
2581
|
+
{ error: "Resource not found" },
|
|
2582
|
+
{ status: 404 }
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
const page = await listMappings({
|
|
2586
|
+
resourceId,
|
|
2587
|
+
limit,
|
|
2588
|
+
offset,
|
|
2589
|
+
...search ? { search } : {}
|
|
2590
|
+
});
|
|
2591
|
+
if (!wantsMappingPage) {
|
|
2592
|
+
return Response.json(page.items);
|
|
2593
|
+
}
|
|
2594
|
+
return Response.json(page);
|
|
1666
2595
|
}
|
|
1667
2596
|
if (resourcesIdx !== -1 && resourcesIdx === segments.length - 2) {
|
|
1668
2597
|
const resourceId = segments[resourcesIdx + 1];
|
|
@@ -1674,7 +2603,7 @@ function khotan(config) {
|
|
|
1674
2603
|
);
|
|
1675
2604
|
}
|
|
1676
2605
|
const flows = await adapter.getResourceFlows(resourceId);
|
|
1677
|
-
return Response.json({ ...resource, flows });
|
|
2606
|
+
return Response.json({ ...decorateResourceRecord(resource), flows });
|
|
1678
2607
|
}
|
|
1679
2608
|
if (mappingsIdx !== -1 && mappingsIdx === segments.length - 2) {
|
|
1680
2609
|
const mappingId = segments[mappingsIdx + 1];
|
|
@@ -1686,7 +2615,42 @@ function khotan(config) {
|
|
|
1686
2615
|
}
|
|
1687
2616
|
}
|
|
1688
2617
|
if (request.method === "POST") {
|
|
1689
|
-
|
|
2618
|
+
if (cachesIdx !== -1 && cachesIdx === segments.length - 3) {
|
|
2619
|
+
const cacheName = decodeURIComponent(segments[cachesIdx + 1]);
|
|
2620
|
+
const key = decodeURIComponent(segments[cachesIdx + 2]);
|
|
2621
|
+
const body = await request.json().catch(() => ({}));
|
|
2622
|
+
if (!("value" in body)) {
|
|
2623
|
+
return Response.json({ error: "Cache writes require a value" }, { status: 400 });
|
|
2624
|
+
}
|
|
2625
|
+
try {
|
|
2626
|
+
const cacheHandle = createCacheInstance(cacheName);
|
|
2627
|
+
await cacheHandle.set(key, body.value);
|
|
2628
|
+
const entry = await readCacheEntry(cacheName, key);
|
|
2629
|
+
return Response.json({
|
|
2630
|
+
cache: cacheName,
|
|
2631
|
+
key,
|
|
2632
|
+
value: body.value,
|
|
2633
|
+
expiresAt: entry?.expiresAt ?? null
|
|
2634
|
+
});
|
|
2635
|
+
} catch (error) {
|
|
2636
|
+
const message = error instanceof Error ? error.message : "Invalid cache payload";
|
|
2637
|
+
return Response.json({ error: message }, { status: 400 });
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
if (cronIdx !== -1 && cronIdx === segments.length - 1) {
|
|
2641
|
+
if (!isCronRequestAuthorized(request)) {
|
|
2642
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2643
|
+
}
|
|
2644
|
+
let body = {};
|
|
2645
|
+
try {
|
|
2646
|
+
body = await request.json();
|
|
2647
|
+
} catch {
|
|
2648
|
+
body = {};
|
|
2649
|
+
}
|
|
2650
|
+
const runType = typeof body["runType"] === "string" ? body["runType"] : "full";
|
|
2651
|
+
const result = await dispatchScheduledFlows({ runType });
|
|
2652
|
+
return Response.json(result);
|
|
2653
|
+
}
|
|
1690
2654
|
if (webhookIdx !== -1 && webhookIdx === segments.length - 2) {
|
|
1691
2655
|
const plugName = segments[webhookIdx + 1];
|
|
1692
2656
|
const plugReg = plugs.find((p) => p.name === plugName);
|
|
@@ -1800,7 +2764,13 @@ function khotan(config) {
|
|
|
1800
2764
|
}
|
|
1801
2765
|
try {
|
|
1802
2766
|
const result = await startWorkflow(c.workflow, [
|
|
1803
|
-
{
|
|
2767
|
+
{
|
|
2768
|
+
event,
|
|
2769
|
+
eventType,
|
|
2770
|
+
headers,
|
|
2771
|
+
khotanRunId,
|
|
2772
|
+
khotanInstanceId: instanceId
|
|
2773
|
+
}
|
|
1804
2774
|
]);
|
|
1805
2775
|
const workflowRunId = result && typeof result === "object" ? "runId" in result ? String(result.runId) : "id" in result ? String(result.id) : null : null;
|
|
1806
2776
|
if (workflowRunId) {
|
|
@@ -1863,7 +2833,14 @@ function khotan(config) {
|
|
|
1863
2833
|
}
|
|
1864
2834
|
try {
|
|
1865
2835
|
const result = await startWorkflow(p.workflow, [
|
|
1866
|
-
{
|
|
2836
|
+
{
|
|
2837
|
+
event,
|
|
2838
|
+
eventType,
|
|
2839
|
+
headers,
|
|
2840
|
+
destVars,
|
|
2841
|
+
khotanRunId,
|
|
2842
|
+
khotanInstanceId: instanceId
|
|
2843
|
+
}
|
|
1867
2844
|
]);
|
|
1868
2845
|
const workflowRunId = result && typeof result === "object" ? "runId" in result ? String(result.runId) : "id" in result ? String(result.id) : null : null;
|
|
1869
2846
|
if (workflowRunId) {
|
|
@@ -1890,8 +2867,7 @@ function khotan(config) {
|
|
|
1890
2867
|
return Response.json({ received: true }, { status: 202 });
|
|
1891
2868
|
}
|
|
1892
2869
|
if (debugIdx !== -1 && debugIdx === segments.length - 2) {
|
|
1893
|
-
|
|
1894
|
-
if (!debugActive) {
|
|
2870
|
+
if (!isDebugEnabled()) {
|
|
1895
2871
|
return Response.json({ error: "Not found" }, { status: 404 });
|
|
1896
2872
|
}
|
|
1897
2873
|
const plugName = segments[debugIdx + 1];
|
|
@@ -2091,7 +3067,38 @@ function khotan(config) {
|
|
|
2091
3067
|
}
|
|
2092
3068
|
if (mappingsIdx !== -1 && mappingsIdx === segments.length - 2 && segments[mappingsIdx + 1] === "lookup") {
|
|
2093
3069
|
const body = await request.json();
|
|
2094
|
-
|
|
3070
|
+
if (!body || typeof body !== "object" || typeof body["resourceId"] !== "string") {
|
|
3071
|
+
return Response.json(
|
|
3072
|
+
{
|
|
3073
|
+
error: "Lookup requires resourceId plus either connectValue or plugName with ref"
|
|
3074
|
+
},
|
|
3075
|
+
{ status: 400 }
|
|
3076
|
+
);
|
|
3077
|
+
}
|
|
3078
|
+
const hasConnectValue = "connectValue" in body;
|
|
3079
|
+
const hasPlugRef = "plugName" in body && typeof body.plugName === "string" && "ref" in body && typeof body.ref === "string";
|
|
3080
|
+
if (!hasConnectValue && !hasPlugRef) {
|
|
3081
|
+
return Response.json(
|
|
3082
|
+
{
|
|
3083
|
+
error: "Lookup requires either connectValue or plugName with ref"
|
|
3084
|
+
},
|
|
3085
|
+
{ status: 400 }
|
|
3086
|
+
);
|
|
3087
|
+
}
|
|
3088
|
+
let mapping;
|
|
3089
|
+
try {
|
|
3090
|
+
mapping = hasConnectValue ? await lookupMapping({
|
|
3091
|
+
resourceId: body.resourceId,
|
|
3092
|
+
connectValue: body.connectValue
|
|
3093
|
+
}) : await lookupMapping({
|
|
3094
|
+
resourceId: body.resourceId,
|
|
3095
|
+
plugName: body.plugName,
|
|
3096
|
+
ref: body.ref
|
|
3097
|
+
});
|
|
3098
|
+
} catch (error) {
|
|
3099
|
+
const message = error instanceof Error ? error.message : "Invalid lookup request";
|
|
3100
|
+
return Response.json({ error: message }, { status: 400 });
|
|
3101
|
+
}
|
|
2095
3102
|
if (!mapping) {
|
|
2096
3103
|
return Response.json({ error: "Mapping not found" }, { status: 404 });
|
|
2097
3104
|
}
|
|
@@ -2099,11 +3106,17 @@ function khotan(config) {
|
|
|
2099
3106
|
}
|
|
2100
3107
|
if (mappingsIdx !== -1 && mappingsIdx === segments.length - 1) {
|
|
2101
3108
|
const body = await request.json();
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
3109
|
+
try {
|
|
3110
|
+
const existing = await lookupMapping({
|
|
3111
|
+
resourceId: body.resourceId,
|
|
3112
|
+
connectValue: body.connectValue
|
|
3113
|
+
});
|
|
3114
|
+
const saved = await upsertMapping(body);
|
|
3115
|
+
return Response.json(saved, { status: existing ? 200 : 201 });
|
|
3116
|
+
} catch (error) {
|
|
3117
|
+
const message = error instanceof Error ? error.message : "Invalid mapping payload";
|
|
3118
|
+
return Response.json({ error: message }, { status: 400 });
|
|
3119
|
+
}
|
|
2107
3120
|
}
|
|
2108
3121
|
}
|
|
2109
3122
|
if (request.method === "PATCH") {
|
|
@@ -2141,11 +3154,30 @@ function khotan(config) {
|
|
|
2141
3154
|
if (mappingsIdx !== -1 && mappingsIdx === segments.length - 2) {
|
|
2142
3155
|
const mappingId = segments[mappingsIdx + 1];
|
|
2143
3156
|
const body = await request.json();
|
|
2144
|
-
|
|
2145
|
-
|
|
3157
|
+
try {
|
|
3158
|
+
const saved = await updateMapping(mappingId, body);
|
|
3159
|
+
return Response.json(saved);
|
|
3160
|
+
} catch (error) {
|
|
3161
|
+
const message = error instanceof Error ? error.message : "Invalid mapping payload";
|
|
3162
|
+
return Response.json(
|
|
3163
|
+
{ error: message },
|
|
3164
|
+
{ status: message.includes("not found") ? 404 : 400 }
|
|
3165
|
+
);
|
|
3166
|
+
}
|
|
2146
3167
|
}
|
|
2147
3168
|
}
|
|
2148
3169
|
if (request.method === "DELETE") {
|
|
3170
|
+
if (cachesIdx !== -1 && cachesIdx === segments.length - 3) {
|
|
3171
|
+
const cacheName = decodeURIComponent(segments[cachesIdx + 1]);
|
|
3172
|
+
const key = decodeURIComponent(segments[cachesIdx + 2]);
|
|
3173
|
+
try {
|
|
3174
|
+
await createCacheInstance(cacheName).delete(key);
|
|
3175
|
+
return new Response(null, { status: 204 });
|
|
3176
|
+
} catch (error) {
|
|
3177
|
+
const message = error instanceof Error ? error.message : "Invalid cache request";
|
|
3178
|
+
return Response.json({ error: message }, { status: 400 });
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
2149
3181
|
if (variablesIdx !== -1 && variablesIdx === segments.length - 2) {
|
|
2150
3182
|
const plugName = segments[variablesIdx + 1];
|
|
2151
3183
|
if (!plugNames.has(plugName)) {
|
|
@@ -2177,6 +3209,10 @@ function khotan(config) {
|
|
|
2177
3209
|
}
|
|
2178
3210
|
if (mappingsIdx !== -1 && mappingsIdx === segments.length - 2) {
|
|
2179
3211
|
const mappingId = segments[mappingsIdx + 1];
|
|
3212
|
+
const existing = await adapter.getMapping(mappingId);
|
|
3213
|
+
if (!existing) {
|
|
3214
|
+
return Response.json({ error: "Mapping not found" }, { status: 404 });
|
|
3215
|
+
}
|
|
2180
3216
|
await adapter.deleteMapping(mappingId);
|
|
2181
3217
|
return new Response(null, { status: 204 });
|
|
2182
3218
|
}
|
|
@@ -2280,11 +3316,25 @@ function khotan(config) {
|
|
|
2280
3316
|
}
|
|
2281
3317
|
return plugReg.plug;
|
|
2282
3318
|
}
|
|
3319
|
+
khotanRuntimeRegistry.set(instanceId, {
|
|
3320
|
+
cache: createCacheInstance,
|
|
3321
|
+
listMappings,
|
|
3322
|
+
lookupMapping,
|
|
3323
|
+
upsertMapping,
|
|
3324
|
+
updateMapping,
|
|
3325
|
+
deleteMapping
|
|
3326
|
+
});
|
|
2283
3327
|
return {
|
|
2284
3328
|
handler,
|
|
2285
3329
|
init,
|
|
2286
3330
|
flow,
|
|
2287
3331
|
wire,
|
|
3332
|
+
cache: createCacheInstance,
|
|
3333
|
+
listMappings,
|
|
3334
|
+
lookupMapping,
|
|
3335
|
+
upsertMapping,
|
|
3336
|
+
updateMapping,
|
|
3337
|
+
deleteMapping,
|
|
2288
3338
|
getVars,
|
|
2289
3339
|
setVars,
|
|
2290
3340
|
clearVars,
|
|
@@ -2306,6 +3356,6 @@ function toNextJsHandler(factoryHandler) {
|
|
|
2306
3356
|
};
|
|
2307
3357
|
}
|
|
2308
3358
|
|
|
2309
|
-
export { __setWorkflowGetRunForTests, __setWorkflowGetWritableForTests, __setWorkflowStartForTests, drizzleAdapter, khotan, sendUpdate, toNextJsHandler };
|
|
3359
|
+
export { __setWorkflowGetRunForTests, __setWorkflowGetWritableForTests, __setWorkflowStartForTests, bindWorkflowPlug, deriveCliToken, drizzleAdapter, khotan, khotanCache, khotanMappings, sendUpdate, toNextJsHandler };
|
|
2310
3360
|
//# sourceMappingURL=factory.js.map
|
|
2311
3361
|
//# sourceMappingURL=factory.js.map
|