khotan-data 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -6
- package/dist/cli.js +292 -8
- package/dist/factory.cjs +1083 -99
- package/dist/factory.cjs.map +1 -1
- package/dist/factory.d.cts +225 -38
- package/dist/factory.d.ts +225 -38
- package/dist/factory.js +1082 -101
- package/dist/factory.js.map +1 -1
- 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/inflow.ts +5 -6
- package/dist/templates/khotan-config.ts +13 -4
- package/dist/templates/mapping-browser.tsx +761 -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/relay.example.ts +11 -1
- package/dist/templates/relay.ts +16 -7
- package/dist/templates/schema.ts +81 -0
- package/dist/templates/skill-plug.md +38 -15
- package/dist/templates/skill-setup.md +44 -2
- package/package.json +1 -1
package/dist/factory.cjs
CHANGED
|
@@ -164,7 +164,7 @@ var khotanRuns = pgCore.pgTable(
|
|
|
164
164
|
)
|
|
165
165
|
]
|
|
166
166
|
);
|
|
167
|
-
var
|
|
167
|
+
var khotanMappingsTable = pgCore.pgTable(
|
|
168
168
|
"khotan_mappings",
|
|
169
169
|
{
|
|
170
170
|
id: pgCore.text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
@@ -184,6 +184,39 @@ var khotanMappings = pgCore.pgTable(
|
|
|
184
184
|
pgCore.index("khotan_mappings_refs_gin_idx").using("gin", table.refs)
|
|
185
185
|
]
|
|
186
186
|
);
|
|
187
|
+
var khotanCaches = pgCore.pgTable(
|
|
188
|
+
"khotan_caches",
|
|
189
|
+
{
|
|
190
|
+
id: pgCore.text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
191
|
+
name: pgCore.text("name").notNull().unique(),
|
|
192
|
+
scope: pgCore.jsonb("scope").$type(),
|
|
193
|
+
ttlSeconds: pgCore.integer("ttl_seconds"),
|
|
194
|
+
createdAt: pgCore.timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
195
|
+
updatedAt: pgCore.timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
|
|
196
|
+
},
|
|
197
|
+
(table) => [pgCore.index("khotan_caches_name_idx").on(table.name)]
|
|
198
|
+
);
|
|
199
|
+
var khotanCacheEntries = pgCore.pgTable(
|
|
200
|
+
"khotan_cache_entries",
|
|
201
|
+
{
|
|
202
|
+
id: pgCore.text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
203
|
+
cacheId: pgCore.text("cache_id").notNull(),
|
|
204
|
+
key: pgCore.text("key").notNull(),
|
|
205
|
+
value: pgCore.jsonb("value").notNull().$type(),
|
|
206
|
+
expiresAt: pgCore.timestamp("expires_at", { withTimezone: true }),
|
|
207
|
+
createdAt: pgCore.timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
208
|
+
updatedAt: pgCore.timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
|
|
209
|
+
},
|
|
210
|
+
(table) => [
|
|
211
|
+
pgCore.unique("khotan_cache_entries_cache_id_key_unique").on(
|
|
212
|
+
table.cacheId,
|
|
213
|
+
table.key
|
|
214
|
+
),
|
|
215
|
+
pgCore.index("khotan_cache_entries_cache_id_idx").on(table.cacheId),
|
|
216
|
+
pgCore.index("khotan_cache_entries_cache_id_key_idx").on(table.cacheId, table.key),
|
|
217
|
+
pgCore.index("khotan_cache_entries_expires_at_idx").on(table.expiresAt)
|
|
218
|
+
]
|
|
219
|
+
);
|
|
187
220
|
async function deriveKey(secret) {
|
|
188
221
|
const encoded = new TextEncoder().encode(secret);
|
|
189
222
|
const hash = await crypto.subtle.digest("SHA-256", encoded);
|
|
@@ -228,6 +261,63 @@ function hexToBytes(hex) {
|
|
|
228
261
|
function bytesToHex(bytes) {
|
|
229
262
|
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
230
263
|
}
|
|
264
|
+
function bindPlugWithVars(plug, vars, setVars) {
|
|
265
|
+
const opts = (extra) => ({
|
|
266
|
+
...extra,
|
|
267
|
+
vars,
|
|
268
|
+
...setVars ? { _setVars: setVars } : {}
|
|
269
|
+
});
|
|
270
|
+
return {
|
|
271
|
+
get(path, extra) {
|
|
272
|
+
return plug.get(path, opts(extra));
|
|
273
|
+
},
|
|
274
|
+
post(path, extra) {
|
|
275
|
+
return plug.post(path, opts(extra));
|
|
276
|
+
},
|
|
277
|
+
put(path, extra) {
|
|
278
|
+
return plug.put(path, opts(extra));
|
|
279
|
+
},
|
|
280
|
+
patch(path, extra) {
|
|
281
|
+
return plug.patch(path, opts(extra));
|
|
282
|
+
},
|
|
283
|
+
delete(path, extra) {
|
|
284
|
+
return plug.delete(path, opts(extra));
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function bindWorkflowPlug(plug, ctx, plugName = ctx.flow.plugName) {
|
|
289
|
+
const vars = plugName === ctx.flow.plugName ? ctx.vars : ctx.plugVarsByName?.[plugName] ?? {};
|
|
290
|
+
if (plugName !== ctx.flow.plugName) {
|
|
291
|
+
ctx.plugVarsByName ??= {};
|
|
292
|
+
ctx.plugVarsByName[plugName] = vars;
|
|
293
|
+
}
|
|
294
|
+
return bindPlugWithVars(plug, vars, async (updates) => {
|
|
295
|
+
Object.assign(vars, updates);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
var khotanRuntimeRegistry = /* @__PURE__ */ new Map();
|
|
299
|
+
function getWorkflowRuntimeHelpers(ctx) {
|
|
300
|
+
const helpers = khotanRuntimeRegistry.get(ctx.khotanInstanceId);
|
|
301
|
+
if (!helpers) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`Khotan runtime helpers for instance "${ctx.khotanInstanceId}" are not registered`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
return helpers;
|
|
307
|
+
}
|
|
308
|
+
function khotanCache(ctx, cacheName) {
|
|
309
|
+
return getWorkflowRuntimeHelpers(ctx).cache(cacheName);
|
|
310
|
+
}
|
|
311
|
+
function khotanMappings(ctx) {
|
|
312
|
+
const helpers = getWorkflowRuntimeHelpers(ctx);
|
|
313
|
+
return {
|
|
314
|
+
list: helpers.listMappings,
|
|
315
|
+
lookup: helpers.lookupMapping,
|
|
316
|
+
upsert: helpers.upsertMapping,
|
|
317
|
+
update: helpers.updateMapping,
|
|
318
|
+
delete: helpers.deleteMapping
|
|
319
|
+
};
|
|
320
|
+
}
|
|
231
321
|
function drizzleAdapter(db) {
|
|
232
322
|
return {
|
|
233
323
|
async upsertPlug(plug) {
|
|
@@ -376,27 +466,93 @@ function drizzleAdapter(db) {
|
|
|
376
466
|
async upsertResource(resource) {
|
|
377
467
|
const rows = await db.insert(khotanResources).values({
|
|
378
468
|
name: resource.name,
|
|
379
|
-
connectField: resource.connectField,
|
|
469
|
+
connectField: serializeConnectField(resource.connectField),
|
|
380
470
|
description: resource.description ?? null
|
|
381
471
|
}).onConflictDoUpdate({
|
|
382
472
|
target: khotanResources.name,
|
|
383
473
|
set: {
|
|
384
|
-
connectField: resource.connectField,
|
|
474
|
+
connectField: serializeConnectField(resource.connectField),
|
|
385
475
|
description: resource.description ?? null,
|
|
386
476
|
updatedAt: /* @__PURE__ */ new Date()
|
|
387
477
|
}
|
|
388
478
|
}).returning({ id: khotanResources.id });
|
|
389
479
|
return { id: rows[0].id };
|
|
390
480
|
},
|
|
481
|
+
async upsertCache(cache) {
|
|
482
|
+
const rows = await db.insert(khotanCaches).values({
|
|
483
|
+
name: cache.name,
|
|
484
|
+
scope: cache.scope ?? null,
|
|
485
|
+
ttlSeconds: cache.ttlSeconds ?? null
|
|
486
|
+
}).onConflictDoUpdate({
|
|
487
|
+
target: khotanCaches.name,
|
|
488
|
+
set: {
|
|
489
|
+
scope: cache.scope ?? null,
|
|
490
|
+
ttlSeconds: cache.ttlSeconds ?? null,
|
|
491
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
492
|
+
}
|
|
493
|
+
}).returning({ id: khotanCaches.id });
|
|
494
|
+
return { id: rows[0].id };
|
|
495
|
+
},
|
|
496
|
+
async getCacheByName(name) {
|
|
497
|
+
const rows = await db.select().from(khotanCaches).where(drizzleOrm.eq(khotanCaches.name, name)).limit(1);
|
|
498
|
+
return rows[0] ?? null;
|
|
499
|
+
},
|
|
500
|
+
async getCacheEntry(cacheId, key) {
|
|
501
|
+
const rows = await db.select({
|
|
502
|
+
id: khotanCacheEntries.id,
|
|
503
|
+
cacheId: khotanCacheEntries.cacheId,
|
|
504
|
+
key: khotanCacheEntries.key,
|
|
505
|
+
value: khotanCacheEntries.value,
|
|
506
|
+
expiresAt: khotanCacheEntries.expiresAt,
|
|
507
|
+
createdAt: khotanCacheEntries.createdAt,
|
|
508
|
+
updatedAt: khotanCacheEntries.updatedAt
|
|
509
|
+
}).from(khotanCacheEntries).where(
|
|
510
|
+
drizzleOrm.and(
|
|
511
|
+
drizzleOrm.eq(khotanCacheEntries.cacheId, cacheId),
|
|
512
|
+
drizzleOrm.eq(khotanCacheEntries.key, key)
|
|
513
|
+
)
|
|
514
|
+
).limit(1);
|
|
515
|
+
return rows[0] ?? null;
|
|
516
|
+
},
|
|
517
|
+
async upsertCacheEntry(entry) {
|
|
518
|
+
const existing = await db.select({ id: khotanCacheEntries.id }).from(khotanCacheEntries).where(
|
|
519
|
+
drizzleOrm.and(
|
|
520
|
+
drizzleOrm.eq(khotanCacheEntries.cacheId, entry.cacheId),
|
|
521
|
+
drizzleOrm.eq(khotanCacheEntries.key, entry.key)
|
|
522
|
+
)
|
|
523
|
+
).limit(1);
|
|
524
|
+
const rows = await db.insert(khotanCacheEntries).values({
|
|
525
|
+
cacheId: entry.cacheId,
|
|
526
|
+
key: entry.key,
|
|
527
|
+
value: entry.value,
|
|
528
|
+
expiresAt: entry.expiresAt ?? null
|
|
529
|
+
}).onConflictDoUpdate({
|
|
530
|
+
target: [khotanCacheEntries.cacheId, khotanCacheEntries.key],
|
|
531
|
+
set: {
|
|
532
|
+
value: entry.value,
|
|
533
|
+
expiresAt: entry.expiresAt ?? null,
|
|
534
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
535
|
+
}
|
|
536
|
+
}).returning({ id: khotanCacheEntries.id });
|
|
537
|
+
return { id: rows[0].id, created: existing.length === 0 };
|
|
538
|
+
},
|
|
539
|
+
async deleteCacheEntry(cacheId, key) {
|
|
540
|
+
await db.delete(khotanCacheEntries).where(
|
|
541
|
+
drizzleOrm.and(
|
|
542
|
+
drizzleOrm.eq(khotanCacheEntries.cacheId, cacheId),
|
|
543
|
+
drizzleOrm.eq(khotanCacheEntries.key, key)
|
|
544
|
+
)
|
|
545
|
+
);
|
|
546
|
+
},
|
|
391
547
|
async listResources() {
|
|
392
548
|
const flowCounts = db.select({
|
|
393
549
|
resourceId: khotanFlows.resourceId,
|
|
394
550
|
flowCount: drizzleOrm.count(khotanFlows.id).as("flow_count")
|
|
395
551
|
}).from(khotanFlows).where(drizzleOrm.sql`${khotanFlows.resourceId} is not null`).groupBy(khotanFlows.resourceId).as("flow_counts");
|
|
396
552
|
const mappingCounts = db.select({
|
|
397
|
-
resourceId:
|
|
398
|
-
mappingCount: drizzleOrm.count(
|
|
399
|
-
}).from(
|
|
553
|
+
resourceId: khotanMappingsTable.resourceId,
|
|
554
|
+
mappingCount: drizzleOrm.count(khotanMappingsTable.id).as("mapping_count")
|
|
555
|
+
}).from(khotanMappingsTable).groupBy(khotanMappingsTable.resourceId).as("mapping_counts");
|
|
400
556
|
const rows = await db.select({
|
|
401
557
|
id: khotanResources.id,
|
|
402
558
|
name: khotanResources.name,
|
|
@@ -410,57 +566,79 @@ function drizzleAdapter(db) {
|
|
|
410
566
|
mappingCounts,
|
|
411
567
|
drizzleOrm.eq(khotanResources.id, mappingCounts.resourceId)
|
|
412
568
|
);
|
|
413
|
-
return rows
|
|
569
|
+
return rows.map((row) => ({
|
|
570
|
+
...row,
|
|
571
|
+
connectField: deserializeConnectField(row.connectField)
|
|
572
|
+
}));
|
|
414
573
|
},
|
|
415
574
|
async getResource(id) {
|
|
416
575
|
const rows = await db.select().from(khotanResources).where(drizzleOrm.eq(khotanResources.id, id)).limit(1);
|
|
417
|
-
|
|
576
|
+
if (!rows[0]) return null;
|
|
577
|
+
return {
|
|
578
|
+
...rows[0],
|
|
579
|
+
connectField: deserializeConnectField(rows[0].connectField)
|
|
580
|
+
};
|
|
418
581
|
},
|
|
419
582
|
async getResourceFlows(resourceId) {
|
|
420
583
|
return db.select().from(khotanFlows).where(drizzleOrm.eq(khotanFlows.resourceId, resourceId));
|
|
421
584
|
},
|
|
422
585
|
async upsertMapping(mapping) {
|
|
423
586
|
if (mapping.id) {
|
|
424
|
-
const rows2 = await db.update(
|
|
587
|
+
const rows2 = await db.update(khotanMappingsTable).set({
|
|
425
588
|
resourceId: mapping.resourceId,
|
|
426
589
|
connectValue: mapping.connectValue,
|
|
427
590
|
refs: mapping.refs,
|
|
428
591
|
metadata: mapping.metadata ?? null,
|
|
429
592
|
updatedAt: /* @__PURE__ */ new Date()
|
|
430
|
-
}).where(drizzleOrm.eq(
|
|
593
|
+
}).where(drizzleOrm.eq(khotanMappingsTable.id, mapping.id)).returning({ id: khotanMappingsTable.id });
|
|
431
594
|
return { id: rows2[0].id, created: false };
|
|
432
595
|
}
|
|
433
|
-
const existing = await db.select({ id:
|
|
434
|
-
drizzleOrm.sql`${
|
|
596
|
+
const existing = await db.select({ id: khotanMappingsTable.id }).from(khotanMappingsTable).where(
|
|
597
|
+
drizzleOrm.sql`${khotanMappingsTable.resourceId} = ${mapping.resourceId} and ${khotanMappingsTable.connectValue} = ${mapping.connectValue}`
|
|
435
598
|
).limit(1);
|
|
436
|
-
const rows = await db.insert(
|
|
599
|
+
const rows = await db.insert(khotanMappingsTable).values({
|
|
437
600
|
resourceId: mapping.resourceId,
|
|
438
601
|
connectValue: mapping.connectValue,
|
|
439
602
|
refs: mapping.refs,
|
|
440
603
|
metadata: mapping.metadata ?? null
|
|
441
604
|
}).onConflictDoUpdate({
|
|
442
|
-
target: [
|
|
605
|
+
target: [khotanMappingsTable.resourceId, khotanMappingsTable.connectValue],
|
|
443
606
|
set: {
|
|
444
|
-
refs: drizzleOrm.sql`${
|
|
607
|
+
refs: drizzleOrm.sql`${khotanMappingsTable.refs} || ${JSON.stringify(mapping.refs)}::jsonb`,
|
|
445
608
|
metadata: mapping.metadata ?? null,
|
|
446
609
|
updatedAt: /* @__PURE__ */ new Date()
|
|
447
610
|
}
|
|
448
|
-
}).returning({ id:
|
|
611
|
+
}).returning({ id: khotanMappingsTable.id });
|
|
449
612
|
return { id: rows[0].id, created: existing.length === 0 };
|
|
450
613
|
},
|
|
451
614
|
async getMapping(id) {
|
|
452
|
-
const rows = await db.select().from(
|
|
615
|
+
const rows = await db.select().from(khotanMappingsTable).where(drizzleOrm.eq(khotanMappingsTable.id, id)).limit(1);
|
|
453
616
|
return rows[0] ?? null;
|
|
454
617
|
},
|
|
455
|
-
async listMappings(resourceId) {
|
|
456
|
-
|
|
618
|
+
async listMappings({ resourceId, limit, offset, search }) {
|
|
619
|
+
const normalizedSearch = search?.trim();
|
|
620
|
+
const searchPattern = normalizedSearch ? `%${normalizedSearch.replace(/[%_]/g, "\\$&")}%` : null;
|
|
621
|
+
const filters = searchPattern ? drizzleOrm.sql`${khotanMappingsTable.resourceId} = ${resourceId} and (${khotanMappingsTable.connectValue} ilike ${searchPattern} escape '\\' or ${khotanMappingsTable.refs}::text ilike ${searchPattern} escape '\\' or ${khotanMappingsTable.metadata}::text ilike ${searchPattern} escape '\\')` : drizzleOrm.sql`${khotanMappingsTable.resourceId} = ${resourceId}`;
|
|
622
|
+
const totalRows = await db.select({ total: drizzleOrm.count(khotanMappingsTable.id) }).from(khotanMappingsTable).where(filters);
|
|
623
|
+
const rows = await db.select().from(khotanMappingsTable).where(filters).orderBy(khotanMappingsTable.connectValue, khotanMappingsTable.id).limit(limit + 1).offset(offset);
|
|
624
|
+
return {
|
|
625
|
+
items: rows.slice(0, limit),
|
|
626
|
+
hasMore: rows.length > limit,
|
|
627
|
+
total: totalRows[0]?.total ?? 0
|
|
628
|
+
};
|
|
457
629
|
},
|
|
458
630
|
async deleteMapping(id) {
|
|
459
|
-
await db.delete(
|
|
631
|
+
await db.delete(khotanMappingsTable).where(drizzleOrm.eq(khotanMappingsTable.id, id));
|
|
460
632
|
},
|
|
461
|
-
async lookupMapping(
|
|
462
|
-
|
|
463
|
-
|
|
633
|
+
async lookupMapping(params) {
|
|
634
|
+
if ("connectValue" in params) {
|
|
635
|
+
const rows2 = await db.select().from(khotanMappingsTable).where(
|
|
636
|
+
drizzleOrm.sql`${khotanMappingsTable.resourceId} = ${params.resourceId} and ${khotanMappingsTable.connectValue} = ${params.connectValue}`
|
|
637
|
+
).limit(1);
|
|
638
|
+
return rows2[0] ?? null;
|
|
639
|
+
}
|
|
640
|
+
const rows = await db.select().from(khotanMappingsTable).where(
|
|
641
|
+
drizzleOrm.sql`${khotanMappingsTable.resourceId} = ${params.resourceId} and ${khotanMappingsTable.refs}->>${params.plugName} = ${params.ref}`
|
|
464
642
|
).limit(1);
|
|
465
643
|
return rows[0] ?? null;
|
|
466
644
|
},
|
|
@@ -793,6 +971,144 @@ function getErrorMessage(error) {
|
|
|
793
971
|
if (error instanceof Error) return error.message;
|
|
794
972
|
return "Unknown error";
|
|
795
973
|
}
|
|
974
|
+
var CRON_MONTH_ALIASES = {
|
|
975
|
+
jan: 1,
|
|
976
|
+
feb: 2,
|
|
977
|
+
mar: 3,
|
|
978
|
+
apr: 4,
|
|
979
|
+
may: 5,
|
|
980
|
+
jun: 6,
|
|
981
|
+
jul: 7,
|
|
982
|
+
aug: 8,
|
|
983
|
+
sep: 9,
|
|
984
|
+
oct: 10,
|
|
985
|
+
nov: 11,
|
|
986
|
+
dec: 12
|
|
987
|
+
};
|
|
988
|
+
var CRON_DAY_ALIASES = {
|
|
989
|
+
sun: 0,
|
|
990
|
+
mon: 1,
|
|
991
|
+
tue: 2,
|
|
992
|
+
wed: 3,
|
|
993
|
+
thu: 4,
|
|
994
|
+
fri: 5,
|
|
995
|
+
sat: 6
|
|
996
|
+
};
|
|
997
|
+
function parseCronValue(token, spec) {
|
|
998
|
+
const normalized = token.trim().toLowerCase();
|
|
999
|
+
if (normalized === "") {
|
|
1000
|
+
throw new Error("Cron token cannot be empty");
|
|
1001
|
+
}
|
|
1002
|
+
if (spec.aliases?.[normalized] !== void 0) {
|
|
1003
|
+
return spec.aliases[normalized];
|
|
1004
|
+
}
|
|
1005
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
1006
|
+
if (!Number.isFinite(parsed)) {
|
|
1007
|
+
throw new Error(`Invalid cron token: "${token}"`);
|
|
1008
|
+
}
|
|
1009
|
+
if (spec.aliases === CRON_DAY_ALIASES && parsed === 7) {
|
|
1010
|
+
return 0;
|
|
1011
|
+
}
|
|
1012
|
+
if (parsed < spec.min || parsed > spec.max) {
|
|
1013
|
+
throw new Error(
|
|
1014
|
+
`Cron value "${token}" is out of range ${spec.min}-${spec.max}`
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
return parsed;
|
|
1018
|
+
}
|
|
1019
|
+
function cronFieldIsWildcard(field) {
|
|
1020
|
+
return field.trim() === "*";
|
|
1021
|
+
}
|
|
1022
|
+
function matchesCronField(field, value, spec) {
|
|
1023
|
+
return field.split(",").map((part) => part.trim()).filter(Boolean).some((part) => {
|
|
1024
|
+
if (part === "*") return true;
|
|
1025
|
+
const [baseRaw, stepRaw] = part.split("/");
|
|
1026
|
+
const base = baseRaw?.trim() ?? "";
|
|
1027
|
+
const step = stepRaw ? Number.parseInt(stepRaw.trim(), 10) : 1;
|
|
1028
|
+
if (!Number.isFinite(step) || step <= 0) {
|
|
1029
|
+
throw new Error(`Invalid cron step: "${part}"`);
|
|
1030
|
+
}
|
|
1031
|
+
let start;
|
|
1032
|
+
let end;
|
|
1033
|
+
if (base === "*" || base === "") {
|
|
1034
|
+
start = spec.min;
|
|
1035
|
+
end = spec.max;
|
|
1036
|
+
} else if (base.includes("-")) {
|
|
1037
|
+
const [startRaw, endRaw] = base.split("-");
|
|
1038
|
+
start = parseCronValue(startRaw ?? "", spec);
|
|
1039
|
+
end = parseCronValue(endRaw ?? "", spec);
|
|
1040
|
+
if (end < start) {
|
|
1041
|
+
throw new Error(`Invalid cron range: "${part}"`);
|
|
1042
|
+
}
|
|
1043
|
+
} else {
|
|
1044
|
+
start = parseCronValue(base, spec);
|
|
1045
|
+
end = stepRaw ? spec.max : start;
|
|
1046
|
+
}
|
|
1047
|
+
if (value < start || value > end) return false;
|
|
1048
|
+
return (value - start) % step === 0;
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
function matchesCronSchedule(schedule, now) {
|
|
1052
|
+
const parts = schedule.trim().split(/\s+/);
|
|
1053
|
+
if (parts.length !== 5) {
|
|
1054
|
+
throw new Error(`Cron schedule must have 5 fields: "${schedule}"`);
|
|
1055
|
+
}
|
|
1056
|
+
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
|
1057
|
+
const minuteMatch = matchesCronField(minute, now.getUTCMinutes(), {
|
|
1058
|
+
min: 0,
|
|
1059
|
+
max: 59
|
|
1060
|
+
});
|
|
1061
|
+
const hourMatch = matchesCronField(hour, now.getUTCHours(), {
|
|
1062
|
+
min: 0,
|
|
1063
|
+
max: 23
|
|
1064
|
+
});
|
|
1065
|
+
const monthMatch = matchesCronField(month, now.getUTCMonth() + 1, {
|
|
1066
|
+
min: 1,
|
|
1067
|
+
max: 12,
|
|
1068
|
+
aliases: CRON_MONTH_ALIASES
|
|
1069
|
+
});
|
|
1070
|
+
const dayOfMonthMatch = matchesCronField(dayOfMonth, now.getUTCDate(), {
|
|
1071
|
+
min: 1,
|
|
1072
|
+
max: 31
|
|
1073
|
+
});
|
|
1074
|
+
const dayOfWeekMatch = matchesCronField(dayOfWeek, now.getUTCDay(), {
|
|
1075
|
+
min: 0,
|
|
1076
|
+
max: 6,
|
|
1077
|
+
aliases: CRON_DAY_ALIASES
|
|
1078
|
+
});
|
|
1079
|
+
const dayOfMonthWildcard = cronFieldIsWildcard(dayOfMonth);
|
|
1080
|
+
const dayOfWeekWildcard = cronFieldIsWildcard(dayOfWeek);
|
|
1081
|
+
const dayMatches = dayOfMonthWildcard && dayOfWeekWildcard ? true : dayOfMonthWildcard ? dayOfWeekMatch : dayOfWeekWildcard ? dayOfMonthMatch : dayOfMonthMatch || dayOfWeekMatch;
|
|
1082
|
+
return minuteMatch && hourMatch && monthMatch && dayMatches;
|
|
1083
|
+
}
|
|
1084
|
+
function startOfUtcMinute(date) {
|
|
1085
|
+
return new Date(
|
|
1086
|
+
Date.UTC(
|
|
1087
|
+
date.getUTCFullYear(),
|
|
1088
|
+
date.getUTCMonth(),
|
|
1089
|
+
date.getUTCDate(),
|
|
1090
|
+
date.getUTCHours(),
|
|
1091
|
+
date.getUTCMinutes(),
|
|
1092
|
+
0,
|
|
1093
|
+
0
|
|
1094
|
+
)
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
function coerceDate(value) {
|
|
1098
|
+
if (value instanceof Date && Number.isFinite(value.getTime())) {
|
|
1099
|
+
return value;
|
|
1100
|
+
}
|
|
1101
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
1102
|
+
const parsed = new Date(value);
|
|
1103
|
+
if (Number.isFinite(parsed.getTime())) return parsed;
|
|
1104
|
+
}
|
|
1105
|
+
return null;
|
|
1106
|
+
}
|
|
1107
|
+
function isCronRequestAuthorized(request) {
|
|
1108
|
+
const secret = process.env["CRON_SECRET"]?.trim();
|
|
1109
|
+
if (!secret) return true;
|
|
1110
|
+
return request.headers.get("authorization") === `Bearer ${secret}`;
|
|
1111
|
+
}
|
|
796
1112
|
function isWorkflowCancelledError(error) {
|
|
797
1113
|
if (!error || typeof error !== "object") return false;
|
|
798
1114
|
const record = error;
|
|
@@ -905,31 +1221,253 @@ function serializeEndpoints(endpoints) {
|
|
|
905
1221
|
}
|
|
906
1222
|
return result;
|
|
907
1223
|
}
|
|
1224
|
+
function isPlainObject(value) {
|
|
1225
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1226
|
+
}
|
|
1227
|
+
function serializeConnectField(connectField) {
|
|
1228
|
+
return Array.isArray(connectField) ? JSON.stringify(connectField) : connectField;
|
|
1229
|
+
}
|
|
1230
|
+
function deserializeConnectField(connectField) {
|
|
1231
|
+
if (Array.isArray(connectField)) {
|
|
1232
|
+
return connectField;
|
|
1233
|
+
}
|
|
1234
|
+
if (typeof connectField !== "string") {
|
|
1235
|
+
throw new Error("Resource connectField must be a string or string array");
|
|
1236
|
+
}
|
|
1237
|
+
const trimmed = connectField.trim();
|
|
1238
|
+
if (!trimmed.startsWith("[")) {
|
|
1239
|
+
return connectField;
|
|
1240
|
+
}
|
|
1241
|
+
try {
|
|
1242
|
+
const parsed = JSON.parse(trimmed);
|
|
1243
|
+
if (Array.isArray(parsed) && parsed.length > 0 && parsed.every((value) => typeof value === "string" && value.length > 0)) {
|
|
1244
|
+
return parsed;
|
|
1245
|
+
}
|
|
1246
|
+
} catch {
|
|
1247
|
+
}
|
|
1248
|
+
return connectField;
|
|
1249
|
+
}
|
|
1250
|
+
function validateConnectField(resourceName, connectField) {
|
|
1251
|
+
if (typeof connectField === "string") {
|
|
1252
|
+
if (!connectField.trim()) {
|
|
1253
|
+
throw new Error(
|
|
1254
|
+
`Resource "${resourceName}" must declare a non-empty connectField`
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
if (!Array.isArray(connectField) || connectField.length === 0) {
|
|
1260
|
+
throw new Error(
|
|
1261
|
+
`Resource "${resourceName}" must declare connectField as a string or non-empty ordered string array`
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
for (const field of connectField) {
|
|
1265
|
+
if (typeof field !== "string" || !field.trim()) {
|
|
1266
|
+
throw new Error(
|
|
1267
|
+
`Resource "${resourceName}" has an invalid composite connectField entry`
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
function validateResourcePlugs(resource, plugNames) {
|
|
1273
|
+
if (resource.mapping.plugs === void 0) {
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
if (!isPlainObject(resource.mapping.plugs)) {
|
|
1277
|
+
throw new Error(
|
|
1278
|
+
`Resource "${resource.name}" must declare mapping.plugs as an object keyed by plug name`
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
for (const [plugName, declaration] of Object.entries(resource.mapping.plugs)) {
|
|
1282
|
+
if (!plugNames.has(plugName)) {
|
|
1283
|
+
throw new Error(
|
|
1284
|
+
`Resource "${resource.name}" references unknown plug: "${plugName}"`
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
if (!isPlainObject(declaration)) {
|
|
1288
|
+
throw new Error(
|
|
1289
|
+
`Resource "${resource.name}" has an invalid plug declaration for "${plugName}"`
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
const keys = Object.keys(declaration);
|
|
1293
|
+
if (keys.length !== 1 || typeof declaration["uniqueIdentifier"] !== "string" || !declaration["uniqueIdentifier"].trim()) {
|
|
1294
|
+
throw new Error(
|
|
1295
|
+
`Resource "${resource.name}" must declare exactly one uniqueIdentifier for plug "${plugName}"`
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function normalizeCacheScope(cacheName, scope) {
|
|
1301
|
+
if (scope === void 0) {
|
|
1302
|
+
return void 0;
|
|
1303
|
+
}
|
|
1304
|
+
if (!isPlainObject(scope)) {
|
|
1305
|
+
throw new Error(`Cache "${cacheName}" must declare scope as an object`);
|
|
1306
|
+
}
|
|
1307
|
+
const normalized = {};
|
|
1308
|
+
for (const key of ["plug", "resource", "flow"]) {
|
|
1309
|
+
const value = scope[key];
|
|
1310
|
+
if (value === void 0) {
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1314
|
+
throw new Error(`Cache "${cacheName}" has an invalid scope.${key} value`);
|
|
1315
|
+
}
|
|
1316
|
+
normalized[key] = value.trim();
|
|
1317
|
+
}
|
|
1318
|
+
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
1319
|
+
}
|
|
1320
|
+
function parseCacheTtlSeconds(cacheName, ttl) {
|
|
1321
|
+
if (ttl === void 0) {
|
|
1322
|
+
return null;
|
|
1323
|
+
}
|
|
1324
|
+
if (typeof ttl === "number") {
|
|
1325
|
+
if (!Number.isFinite(ttl) || ttl <= 0) {
|
|
1326
|
+
throw new Error(`Cache "${cacheName}" must declare a positive ttl`);
|
|
1327
|
+
}
|
|
1328
|
+
return Math.ceil(ttl);
|
|
1329
|
+
}
|
|
1330
|
+
if (typeof ttl !== "string") {
|
|
1331
|
+
throw new Error(`Cache "${cacheName}" must declare ttl as a string or number`);
|
|
1332
|
+
}
|
|
1333
|
+
const normalized = ttl.trim().toLowerCase();
|
|
1334
|
+
const match = /^(\d+)\s*(ms|s|m|h|d)$/.exec(normalized);
|
|
1335
|
+
if (!match) {
|
|
1336
|
+
throw new Error(
|
|
1337
|
+
`Cache "${cacheName}" has an invalid ttl "${ttl}". Use values like "30s", "15m", or "6h"`
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
const amount = Number.parseInt(match[1], 10);
|
|
1341
|
+
const unit = match[2];
|
|
1342
|
+
const milliseconds = unit === "ms" ? amount : unit === "s" ? amount * 1e3 : unit === "m" ? amount * 6e4 : unit === "h" ? amount * 36e5 : amount * 864e5;
|
|
1343
|
+
return Math.max(1, Math.ceil(milliseconds / 1e3));
|
|
1344
|
+
}
|
|
1345
|
+
function validateCacheKey(key) {
|
|
1346
|
+
if (typeof key !== "string" || !key.trim()) {
|
|
1347
|
+
throw new Error("Cache key must be a non-empty string");
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
function coerceCacheEntryRecord(row) {
|
|
1351
|
+
if (typeof row["id"] !== "string" || typeof row["cacheId"] !== "string" || typeof row["key"] !== "string") {
|
|
1352
|
+
return null;
|
|
1353
|
+
}
|
|
1354
|
+
return {
|
|
1355
|
+
id: row["id"],
|
|
1356
|
+
cacheId: row["cacheId"],
|
|
1357
|
+
key: row["key"],
|
|
1358
|
+
value: row["value"],
|
|
1359
|
+
expiresAt: coerceDate(row["expiresAt"]),
|
|
1360
|
+
createdAt: coerceDate(row["createdAt"]) ?? void 0,
|
|
1361
|
+
updatedAt: coerceDate(row["updatedAt"]) ?? void 0
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
function isCacheEntryExpired(entry, now = /* @__PURE__ */ new Date()) {
|
|
1365
|
+
return entry.expiresAt !== null && entry.expiresAt.getTime() <= now.getTime();
|
|
1366
|
+
}
|
|
1367
|
+
function canonicalizeConnectValue(resource, connectValue) {
|
|
1368
|
+
const { connectField } = resource.mapping;
|
|
1369
|
+
if (Array.isArray(connectField)) {
|
|
1370
|
+
if (typeof connectValue === "string") {
|
|
1371
|
+
return connectValue;
|
|
1372
|
+
}
|
|
1373
|
+
if (!Array.isArray(connectValue)) {
|
|
1374
|
+
throw new Error(
|
|
1375
|
+
`Resource "${resource.name}" expects composite connectValue input matching connectField order`
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
if (connectValue.length !== connectField.length) {
|
|
1379
|
+
throw new Error(
|
|
1380
|
+
`Resource "${resource.name}" expects ${String(connectField.length)} connectValue parts in declared order`
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
const parts = connectValue.map((part) => {
|
|
1384
|
+
if (typeof part === "string") return part;
|
|
1385
|
+
if (typeof part === "number" || typeof part === "boolean") {
|
|
1386
|
+
return String(part);
|
|
1387
|
+
}
|
|
1388
|
+
throw new Error(
|
|
1389
|
+
`Resource "${resource.name}" connectValue parts must be strings, numbers, or booleans`
|
|
1390
|
+
);
|
|
1391
|
+
});
|
|
1392
|
+
return JSON.stringify(parts);
|
|
1393
|
+
}
|
|
1394
|
+
if (typeof connectValue === "string") {
|
|
1395
|
+
return connectValue;
|
|
1396
|
+
}
|
|
1397
|
+
if (typeof connectValue === "number" || typeof connectValue === "boolean") {
|
|
1398
|
+
return String(connectValue);
|
|
1399
|
+
}
|
|
1400
|
+
throw new Error(
|
|
1401
|
+
`Resource "${resource.name}" expects connectValue to be a string, number, or boolean`
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
908
1404
|
function khotan(config) {
|
|
909
|
-
const { adapter, plugs, resources = [] } = config;
|
|
1405
|
+
const { adapter, plugs, resources = [], caches = [] } = config;
|
|
1406
|
+
const instanceId = crypto.randomUUID();
|
|
1407
|
+
const plugNames = /* @__PURE__ */ new Set();
|
|
1408
|
+
for (const plug of plugs) {
|
|
1409
|
+
if (plugNames.has(plug.name)) {
|
|
1410
|
+
throw new Error(`Duplicate plug name: "${plug.name}"`);
|
|
1411
|
+
}
|
|
1412
|
+
plugNames.add(plug.name);
|
|
1413
|
+
}
|
|
910
1414
|
const resourceNames = /* @__PURE__ */ new Set();
|
|
1415
|
+
const resourceConfigByName = /* @__PURE__ */ new Map();
|
|
911
1416
|
for (const resource of resources) {
|
|
912
1417
|
if (resourceNames.has(resource.name)) {
|
|
913
1418
|
throw new Error(`Duplicate resource name: "${resource.name}"`);
|
|
914
1419
|
}
|
|
1420
|
+
validateConnectField(resource.name, resource.mapping.connectField);
|
|
1421
|
+
validateResourcePlugs(resource, plugNames);
|
|
915
1422
|
resourceNames.add(resource.name);
|
|
1423
|
+
resourceConfigByName.set(resource.name, resource);
|
|
916
1424
|
}
|
|
917
|
-
const
|
|
1425
|
+
const registeredFlowNames = /* @__PURE__ */ new Set();
|
|
918
1426
|
for (const plug of plugs) {
|
|
919
|
-
if (
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
throw new Error(
|
|
927
|
-
`Flow "${flow2.name}" references unknown resource: "${flow2.resource}"`
|
|
928
|
-
);
|
|
929
|
-
}
|
|
1427
|
+
if (!plug.flows) continue;
|
|
1428
|
+
for (const flow2 of plug.flows) {
|
|
1429
|
+
registeredFlowNames.add(flow2.name);
|
|
1430
|
+
if (flow2.resource && !resourceNames.has(flow2.resource)) {
|
|
1431
|
+
throw new Error(
|
|
1432
|
+
`Flow "${flow2.name}" references unknown resource: "${flow2.resource}"`
|
|
1433
|
+
);
|
|
930
1434
|
}
|
|
931
1435
|
}
|
|
932
1436
|
}
|
|
1437
|
+
const cacheStateByName = /* @__PURE__ */ new Map();
|
|
1438
|
+
for (const cache of caches) {
|
|
1439
|
+
if (cacheStateByName.has(cache.name)) {
|
|
1440
|
+
throw new Error(`Duplicate cache name: "${cache.name}"`);
|
|
1441
|
+
}
|
|
1442
|
+
if (typeof cache.name !== "string" || !cache.name.trim()) {
|
|
1443
|
+
throw new Error("Cache registrations must declare a non-empty name");
|
|
1444
|
+
}
|
|
1445
|
+
const normalizedScope = normalizeCacheScope(cache.name, cache.scope);
|
|
1446
|
+
if (normalizedScope?.plug && !plugNames.has(normalizedScope.plug)) {
|
|
1447
|
+
throw new Error(
|
|
1448
|
+
`Cache "${cache.name}" references unknown plug: "${normalizedScope.plug}"`
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
if (normalizedScope?.resource && !resourceNames.has(normalizedScope.resource)) {
|
|
1452
|
+
throw new Error(
|
|
1453
|
+
`Cache "${cache.name}" references unknown resource: "${normalizedScope.resource}"`
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
if (normalizedScope?.flow && !registeredFlowNames.has(normalizedScope.flow)) {
|
|
1457
|
+
throw new Error(
|
|
1458
|
+
`Cache "${cache.name}" references unknown flow: "${normalizedScope.flow}"`
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
cacheStateByName.set(cache.name, {
|
|
1462
|
+
id: "",
|
|
1463
|
+
config: {
|
|
1464
|
+
...cache,
|
|
1465
|
+
name: cache.name.trim(),
|
|
1466
|
+
...normalizedScope ? { scope: normalizedScope } : {}
|
|
1467
|
+
},
|
|
1468
|
+
ttlSeconds: parseCacheTtlSeconds(cache.name, cache.ttl)
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
933
1471
|
const registeredFlowKeys = new Set(
|
|
934
1472
|
plugs.flatMap(
|
|
935
1473
|
(plug) => (plug.flows ?? []).map(
|
|
@@ -971,16 +1509,31 @@ function khotan(config) {
|
|
|
971
1509
|
}
|
|
972
1510
|
let initialized = false;
|
|
973
1511
|
let initPromise = null;
|
|
1512
|
+
const resourceIdByName = /* @__PURE__ */ new Map();
|
|
1513
|
+
const resourceConfigById = /* @__PURE__ */ new Map();
|
|
974
1514
|
async function doInit() {
|
|
975
1515
|
if (initialized) return;
|
|
976
|
-
|
|
1516
|
+
resourceIdByName.clear();
|
|
1517
|
+
resourceConfigById.clear();
|
|
977
1518
|
for (const resource of resources) {
|
|
978
1519
|
const { id } = await adapter.upsertResource({
|
|
979
1520
|
name: resource.name,
|
|
980
|
-
connectField: resource.connectField,
|
|
1521
|
+
connectField: resource.mapping.connectField,
|
|
981
1522
|
description: resource.description ?? null
|
|
982
1523
|
});
|
|
983
|
-
|
|
1524
|
+
resourceIdByName.set(resource.name, id);
|
|
1525
|
+
resourceConfigById.set(id, resource);
|
|
1526
|
+
}
|
|
1527
|
+
for (const [cacheName, cacheState] of cacheStateByName) {
|
|
1528
|
+
const { id } = await adapter.upsertCache({
|
|
1529
|
+
name: cacheName,
|
|
1530
|
+
scope: cacheState.config.scope ?? null,
|
|
1531
|
+
ttlSeconds: cacheState.ttlSeconds
|
|
1532
|
+
});
|
|
1533
|
+
cacheStateByName.set(cacheName, {
|
|
1534
|
+
...cacheState,
|
|
1535
|
+
id
|
|
1536
|
+
});
|
|
984
1537
|
}
|
|
985
1538
|
for (const plug of plugs) {
|
|
986
1539
|
const { id: plugId } = await adapter.upsertPlug({
|
|
@@ -998,7 +1551,7 @@ function khotan(config) {
|
|
|
998
1551
|
schedule: flow2.schedule ?? null
|
|
999
1552
|
});
|
|
1000
1553
|
if (flow2.resource) {
|
|
1001
|
-
const resourceId =
|
|
1554
|
+
const resourceId = resourceIdByName.get(flow2.resource);
|
|
1002
1555
|
await adapter.updateFlowResourceId(flowId, resourceId);
|
|
1003
1556
|
}
|
|
1004
1557
|
}
|
|
@@ -1033,6 +1586,189 @@ function khotan(config) {
|
|
|
1033
1586
|
initPromise ??= doInit();
|
|
1034
1587
|
return initPromise;
|
|
1035
1588
|
}
|
|
1589
|
+
async function getRegisteredResourceById(resourceId) {
|
|
1590
|
+
await init();
|
|
1591
|
+
return resourceConfigById.get(resourceId) ?? null;
|
|
1592
|
+
}
|
|
1593
|
+
async function resolveCacheState(cacheName) {
|
|
1594
|
+
await init();
|
|
1595
|
+
const cacheState = cacheStateByName.get(cacheName);
|
|
1596
|
+
if (!cacheState || !cacheState.id) {
|
|
1597
|
+
throw new Error(`Cache "${cacheName}" is not registered`);
|
|
1598
|
+
}
|
|
1599
|
+
return cacheState;
|
|
1600
|
+
}
|
|
1601
|
+
function createCacheInstance(cacheName) {
|
|
1602
|
+
return {
|
|
1603
|
+
async get(key) {
|
|
1604
|
+
const entry = await readCacheEntry(cacheName, key);
|
|
1605
|
+
return entry ? entry.value : null;
|
|
1606
|
+
},
|
|
1607
|
+
async set(key, value) {
|
|
1608
|
+
validateCacheKey(key);
|
|
1609
|
+
const cacheState = await resolveCacheState(cacheName);
|
|
1610
|
+
const expiresAt = cacheState.ttlSeconds !== null ? new Date(Date.now() + cacheState.ttlSeconds * 1e3) : null;
|
|
1611
|
+
await adapter.upsertCacheEntry({
|
|
1612
|
+
cacheId: cacheState.id,
|
|
1613
|
+
key,
|
|
1614
|
+
value,
|
|
1615
|
+
expiresAt
|
|
1616
|
+
});
|
|
1617
|
+
return value;
|
|
1618
|
+
},
|
|
1619
|
+
async delete(key) {
|
|
1620
|
+
validateCacheKey(key);
|
|
1621
|
+
const cacheState = await resolveCacheState(cacheName);
|
|
1622
|
+
await adapter.deleteCacheEntry(cacheState.id, key);
|
|
1623
|
+
}
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
async function readCacheEntry(cacheName, key) {
|
|
1627
|
+
validateCacheKey(key);
|
|
1628
|
+
const cacheState = await resolveCacheState(cacheName);
|
|
1629
|
+
const row = await adapter.getCacheEntry(cacheState.id, key);
|
|
1630
|
+
if (!row) {
|
|
1631
|
+
return null;
|
|
1632
|
+
}
|
|
1633
|
+
const entry = coerceCacheEntryRecord(row);
|
|
1634
|
+
if (!entry || isCacheEntryExpired(entry)) {
|
|
1635
|
+
return null;
|
|
1636
|
+
}
|
|
1637
|
+
return entry;
|
|
1638
|
+
}
|
|
1639
|
+
function decorateResourceRecord(resource) {
|
|
1640
|
+
const { connectField: storedConnectField, ...rest } = resource;
|
|
1641
|
+
const configResource = typeof resource["name"] === "string" ? resourceConfigByName.get(resource["name"]) : void 0;
|
|
1642
|
+
return {
|
|
1643
|
+
...rest,
|
|
1644
|
+
mapping: {
|
|
1645
|
+
connectField: configResource?.mapping.connectField ?? deserializeConnectField(storedConnectField),
|
|
1646
|
+
...configResource?.mapping.plugs ? { plugs: configResource.mapping.plugs } : {}
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
function buildMappingPage(params) {
|
|
1651
|
+
return {
|
|
1652
|
+
items: params.items,
|
|
1653
|
+
page: {
|
|
1654
|
+
limit: params.limit,
|
|
1655
|
+
offset: params.offset,
|
|
1656
|
+
hasMore: params.hasMore,
|
|
1657
|
+
prevOffset: Math.max(params.offset - params.limit, 0),
|
|
1658
|
+
nextOffset: params.offset + params.limit,
|
|
1659
|
+
total: params.total
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
async function validateMappingPayload(params) {
|
|
1664
|
+
if (!isPlainObject(params.refs)) {
|
|
1665
|
+
throw new Error("Mapping refs must be an object keyed by plug name");
|
|
1666
|
+
}
|
|
1667
|
+
for (const [plugName, ref] of Object.entries(params.refs)) {
|
|
1668
|
+
if (typeof ref !== "string") {
|
|
1669
|
+
throw new Error(`Mapping ref "${plugName}" must be a string`);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
if (params.metadata !== void 0 && params.metadata !== null) {
|
|
1673
|
+
if (!isPlainObject(params.metadata)) {
|
|
1674
|
+
throw new Error("Mapping metadata must be an object when provided");
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
const resource = await getRegisteredResourceById(params.resourceId);
|
|
1678
|
+
if (!resource) {
|
|
1679
|
+
throw new Error(`Resource "${params.resourceId}" is not registered`);
|
|
1680
|
+
}
|
|
1681
|
+
if (resource.mapping.plugs) {
|
|
1682
|
+
const invalidPlugs = Object.keys(params.refs).filter(
|
|
1683
|
+
(plugName) => !resource.mapping.plugs?.[plugName]
|
|
1684
|
+
);
|
|
1685
|
+
if (invalidPlugs.length > 0) {
|
|
1686
|
+
throw new Error(
|
|
1687
|
+
`Resource "${resource.name}" only allows refs for declared plugs. Invalid refs: ${invalidPlugs.join(", ")}`
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
return resource;
|
|
1692
|
+
}
|
|
1693
|
+
async function listMappings(params) {
|
|
1694
|
+
const resource = await getRegisteredResourceById(params.resourceId);
|
|
1695
|
+
if (!resource) {
|
|
1696
|
+
throw new Error(`Resource "${params.resourceId}" is not registered`);
|
|
1697
|
+
}
|
|
1698
|
+
const limit = Math.min(Math.max(params.limit ?? 20, 1), 100);
|
|
1699
|
+
const offset = Math.max(params.offset ?? 0, 0);
|
|
1700
|
+
const page = await adapter.listMappings({
|
|
1701
|
+
resourceId: params.resourceId,
|
|
1702
|
+
limit,
|
|
1703
|
+
offset,
|
|
1704
|
+
...params.search?.trim() ? { search: params.search.trim() } : {}
|
|
1705
|
+
});
|
|
1706
|
+
return buildMappingPage({
|
|
1707
|
+
limit,
|
|
1708
|
+
offset,
|
|
1709
|
+
hasMore: page.hasMore,
|
|
1710
|
+
total: page.total,
|
|
1711
|
+
items: page.items
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
async function lookupMapping(params) {
|
|
1715
|
+
const resource = await getRegisteredResourceById(params.resourceId);
|
|
1716
|
+
if (!resource) {
|
|
1717
|
+
throw new Error(`Resource "${params.resourceId}" is not registered`);
|
|
1718
|
+
}
|
|
1719
|
+
if ("connectValue" in params) {
|
|
1720
|
+
return adapter.lookupMapping({
|
|
1721
|
+
resourceId: params.resourceId,
|
|
1722
|
+
connectValue: canonicalizeConnectValue(resource, params.connectValue)
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
if (resource.mapping.plugs && !resource.mapping.plugs[params.plugName]) {
|
|
1726
|
+
throw new Error(
|
|
1727
|
+
`Resource "${resource.name}" does not declare plug "${params.plugName}"`
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
return adapter.lookupMapping(params);
|
|
1731
|
+
}
|
|
1732
|
+
async function upsertMapping(mapping) {
|
|
1733
|
+
const resource = await validateMappingPayload(mapping);
|
|
1734
|
+
const result = await adapter.upsertMapping({
|
|
1735
|
+
resourceId: mapping.resourceId,
|
|
1736
|
+
connectValue: canonicalizeConnectValue(resource, mapping.connectValue),
|
|
1737
|
+
refs: mapping.refs,
|
|
1738
|
+
metadata: mapping.metadata ?? null
|
|
1739
|
+
});
|
|
1740
|
+
const saved = await adapter.getMapping(result.id);
|
|
1741
|
+
if (!saved) {
|
|
1742
|
+
throw new Error("Mapping was saved but could not be reloaded");
|
|
1743
|
+
}
|
|
1744
|
+
return saved;
|
|
1745
|
+
}
|
|
1746
|
+
async function updateMapping(id, mapping) {
|
|
1747
|
+
const existing = await adapter.getMapping(id);
|
|
1748
|
+
if (!existing) {
|
|
1749
|
+
throw new Error(`Mapping "${id}" not found`);
|
|
1750
|
+
}
|
|
1751
|
+
const resource = await validateMappingPayload(mapping);
|
|
1752
|
+
await adapter.upsertMapping({
|
|
1753
|
+
id,
|
|
1754
|
+
resourceId: mapping.resourceId,
|
|
1755
|
+
connectValue: canonicalizeConnectValue(resource, mapping.connectValue),
|
|
1756
|
+
refs: mapping.refs,
|
|
1757
|
+
metadata: mapping.metadata ?? null
|
|
1758
|
+
});
|
|
1759
|
+
const saved = await adapter.getMapping(id);
|
|
1760
|
+
if (!saved) {
|
|
1761
|
+
throw new Error(`Mapping "${id}" disappeared after update`);
|
|
1762
|
+
}
|
|
1763
|
+
return saved;
|
|
1764
|
+
}
|
|
1765
|
+
async function deleteMapping(id) {
|
|
1766
|
+
const existing = await adapter.getMapping(id);
|
|
1767
|
+
if (!existing) {
|
|
1768
|
+
throw new Error(`Mapping "${id}" not found`);
|
|
1769
|
+
}
|
|
1770
|
+
await adapter.deleteMapping(id);
|
|
1771
|
+
}
|
|
1036
1772
|
function wire(plugName) {
|
|
1037
1773
|
const plugReg = plugs.find((p) => p.name === plugName);
|
|
1038
1774
|
if (!plugReg) {
|
|
@@ -1043,29 +1779,7 @@ function khotan(config) {
|
|
|
1043
1779
|
}
|
|
1044
1780
|
const wireConfig = plugReg.wires[0];
|
|
1045
1781
|
function createBoundPlug(vars, _setVars) {
|
|
1046
|
-
|
|
1047
|
-
const opts = (extra) => ({
|
|
1048
|
-
...extra,
|
|
1049
|
-
vars,
|
|
1050
|
-
..._setVars && { _setVars }
|
|
1051
|
-
});
|
|
1052
|
-
return {
|
|
1053
|
-
get(path, extra) {
|
|
1054
|
-
return plug.get(path, opts(extra));
|
|
1055
|
-
},
|
|
1056
|
-
post(path, extra) {
|
|
1057
|
-
return plug.post(path, opts(extra));
|
|
1058
|
-
},
|
|
1059
|
-
put(path, extra) {
|
|
1060
|
-
return plug.put(path, opts(extra));
|
|
1061
|
-
},
|
|
1062
|
-
patch(path, extra) {
|
|
1063
|
-
return plug.patch(path, opts(extra));
|
|
1064
|
-
},
|
|
1065
|
-
delete(path, extra) {
|
|
1066
|
-
return plug.delete(path, opts(extra));
|
|
1067
|
-
}
|
|
1068
|
-
};
|
|
1782
|
+
return bindPlugWithVars(plugReg.plug, vars, _setVars);
|
|
1069
1783
|
}
|
|
1070
1784
|
async function getWireVars(wireId) {
|
|
1071
1785
|
const raw = await adapter.getWireMetadata(wireId);
|
|
@@ -1255,31 +1969,21 @@ function khotan(config) {
|
|
|
1255
1969
|
const setFlowVars = async (updates) => {
|
|
1256
1970
|
await setVars(plugName, { ...vars, ...updates });
|
|
1257
1971
|
};
|
|
1258
|
-
const
|
|
1259
|
-
|
|
1972
|
+
const boundPlug = bindPlugWithVars(
|
|
1973
|
+
plugReg.plug,
|
|
1260
1974
|
vars,
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1265
|
-
return plugReg.plug.get(path, opts(extra));
|
|
1266
|
-
},
|
|
1267
|
-
post(path, extra) {
|
|
1268
|
-
return plugReg.plug.post(path, opts(extra));
|
|
1269
|
-
},
|
|
1270
|
-
put(path, extra) {
|
|
1271
|
-
return plugReg.plug.put(path, opts(extra));
|
|
1272
|
-
},
|
|
1273
|
-
patch(path, extra) {
|
|
1274
|
-
return plugReg.plug.patch(path, opts(extra));
|
|
1275
|
-
},
|
|
1276
|
-
delete(path, extra) {
|
|
1277
|
-
return plugReg.plug.delete(path, opts(extra));
|
|
1278
|
-
}
|
|
1975
|
+
secret ? setFlowVars : void 0
|
|
1976
|
+
);
|
|
1977
|
+
const plugVarsByName = {
|
|
1978
|
+
[plugName]: vars
|
|
1279
1979
|
};
|
|
1980
|
+
if (flowReg.to && plugNames.has(flowReg.to)) {
|
|
1981
|
+
plugVarsByName[flowReg.to] = secret ? await getVars(flowReg.to).catch(() => ({})) : {};
|
|
1982
|
+
}
|
|
1280
1983
|
const flowContext = {
|
|
1281
1984
|
id: flowId,
|
|
1282
1985
|
name: flowReg.name,
|
|
1986
|
+
plugName,
|
|
1283
1987
|
type: flowReg.type,
|
|
1284
1988
|
resource: flowReg.resource ?? null,
|
|
1285
1989
|
to: flowReg.to ?? null
|
|
@@ -1292,7 +1996,9 @@ function khotan(config) {
|
|
|
1292
1996
|
runType,
|
|
1293
1997
|
body: requestBody["body"],
|
|
1294
1998
|
vars,
|
|
1295
|
-
|
|
1999
|
+
plugVarsByName,
|
|
2000
|
+
khotanRunId: runId,
|
|
2001
|
+
khotanInstanceId: instanceId
|
|
1296
2002
|
}
|
|
1297
2003
|
]);
|
|
1298
2004
|
const workflowRunId = getWorkflowRunId(result2);
|
|
@@ -1317,7 +2023,8 @@ function khotan(config) {
|
|
|
1317
2023
|
runType,
|
|
1318
2024
|
body: requestBody["body"],
|
|
1319
2025
|
vars,
|
|
1320
|
-
setVars: setFlowVars
|
|
2026
|
+
setVars: setFlowVars,
|
|
2027
|
+
cache: createCacheInstance
|
|
1321
2028
|
});
|
|
1322
2029
|
const runResult = toFlowRunResult(result);
|
|
1323
2030
|
const { counters, status } = await completeRunOk(runResult);
|
|
@@ -1338,6 +2045,112 @@ function khotan(config) {
|
|
|
1338
2045
|
);
|
|
1339
2046
|
}
|
|
1340
2047
|
}
|
|
2048
|
+
async function wasFlowTriggeredInMinuteWindow(flowId, slotStart, slotEnd) {
|
|
2049
|
+
const runs = await adapter.listRuns(flowId);
|
|
2050
|
+
return runs.some((run) => {
|
|
2051
|
+
const startedAt = coerceDate(run["startedAt"]);
|
|
2052
|
+
if (!startedAt) return false;
|
|
2053
|
+
const startedAtMs = startedAt.getTime();
|
|
2054
|
+
return startedAtMs >= slotStart.getTime() && startedAtMs < slotEnd.getTime();
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
async function dispatchScheduledFlows(options = {}) {
|
|
2058
|
+
await init();
|
|
2059
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
2060
|
+
const slotStart = startOfUtcMinute(now);
|
|
2061
|
+
const slotEnd = new Date(slotStart.getTime() + 6e4);
|
|
2062
|
+
const runType = options.runType ?? "full";
|
|
2063
|
+
const registeredFlows = (await adapter.listFlows()).filter(
|
|
2064
|
+
(flow2) => isRegisteredFlowRecord(flow2)
|
|
2065
|
+
);
|
|
2066
|
+
const scheduledFlows = registeredFlows.filter(
|
|
2067
|
+
(flow2) => typeof flow2["schedule"] === "string" && flow2["schedule"].trim()
|
|
2068
|
+
);
|
|
2069
|
+
const triggered = [];
|
|
2070
|
+
const skipped = [];
|
|
2071
|
+
for (const flow2 of scheduledFlows) {
|
|
2072
|
+
const flowId = typeof flow2["id"] === "string" ? flow2["id"] : null;
|
|
2073
|
+
const flowName = typeof flow2["name"] === "string" ? flow2["name"] : null;
|
|
2074
|
+
const plugName = typeof flow2["plugName"] === "string" ? flow2["plugName"] : null;
|
|
2075
|
+
const schedule = typeof flow2["schedule"] === "string" ? flow2["schedule"].trim() : "";
|
|
2076
|
+
if (!flowId || !flowName || !plugName || !schedule) continue;
|
|
2077
|
+
if (flow2["enabled"] === false) {
|
|
2078
|
+
skipped.push({
|
|
2079
|
+
flowId,
|
|
2080
|
+
flowName,
|
|
2081
|
+
plugName,
|
|
2082
|
+
schedule,
|
|
2083
|
+
reason: "disabled"
|
|
2084
|
+
});
|
|
2085
|
+
continue;
|
|
2086
|
+
}
|
|
2087
|
+
let isDue = false;
|
|
2088
|
+
try {
|
|
2089
|
+
isDue = matchesCronSchedule(schedule, now);
|
|
2090
|
+
} catch (error) {
|
|
2091
|
+
skipped.push({
|
|
2092
|
+
flowId,
|
|
2093
|
+
flowName,
|
|
2094
|
+
plugName,
|
|
2095
|
+
schedule,
|
|
2096
|
+
reason: "invalid_schedule",
|
|
2097
|
+
detail: getErrorMessage(error)
|
|
2098
|
+
});
|
|
2099
|
+
continue;
|
|
2100
|
+
}
|
|
2101
|
+
if (!isDue) {
|
|
2102
|
+
skipped.push({
|
|
2103
|
+
flowId,
|
|
2104
|
+
flowName,
|
|
2105
|
+
plugName,
|
|
2106
|
+
schedule,
|
|
2107
|
+
reason: "not_due"
|
|
2108
|
+
});
|
|
2109
|
+
continue;
|
|
2110
|
+
}
|
|
2111
|
+
if (await wasFlowTriggeredInMinuteWindow(flowId, slotStart, slotEnd)) {
|
|
2112
|
+
skipped.push({
|
|
2113
|
+
flowId,
|
|
2114
|
+
flowName,
|
|
2115
|
+
plugName,
|
|
2116
|
+
schedule,
|
|
2117
|
+
reason: "already_triggered"
|
|
2118
|
+
});
|
|
2119
|
+
continue;
|
|
2120
|
+
}
|
|
2121
|
+
const response = await triggerFlowRun(flowId, { runType });
|
|
2122
|
+
const payload = await response.json().catch(() => ({}));
|
|
2123
|
+
if (!response.ok) {
|
|
2124
|
+
skipped.push({
|
|
2125
|
+
flowId,
|
|
2126
|
+
flowName,
|
|
2127
|
+
plugName,
|
|
2128
|
+
schedule,
|
|
2129
|
+
reason: "trigger_failed",
|
|
2130
|
+
status: response.status,
|
|
2131
|
+
detail: typeof payload["error"] === "string" ? payload["error"] : response.statusText
|
|
2132
|
+
});
|
|
2133
|
+
continue;
|
|
2134
|
+
}
|
|
2135
|
+
triggered.push({
|
|
2136
|
+
flowId,
|
|
2137
|
+
flowName,
|
|
2138
|
+
plugName,
|
|
2139
|
+
schedule,
|
|
2140
|
+
runId: payload["id"] ?? null,
|
|
2141
|
+
workflowRunId: payload["workflowRunId"] ?? null,
|
|
2142
|
+
status: typeof payload["status"] === "string" ? payload["status"] : "running"
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
return {
|
|
2146
|
+
ok: true,
|
|
2147
|
+
tickAt: slotStart.toISOString(),
|
|
2148
|
+
runType,
|
|
2149
|
+
evaluated: scheduledFlows.length,
|
|
2150
|
+
triggered,
|
|
2151
|
+
skipped
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
1341
2154
|
async function resolveFlowId(flowNameOrId, options = {}) {
|
|
1342
2155
|
await init();
|
|
1343
2156
|
const byId = await adapter.getFlow(flowNameOrId);
|
|
@@ -1409,12 +2222,14 @@ function khotan(config) {
|
|
|
1409
2222
|
const plugsIdx = segments.indexOf("plugs");
|
|
1410
2223
|
const flowsIdx = segments.indexOf("flows");
|
|
1411
2224
|
const resourcesIdx = segments.indexOf("resources");
|
|
2225
|
+
const cachesIdx = segments.indexOf("caches");
|
|
1412
2226
|
const mappingsIdx = segments.indexOf("mappings");
|
|
1413
2227
|
const runsIdx = segments.indexOf("runs");
|
|
1414
2228
|
const wiresIdx = segments.indexOf("wires");
|
|
1415
2229
|
const webhookHandlersIdx = segments.indexOf("webhook-handlers");
|
|
1416
2230
|
const webhookEventsIdx = segments.indexOf("webhook-events");
|
|
1417
2231
|
const variablesIdx = segments.indexOf("variables");
|
|
2232
|
+
const cronIdx = segments.indexOf("cron");
|
|
1418
2233
|
const debugIdx = segments.indexOf("debug");
|
|
1419
2234
|
const limit = Math.min(
|
|
1420
2235
|
Math.max(
|
|
@@ -1427,7 +2242,35 @@ function khotan(config) {
|
|
|
1427
2242
|
Number.parseInt(url.searchParams.get("offset") ?? "0", 10) || 0,
|
|
1428
2243
|
0
|
|
1429
2244
|
);
|
|
2245
|
+
const search = url.searchParams.get("search")?.trim() || void 0;
|
|
2246
|
+
const wantsMappingPage = url.searchParams.has("limit") || url.searchParams.has("offset") || url.searchParams.has("search");
|
|
1430
2247
|
if (request.method === "GET") {
|
|
2248
|
+
if (cachesIdx !== -1 && cachesIdx === segments.length - 3) {
|
|
2249
|
+
const cacheName = decodeURIComponent(segments[cachesIdx + 1]);
|
|
2250
|
+
const key = decodeURIComponent(segments[cachesIdx + 2]);
|
|
2251
|
+
try {
|
|
2252
|
+
const entry = await readCacheEntry(cacheName, key);
|
|
2253
|
+
if (!entry) {
|
|
2254
|
+
return Response.json({ error: "Cache entry not found" }, { status: 404 });
|
|
2255
|
+
}
|
|
2256
|
+
return Response.json({
|
|
2257
|
+
cache: cacheName,
|
|
2258
|
+
key: entry.key,
|
|
2259
|
+
value: entry.value,
|
|
2260
|
+
expiresAt: entry.expiresAt
|
|
2261
|
+
});
|
|
2262
|
+
} catch (error) {
|
|
2263
|
+
const message = error instanceof Error ? error.message : "Invalid cache request";
|
|
2264
|
+
return Response.json({ error: message }, { status: 400 });
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
if (cronIdx !== -1 && cronIdx === segments.length - 1) {
|
|
2268
|
+
if (!isCronRequestAuthorized(request)) {
|
|
2269
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2270
|
+
}
|
|
2271
|
+
const result = await dispatchScheduledFlows();
|
|
2272
|
+
return Response.json(result);
|
|
2273
|
+
}
|
|
1431
2274
|
if (debugIdx !== -1 && debugIdx === segments.length - 1) {
|
|
1432
2275
|
const debugActive = process?.env?.["KHOTAN_DEBUG"];
|
|
1433
2276
|
if (!debugActive) {
|
|
@@ -1659,12 +2502,27 @@ function khotan(config) {
|
|
|
1659
2502
|
const filtered = data.filter(
|
|
1660
2503
|
(r) => typeof r["name"] === "string" && resourceNames.has(r["name"])
|
|
1661
2504
|
);
|
|
1662
|
-
return Response.json(filtered);
|
|
2505
|
+
return Response.json(filtered.map(decorateResourceRecord));
|
|
1663
2506
|
}
|
|
1664
2507
|
if (resourcesIdx !== -1 && resourcesIdx === segments.length - 3 && segments[resourcesIdx + 2] === "mappings") {
|
|
1665
2508
|
const resourceId = segments[resourcesIdx + 1];
|
|
1666
|
-
const
|
|
1667
|
-
|
|
2509
|
+
const resource = await getRegisteredResourceById(resourceId);
|
|
2510
|
+
if (!resource) {
|
|
2511
|
+
return Response.json(
|
|
2512
|
+
{ error: "Resource not found" },
|
|
2513
|
+
{ status: 404 }
|
|
2514
|
+
);
|
|
2515
|
+
}
|
|
2516
|
+
const page = await listMappings({
|
|
2517
|
+
resourceId,
|
|
2518
|
+
limit,
|
|
2519
|
+
offset,
|
|
2520
|
+
...search ? { search } : {}
|
|
2521
|
+
});
|
|
2522
|
+
if (!wantsMappingPage) {
|
|
2523
|
+
return Response.json(page.items);
|
|
2524
|
+
}
|
|
2525
|
+
return Response.json(page);
|
|
1668
2526
|
}
|
|
1669
2527
|
if (resourcesIdx !== -1 && resourcesIdx === segments.length - 2) {
|
|
1670
2528
|
const resourceId = segments[resourcesIdx + 1];
|
|
@@ -1676,7 +2534,7 @@ function khotan(config) {
|
|
|
1676
2534
|
);
|
|
1677
2535
|
}
|
|
1678
2536
|
const flows = await adapter.getResourceFlows(resourceId);
|
|
1679
|
-
return Response.json({ ...resource, flows });
|
|
2537
|
+
return Response.json({ ...decorateResourceRecord(resource), flows });
|
|
1680
2538
|
}
|
|
1681
2539
|
if (mappingsIdx !== -1 && mappingsIdx === segments.length - 2) {
|
|
1682
2540
|
const mappingId = segments[mappingsIdx + 1];
|
|
@@ -1688,6 +2546,42 @@ function khotan(config) {
|
|
|
1688
2546
|
}
|
|
1689
2547
|
}
|
|
1690
2548
|
if (request.method === "POST") {
|
|
2549
|
+
if (cachesIdx !== -1 && cachesIdx === segments.length - 3) {
|
|
2550
|
+
const cacheName = decodeURIComponent(segments[cachesIdx + 1]);
|
|
2551
|
+
const key = decodeURIComponent(segments[cachesIdx + 2]);
|
|
2552
|
+
const body = await request.json().catch(() => ({}));
|
|
2553
|
+
if (!("value" in body)) {
|
|
2554
|
+
return Response.json({ error: "Cache writes require a value" }, { status: 400 });
|
|
2555
|
+
}
|
|
2556
|
+
try {
|
|
2557
|
+
const cacheHandle = createCacheInstance(cacheName);
|
|
2558
|
+
await cacheHandle.set(key, body.value);
|
|
2559
|
+
const entry = await readCacheEntry(cacheName, key);
|
|
2560
|
+
return Response.json({
|
|
2561
|
+
cache: cacheName,
|
|
2562
|
+
key,
|
|
2563
|
+
value: body.value,
|
|
2564
|
+
expiresAt: entry?.expiresAt ?? null
|
|
2565
|
+
});
|
|
2566
|
+
} catch (error) {
|
|
2567
|
+
const message = error instanceof Error ? error.message : "Invalid cache payload";
|
|
2568
|
+
return Response.json({ error: message }, { status: 400 });
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
if (cronIdx !== -1 && cronIdx === segments.length - 1) {
|
|
2572
|
+
if (!isCronRequestAuthorized(request)) {
|
|
2573
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2574
|
+
}
|
|
2575
|
+
let body = {};
|
|
2576
|
+
try {
|
|
2577
|
+
body = await request.json();
|
|
2578
|
+
} catch {
|
|
2579
|
+
body = {};
|
|
2580
|
+
}
|
|
2581
|
+
const runType = typeof body["runType"] === "string" ? body["runType"] : "full";
|
|
2582
|
+
const result = await dispatchScheduledFlows({ runType });
|
|
2583
|
+
return Response.json(result);
|
|
2584
|
+
}
|
|
1691
2585
|
const webhookIdx = segments.indexOf("webhook");
|
|
1692
2586
|
if (webhookIdx !== -1 && webhookIdx === segments.length - 2) {
|
|
1693
2587
|
const plugName = segments[webhookIdx + 1];
|
|
@@ -1802,7 +2696,13 @@ function khotan(config) {
|
|
|
1802
2696
|
}
|
|
1803
2697
|
try {
|
|
1804
2698
|
const result = await startWorkflow(c.workflow, [
|
|
1805
|
-
{
|
|
2699
|
+
{
|
|
2700
|
+
event,
|
|
2701
|
+
eventType,
|
|
2702
|
+
headers,
|
|
2703
|
+
khotanRunId,
|
|
2704
|
+
khotanInstanceId: instanceId
|
|
2705
|
+
}
|
|
1806
2706
|
]);
|
|
1807
2707
|
const workflowRunId = result && typeof result === "object" ? "runId" in result ? String(result.runId) : "id" in result ? String(result.id) : null : null;
|
|
1808
2708
|
if (workflowRunId) {
|
|
@@ -1865,7 +2765,14 @@ function khotan(config) {
|
|
|
1865
2765
|
}
|
|
1866
2766
|
try {
|
|
1867
2767
|
const result = await startWorkflow(p.workflow, [
|
|
1868
|
-
{
|
|
2768
|
+
{
|
|
2769
|
+
event,
|
|
2770
|
+
eventType,
|
|
2771
|
+
headers,
|
|
2772
|
+
destVars,
|
|
2773
|
+
khotanRunId,
|
|
2774
|
+
khotanInstanceId: instanceId
|
|
2775
|
+
}
|
|
1869
2776
|
]);
|
|
1870
2777
|
const workflowRunId = result && typeof result === "object" ? "runId" in result ? String(result.runId) : "id" in result ? String(result.id) : null : null;
|
|
1871
2778
|
if (workflowRunId) {
|
|
@@ -2093,7 +3000,38 @@ function khotan(config) {
|
|
|
2093
3000
|
}
|
|
2094
3001
|
if (mappingsIdx !== -1 && mappingsIdx === segments.length - 2 && segments[mappingsIdx + 1] === "lookup") {
|
|
2095
3002
|
const body = await request.json();
|
|
2096
|
-
|
|
3003
|
+
if (!body || typeof body !== "object" || typeof body["resourceId"] !== "string") {
|
|
3004
|
+
return Response.json(
|
|
3005
|
+
{
|
|
3006
|
+
error: "Lookup requires resourceId plus either connectValue or plugName with ref"
|
|
3007
|
+
},
|
|
3008
|
+
{ status: 400 }
|
|
3009
|
+
);
|
|
3010
|
+
}
|
|
3011
|
+
const hasConnectValue = "connectValue" in body;
|
|
3012
|
+
const hasPlugRef = "plugName" in body && typeof body.plugName === "string" && "ref" in body && typeof body.ref === "string";
|
|
3013
|
+
if (!hasConnectValue && !hasPlugRef) {
|
|
3014
|
+
return Response.json(
|
|
3015
|
+
{
|
|
3016
|
+
error: "Lookup requires either connectValue or plugName with ref"
|
|
3017
|
+
},
|
|
3018
|
+
{ status: 400 }
|
|
3019
|
+
);
|
|
3020
|
+
}
|
|
3021
|
+
let mapping;
|
|
3022
|
+
try {
|
|
3023
|
+
mapping = hasConnectValue ? await lookupMapping({
|
|
3024
|
+
resourceId: body.resourceId,
|
|
3025
|
+
connectValue: body.connectValue
|
|
3026
|
+
}) : await lookupMapping({
|
|
3027
|
+
resourceId: body.resourceId,
|
|
3028
|
+
plugName: body.plugName,
|
|
3029
|
+
ref: body.ref
|
|
3030
|
+
});
|
|
3031
|
+
} catch (error) {
|
|
3032
|
+
const message = error instanceof Error ? error.message : "Invalid lookup request";
|
|
3033
|
+
return Response.json({ error: message }, { status: 400 });
|
|
3034
|
+
}
|
|
2097
3035
|
if (!mapping) {
|
|
2098
3036
|
return Response.json({ error: "Mapping not found" }, { status: 404 });
|
|
2099
3037
|
}
|
|
@@ -2101,11 +3039,17 @@ function khotan(config) {
|
|
|
2101
3039
|
}
|
|
2102
3040
|
if (mappingsIdx !== -1 && mappingsIdx === segments.length - 1) {
|
|
2103
3041
|
const body = await request.json();
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
3042
|
+
try {
|
|
3043
|
+
const existing = await lookupMapping({
|
|
3044
|
+
resourceId: body.resourceId,
|
|
3045
|
+
connectValue: body.connectValue
|
|
3046
|
+
});
|
|
3047
|
+
const saved = await upsertMapping(body);
|
|
3048
|
+
return Response.json(saved, { status: existing ? 200 : 201 });
|
|
3049
|
+
} catch (error) {
|
|
3050
|
+
const message = error instanceof Error ? error.message : "Invalid mapping payload";
|
|
3051
|
+
return Response.json({ error: message }, { status: 400 });
|
|
3052
|
+
}
|
|
2109
3053
|
}
|
|
2110
3054
|
}
|
|
2111
3055
|
if (request.method === "PATCH") {
|
|
@@ -2143,11 +3087,30 @@ function khotan(config) {
|
|
|
2143
3087
|
if (mappingsIdx !== -1 && mappingsIdx === segments.length - 2) {
|
|
2144
3088
|
const mappingId = segments[mappingsIdx + 1];
|
|
2145
3089
|
const body = await request.json();
|
|
2146
|
-
|
|
2147
|
-
|
|
3090
|
+
try {
|
|
3091
|
+
const saved = await updateMapping(mappingId, body);
|
|
3092
|
+
return Response.json(saved);
|
|
3093
|
+
} catch (error) {
|
|
3094
|
+
const message = error instanceof Error ? error.message : "Invalid mapping payload";
|
|
3095
|
+
return Response.json(
|
|
3096
|
+
{ error: message },
|
|
3097
|
+
{ status: message.includes("not found") ? 404 : 400 }
|
|
3098
|
+
);
|
|
3099
|
+
}
|
|
2148
3100
|
}
|
|
2149
3101
|
}
|
|
2150
3102
|
if (request.method === "DELETE") {
|
|
3103
|
+
if (cachesIdx !== -1 && cachesIdx === segments.length - 3) {
|
|
3104
|
+
const cacheName = decodeURIComponent(segments[cachesIdx + 1]);
|
|
3105
|
+
const key = decodeURIComponent(segments[cachesIdx + 2]);
|
|
3106
|
+
try {
|
|
3107
|
+
await createCacheInstance(cacheName).delete(key);
|
|
3108
|
+
return new Response(null, { status: 204 });
|
|
3109
|
+
} catch (error) {
|
|
3110
|
+
const message = error instanceof Error ? error.message : "Invalid cache request";
|
|
3111
|
+
return Response.json({ error: message }, { status: 400 });
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
2151
3114
|
if (variablesIdx !== -1 && variablesIdx === segments.length - 2) {
|
|
2152
3115
|
const plugName = segments[variablesIdx + 1];
|
|
2153
3116
|
if (!plugNames.has(plugName)) {
|
|
@@ -2179,6 +3142,10 @@ function khotan(config) {
|
|
|
2179
3142
|
}
|
|
2180
3143
|
if (mappingsIdx !== -1 && mappingsIdx === segments.length - 2) {
|
|
2181
3144
|
const mappingId = segments[mappingsIdx + 1];
|
|
3145
|
+
const existing = await adapter.getMapping(mappingId);
|
|
3146
|
+
if (!existing) {
|
|
3147
|
+
return Response.json({ error: "Mapping not found" }, { status: 404 });
|
|
3148
|
+
}
|
|
2182
3149
|
await adapter.deleteMapping(mappingId);
|
|
2183
3150
|
return new Response(null, { status: 204 });
|
|
2184
3151
|
}
|
|
@@ -2282,11 +3249,25 @@ function khotan(config) {
|
|
|
2282
3249
|
}
|
|
2283
3250
|
return plugReg.plug;
|
|
2284
3251
|
}
|
|
3252
|
+
khotanRuntimeRegistry.set(instanceId, {
|
|
3253
|
+
cache: createCacheInstance,
|
|
3254
|
+
listMappings,
|
|
3255
|
+
lookupMapping,
|
|
3256
|
+
upsertMapping,
|
|
3257
|
+
updateMapping,
|
|
3258
|
+
deleteMapping
|
|
3259
|
+
});
|
|
2285
3260
|
return {
|
|
2286
3261
|
handler,
|
|
2287
3262
|
init,
|
|
2288
3263
|
flow,
|
|
2289
3264
|
wire,
|
|
3265
|
+
cache: createCacheInstance,
|
|
3266
|
+
listMappings,
|
|
3267
|
+
lookupMapping,
|
|
3268
|
+
upsertMapping,
|
|
3269
|
+
updateMapping,
|
|
3270
|
+
deleteMapping,
|
|
2290
3271
|
getVars,
|
|
2291
3272
|
setVars,
|
|
2292
3273
|
clearVars,
|
|
@@ -2311,8 +3292,11 @@ function toNextJsHandler(factoryHandler) {
|
|
|
2311
3292
|
exports.__setWorkflowGetRunForTests = __setWorkflowGetRunForTests;
|
|
2312
3293
|
exports.__setWorkflowGetWritableForTests = __setWorkflowGetWritableForTests;
|
|
2313
3294
|
exports.__setWorkflowStartForTests = __setWorkflowStartForTests;
|
|
3295
|
+
exports.bindWorkflowPlug = bindWorkflowPlug;
|
|
2314
3296
|
exports.drizzleAdapter = drizzleAdapter;
|
|
2315
3297
|
exports.khotan = khotan;
|
|
3298
|
+
exports.khotanCache = khotanCache;
|
|
3299
|
+
exports.khotanMappings = khotanMappings;
|
|
2316
3300
|
exports.sendUpdate = sendUpdate;
|
|
2317
3301
|
exports.toNextJsHandler = toNextJsHandler;
|
|
2318
3302
|
//# sourceMappingURL=factory.cjs.map
|