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/dist/factory.cjs CHANGED
@@ -164,7 +164,7 @@ var khotanRuns = pgCore.pgTable(
164
164
  )
165
165
  ]
166
166
  );
167
- var khotanMappings = pgCore.pgTable(
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: khotanMappings.resourceId,
398
- mappingCount: drizzleOrm.count(khotanMappings.id).as("mapping_count")
399
- }).from(khotanMappings).groupBy(khotanMappings.resourceId).as("mapping_counts");
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
- return rows[0] ?? null;
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(khotanMappings).set({
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(khotanMappings.id, mapping.id)).returning({ id: khotanMappings.id });
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: khotanMappings.id }).from(khotanMappings).where(
434
- drizzleOrm.sql`${khotanMappings.resourceId} = ${mapping.resourceId} and ${khotanMappings.connectValue} = ${mapping.connectValue}`
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(khotanMappings).values({
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: [khotanMappings.resourceId, khotanMappings.connectValue],
605
+ target: [khotanMappingsTable.resourceId, khotanMappingsTable.connectValue],
443
606
  set: {
444
- refs: drizzleOrm.sql`${khotanMappings.refs} || ${JSON.stringify(mapping.refs)}::jsonb`,
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: khotanMappings.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(khotanMappings).where(drizzleOrm.eq(khotanMappings.id, id)).limit(1);
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
- return db.select().from(khotanMappings).where(drizzleOrm.eq(khotanMappings.resourceId, resourceId));
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(khotanMappings).where(drizzleOrm.eq(khotanMappings.id, id));
631
+ await db.delete(khotanMappingsTable).where(drizzleOrm.eq(khotanMappingsTable.id, id));
460
632
  },
461
- async lookupMapping({ resourceId, plugName, ref }) {
462
- const rows = await db.select().from(khotanMappings).where(
463
- drizzleOrm.sql`${khotanMappings.resourceId} = ${resourceId} and ${khotanMappings.refs}->>${plugName} = ${ref}`
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 plugNames = /* @__PURE__ */ new Set();
1425
+ const registeredFlowNames = /* @__PURE__ */ new Set();
918
1426
  for (const plug of plugs) {
919
- if (plugNames.has(plug.name)) {
920
- throw new Error(`Duplicate plug name: "${plug.name}"`);
921
- }
922
- plugNames.add(plug.name);
923
- if (plug.flows) {
924
- for (const flow2 of plug.flows) {
925
- if (flow2.resource && !resourceNames.has(flow2.resource)) {
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
- const resourceIdMap = /* @__PURE__ */ new Map();
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
- resourceIdMap.set(resource.name, id);
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 = resourceIdMap.get(flow2.resource);
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
- const plug = plugReg.plug;
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 opts = (extra) => ({
1259
- ...extra,
1972
+ const boundPlug = bindPlugWithVars(
1973
+ plugReg.plug,
1260
1974
  vars,
1261
- ...secret ? { _setVars: setFlowVars } : {}
1262
- });
1263
- const boundPlug = {
1264
- get(path, extra) {
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
- khotanRunId: runId
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 data = await adapter.listMappings(resourceId);
1667
- return Response.json(data);
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
- { event, eventType, headers, khotanRunId }
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
- { event, eventType, headers, destVars, khotanRunId }
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
- const mapping = await adapter.lookupMapping(body);
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
- const result = await adapter.upsertMapping(body);
2105
- return Response.json(
2106
- { id: result.id },
2107
- { status: result.created ? 201 : 200 }
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
- const result = await adapter.upsertMapping({ ...body, id: mappingId });
2147
- return Response.json(result);
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