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