@rawdash/core 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -291,8 +291,79 @@ function computeDelay(attempt, initialDelayMs, maxDelayMs) {
291
291
  const jitter = base * 0.25 * Math.random();
292
292
  return Math.min(base + jitter, maxDelayMs);
293
293
  }
294
+ var MAX_VALUE_LEN = 120;
295
+ function truncate(s, max = MAX_VALUE_LEN) {
296
+ if (s.length <= max) {
297
+ return s;
298
+ }
299
+ return `${s.slice(0, max - 1)}\u2026`;
300
+ }
301
+ function formatValue(value) {
302
+ if (value === null) {
303
+ return "null";
304
+ }
305
+ if (value === void 0) {
306
+ return "";
307
+ }
308
+ if (typeof value === "number" || typeof value === "boolean") {
309
+ return String(value);
310
+ }
311
+ if (typeof value === "string") {
312
+ const t = truncate(value);
313
+ if (/[\s"=]/.test(t)) {
314
+ return JSON.stringify(t);
315
+ }
316
+ return t;
317
+ }
318
+ if (typeof value === "bigint") {
319
+ return value.toString();
320
+ }
321
+ let json;
322
+ try {
323
+ json = JSON.stringify(value);
324
+ } catch {
325
+ json = void 0;
326
+ }
327
+ return truncate(json ?? String(value));
328
+ }
329
+ function formatLogFields(fields) {
330
+ if (!fields) {
331
+ return "";
332
+ }
333
+ const parts = [];
334
+ for (const [k, v] of Object.entries(fields)) {
335
+ if (v === void 0) {
336
+ continue;
337
+ }
338
+ parts.push(`${k}=${formatValue(v)}`);
339
+ }
340
+ return parts.length > 0 ? ` ${parts.join(" ")}` : "";
341
+ }
342
+ function formatLogLine(scope, event, fields) {
343
+ return `[${scope}] ${event}${formatLogFields(fields)}`;
344
+ }
345
+ function createDefaultConnectorLogger(opts) {
346
+ return {
347
+ info(event, fields) {
348
+ console.info(formatLogLine(opts.scope, event, fields));
349
+ },
350
+ warn(event, fields) {
351
+ console.warn(formatLogLine(opts.scope, event, fields));
352
+ }
353
+ };
354
+ }
355
+ var NOOP_LOGGER = {
356
+ info() {
357
+ },
358
+ warn() {
359
+ }
360
+ };
361
+ function noopConnectorLogger() {
362
+ return NOOP_LOGGER;
363
+ }
294
364
 
295
365
  // src/secrets.ts
366
+ import { z } from "zod";
296
367
  function secret(name) {
297
368
  if (!/^[A-Z][A-Z0-9_]*$/.test(name)) {
298
369
  throw new Error(
@@ -304,10 +375,31 @@ function secret(name) {
304
375
  function isSecret(value) {
305
376
  return typeof value === "object" && value !== null && "$secret" in value && typeof value.$secret === "string";
306
377
  }
378
+ var secretRefSchema = z.strictObject({
379
+ $secret: z.string()
380
+ });
381
+ function withSecretRef(schema) {
382
+ return z.union([schema, secretRefSchema]);
383
+ }
307
384
  var EnvSecretsResolver = class {
308
385
  resolve(name) {
309
386
  const env = globalThis.process?.env;
310
- return env?.[name];
387
+ const raw = env?.[name];
388
+ if (raw === void 0) {
389
+ return void 0;
390
+ }
391
+ if (raw.length === 0) {
392
+ return raw;
393
+ }
394
+ const first = raw.charCodeAt(0);
395
+ if (first !== 123 && first !== 91) {
396
+ return raw;
397
+ }
398
+ try {
399
+ return JSON.parse(raw);
400
+ } catch {
401
+ return raw;
402
+ }
311
403
  }
312
404
  };
313
405
  function extractSecretNames(value) {
@@ -364,6 +456,7 @@ var BaseConnector = class {
364
456
  creds;
365
457
  rawCredInput;
366
458
  ctx;
459
+ cachedLogger;
367
460
  constructor(settings, creds, ctx) {
368
461
  this.settings = settings;
369
462
  this.rawCredInput = creds;
@@ -373,6 +466,12 @@ var BaseConnector = class {
373
466
  this.ctx.secretsResolver ?? new EnvSecretsResolver()
374
467
  ) : {};
375
468
  }
469
+ get logger() {
470
+ if (!this.cachedLogger) {
471
+ this.cachedLogger = this.ctx.logger ?? createDefaultConnectorLogger({ scope: this.id });
472
+ }
473
+ return this.cachedLogger;
474
+ }
376
475
  request(req, opts) {
377
476
  return request(req, {
378
477
  resource: opts.resource,
@@ -405,6 +504,13 @@ var BaseConnector = class {
405
504
  { resource: opts.resource, requestId: opts.requestId }
406
505
  );
407
506
  }
507
+ isResourceEnabled(resource) {
508
+ const enabled = this.settings?.resources;
509
+ if (!enabled || enabled.length === 0) {
510
+ return true;
511
+ }
512
+ return enabled.includes(resource);
513
+ }
408
514
  serializeConfig() {
409
515
  const config = {
410
516
  ...this.settings
@@ -478,8 +584,44 @@ function defineConnector() {
478
584
  }
479
585
 
480
586
  // src/paginate-chunked.ts
587
+ function selectActivePhases(resourceToPhase, order, enabled) {
588
+ if (!enabled || enabled.length === 0) {
589
+ return [...order];
590
+ }
591
+ const want = /* @__PURE__ */ new Set();
592
+ for (const r of enabled) {
593
+ want.add(resourceToPhase(r));
594
+ }
595
+ return order.filter((p) => want.has(p));
596
+ }
597
+ function makeChunkedCursorGuard(phases) {
598
+ const phaseSet = new Set(phases);
599
+ return (value) => {
600
+ if (typeof value !== "object" || value === null) {
601
+ return false;
602
+ }
603
+ const v = value;
604
+ if (typeof v.phase !== "string" || !phaseSet.has(v.phase)) {
605
+ return false;
606
+ }
607
+ if (v.page !== null && typeof v.page !== "string") {
608
+ return false;
609
+ }
610
+ return true;
611
+ };
612
+ }
613
+ function truncateCursor(page) {
614
+ if (page === null || page === void 0) {
615
+ return void 0;
616
+ }
617
+ const s = typeof page === "string" ? page : JSON.stringify(page);
618
+ if (s.length <= 80) {
619
+ return s;
620
+ }
621
+ return `${s.slice(0, 79)}\u2026`;
622
+ }
481
623
  async function paginateChunked(opts) {
482
- const { phases, cursor, signal, fetchPage, writeBatch } = opts;
624
+ const { phases, cursor, signal, fetchPage, writeBatch, logger } = opts;
483
625
  if (phases.length === 0) {
484
626
  return { done: true };
485
627
  }
@@ -489,6 +631,9 @@ async function paginateChunked(opts) {
489
631
  for (let i = startIdx; i < phases.length; i++) {
490
632
  const phase = phases[i];
491
633
  let page = i === startIdx && hasKnownResumePhase ? cursor.page : null;
634
+ let pageCount = 0;
635
+ let itemCount = 0;
636
+ const phaseStart = Date.now();
492
637
  while (true) {
493
638
  if (signal?.aborted) {
494
639
  return {
@@ -496,6 +641,7 @@ async function paginateChunked(opts) {
496
641
  cursor: { phase, page }
497
642
  };
498
643
  }
644
+ pageCount += 1;
499
645
  let items;
500
646
  let next;
501
647
  try {
@@ -507,12 +653,26 @@ async function paginateChunked(opts) {
507
653
  cursor: { phase, page }
508
654
  };
509
655
  }
656
+ logger?.warn("fetch page failed", {
657
+ resource: phase,
658
+ page: pageCount,
659
+ cursor: truncateCursor(page),
660
+ error: err instanceof Error ? err.message : String(err)
661
+ });
510
662
  return {
511
663
  done: false,
512
664
  cursor: { phase, page },
513
665
  transientError: err
514
666
  };
515
667
  }
668
+ itemCount += items.length;
669
+ logger?.info("fetched page", {
670
+ resource: phase,
671
+ page: pageCount,
672
+ items: items.length,
673
+ cursor: truncateCursor(page),
674
+ next: truncateCursor(next)
675
+ });
516
676
  try {
517
677
  await writeBatch(phase, items, page);
518
678
  } catch (err) {
@@ -522,6 +682,12 @@ async function paginateChunked(opts) {
522
682
  cursor: { phase, page }
523
683
  };
524
684
  }
685
+ logger?.warn("write batch failed", {
686
+ resource: phase,
687
+ page: pageCount,
688
+ cursor: truncateCursor(page),
689
+ error: err instanceof Error ? err.message : String(err)
690
+ });
525
691
  return {
526
692
  done: false,
527
693
  cursor: { phase, page },
@@ -533,20 +699,26 @@ async function paginateChunked(opts) {
533
699
  }
534
700
  page = next;
535
701
  }
702
+ logger?.info("resource done", {
703
+ resource: phase,
704
+ pages: pageCount,
705
+ items: itemCount,
706
+ duration_ms: Date.now() - phaseStart
707
+ });
536
708
  }
537
709
  return { done: true };
538
710
  }
539
711
 
540
712
  // src/widget-schemas.ts
541
- import { z } from "zod";
542
- var shapeSchema = z.enum([
713
+ import { z as z2 } from "zod";
714
+ var shapeSchema = z2.enum([
543
715
  "event",
544
716
  "entity",
545
717
  "metric",
546
718
  "edge",
547
719
  "distribution"
548
720
  ]);
549
- var aggFnSchema = z.enum([
721
+ var aggFnSchema = z2.enum([
550
722
  "count",
551
723
  "sum",
552
724
  "avg",
@@ -555,7 +727,7 @@ var aggFnSchema = z.enum([
555
727
  "latest",
556
728
  "first"
557
729
  ]);
558
- var filterOperatorSchema = z.enum([
730
+ var filterOperatorSchema = z2.enum([
559
731
  "eq",
560
732
  "neq",
561
733
  "gt",
@@ -564,70 +736,70 @@ var filterOperatorSchema = z.enum([
564
736
  "lte",
565
737
  "contains"
566
738
  ]);
567
- var filterConditionSchema = z.object({
568
- field: z.string(),
739
+ var filterConditionSchema = z2.object({
740
+ field: z2.string(),
569
741
  op: filterOperatorSchema,
570
- value: z.union([z.string(), z.number(), z.boolean()])
742
+ value: z2.union([z2.string(), z2.number(), z2.boolean()])
571
743
  });
572
- var filterClauseSchema = z.union([
744
+ var filterClauseSchema = z2.union([
573
745
  filterConditionSchema,
574
- z.object({ or: z.array(filterConditionSchema) })
746
+ z2.object({ or: z2.array(filterConditionSchema) })
575
747
  ]);
576
- var groupBySchema = z.object({
577
- field: z.string(),
578
- granularity: z.enum(["hour", "day", "week", "month"])
748
+ var groupBySchema = z2.object({
749
+ field: z2.string(),
750
+ granularity: z2.enum(["hour", "day", "week", "month"])
579
751
  });
580
- var computedMetricSchema = z.object({
581
- connectorId: z.string(),
752
+ var computedMetricSchema = z2.object({
753
+ connectorId: z2.string(),
582
754
  shape: shapeSchema,
583
- name: z.string().optional(),
584
- entityType: z.string().optional(),
585
- field: z.string().optional(),
755
+ name: z2.string().optional(),
756
+ entityType: z2.string().optional(),
757
+ field: z2.string().optional(),
586
758
  fn: aggFnSchema,
587
- window: z.string().optional(),
588
- filter: z.array(filterClauseSchema).optional(),
759
+ window: z2.string().optional(),
760
+ filter: z2.array(filterClauseSchema).optional(),
589
761
  groupBy: groupBySchema.optional()
590
762
  }).refine((m) => m.fn === "count" || m.field !== void 0, {
591
763
  message: 'field is required unless fn is "count"',
592
764
  path: ["field"]
593
765
  });
594
- var titleField = z.string().meta({ label: "Title", description: "Widget title." });
595
- var statWidgetSchema = z.object({
596
- kind: z.literal("stat"),
766
+ var titleField = z2.string().meta({ label: "Title", description: "Widget title." });
767
+ var statWidgetSchema = z2.object({
768
+ kind: z2.literal("stat"),
597
769
  title: titleField,
598
770
  metric: computedMetricSchema.meta({
599
771
  label: "Metric",
600
772
  description: "Computed metric definition."
601
773
  }),
602
- window: z.string().optional().meta({ label: "Window", description: "Time window, e.g. '7d'." }),
603
- compare: z.enum(["none", "previous-period"]).default("none").meta({ label: "Compare", description: "Comparison mode." })
774
+ window: z2.string().optional().meta({ label: "Window", description: "Time window, e.g. '7d'." }),
775
+ compare: z2.enum(["none", "previous-period"]).default("none").meta({ label: "Compare", description: "Comparison mode." })
604
776
  });
605
- var statusWidgetSchema = z.object({
606
- kind: z.literal("status"),
777
+ var statusWidgetSchema = z2.object({
778
+ kind: z2.literal("status"),
607
779
  title: titleField,
608
- source: z.string().meta({
780
+ source: z2.string().meta({
609
781
  label: "Source",
610
782
  description: "Connector or data source reference."
611
783
  })
612
784
  });
613
- var timeseriesWidgetSchema = z.object({
614
- kind: z.literal("timeseries"),
785
+ var timeseriesWidgetSchema = z2.object({
786
+ kind: z2.literal("timeseries"),
615
787
  title: titleField,
616
788
  metric: computedMetricSchema.meta({
617
789
  label: "Metric",
618
790
  description: "Computed metric definition."
619
791
  }),
620
- window: z.string().meta({ label: "Window", description: "Time window, e.g. '30d'." }),
621
- granularity: z.enum(["hour", "day", "week"]).default("day").meta({ label: "Granularity", description: "Time bucket size." })
792
+ window: z2.string().meta({ label: "Window", description: "Time window, e.g. '30d'." }),
793
+ granularity: z2.enum(["hour", "day", "week"]).default("day").meta({ label: "Granularity", description: "Time bucket size." })
622
794
  });
623
- var distributionWidgetSchema = z.object({
624
- kind: z.literal("distribution"),
795
+ var distributionWidgetSchema = z2.object({
796
+ kind: z2.literal("distribution"),
625
797
  title: titleField,
626
798
  metric: computedMetricSchema.meta({
627
799
  label: "Metric",
628
800
  description: "Computed metric definition."
629
801
  }),
630
- window: z.string().meta({ label: "Window", description: "Time window, e.g. '7d'." })
802
+ window: z2.string().meta({ label: "Window", description: "Time window, e.g. '7d'." })
631
803
  });
632
804
  var widgetSchemas = {
633
805
  stat: statWidgetSchema,
@@ -635,7 +807,7 @@ var widgetSchemas = {
635
807
  timeseries: timeseriesWidgetSchema,
636
808
  distribution: distributionWidgetSchema
637
809
  };
638
- var widgetSchema = z.discriminatedUnion("kind", [
810
+ var widgetSchema = z2.discriminatedUnion("kind", [
639
811
  statWidgetSchema,
640
812
  statusWidgetSchema,
641
813
  timeseriesWidgetSchema,
@@ -666,7 +838,7 @@ function defineDashboard(options) {
666
838
  }
667
839
  function defineMetric(options) {
668
840
  return {
669
- connectorId: options.connector.id,
841
+ connectorId: options.connector.name,
670
842
  shape: options.shape,
671
843
  name: options.name,
672
844
  entityType: options.entityType,
@@ -715,7 +887,36 @@ function validateConfig(config) {
715
887
  'defineConfig: config must include a "dashboards" record \u2014 did you mean to migrate from the old flat "widgets" shape?'
716
888
  );
717
889
  }
718
- const connectorIds = new Set(config.connectors.map((e) => e.connector.id));
890
+ if (!Array.isArray(config.connectors)) {
891
+ throw new Error('defineConfig: "connectors" must be an array');
892
+ }
893
+ const connectorNames = /* @__PURE__ */ new Set();
894
+ for (const entry of config.connectors) {
895
+ if (!entry || typeof entry !== "object") {
896
+ throw new Error(
897
+ 'defineConfig: every connector entry must be an object with "name", "connectorId", and "config"'
898
+ );
899
+ }
900
+ if (!entry.name) {
901
+ throw new Error('defineConfig: every connector entry must have a "name"');
902
+ }
903
+ if (!entry.connectorId) {
904
+ throw new Error(
905
+ `defineConfig: connector "${entry.name}" must have a "connectorId" (the connector type id)`
906
+ );
907
+ }
908
+ if (entry.config === null || typeof entry.config !== "object" || Array.isArray(entry.config)) {
909
+ throw new Error(
910
+ `defineConfig: connector "${entry.name}" must have a "config" object`
911
+ );
912
+ }
913
+ if (connectorNames.has(entry.name)) {
914
+ throw new Error(
915
+ `defineConfig: duplicate connector name "${entry.name}". Each instance must have a unique name.`
916
+ );
917
+ }
918
+ connectorNames.add(entry.name);
919
+ }
719
920
  for (const [dashboardKey, dashboard] of Object.entries(config.dashboards)) {
720
921
  if (!dashboard.widgets || typeof dashboard.widgets !== "object" || Array.isArray(dashboard.widgets)) {
721
922
  throw new Error(
@@ -738,7 +939,7 @@ function validateConfig(config) {
738
939
  continue;
739
940
  }
740
941
  const { connectorId, shape, fn } = widget.metric;
741
- if (!connectorIds.has(connectorId)) {
942
+ if (!connectorNames.has(connectorId)) {
742
943
  throw new Error(
743
944
  `${ref}: connector "${connectorId}" is not listed in connectors`
744
945
  );
@@ -808,9 +1009,9 @@ async function computeRetention(handle, config, nowMs = Date.now()) {
808
1009
  }
809
1010
 
810
1011
  // src/config-fields.ts
811
- import { z as z2 } from "zod";
1012
+ import { z as z3 } from "zod";
812
1013
  function defineConfigFields(schema) {
813
- if (!(schema instanceof z2.ZodObject)) {
1014
+ if (!(schema instanceof z3.ZodObject)) {
814
1015
  throw new Error(
815
1016
  `configFields must be a Zod object schema (z.object({...})). Received: ${Object.prototype.toString.call(schema)}`
816
1017
  );
@@ -818,6 +1019,88 @@ function defineConfigFields(schema) {
818
1019
  return schema;
819
1020
  }
820
1021
 
1022
+ // src/connector-doc.ts
1023
+ import { z as z4 } from "zod";
1024
+ var connectorCategorySchema = z4.enum([
1025
+ "engineering",
1026
+ "product",
1027
+ "analytics",
1028
+ "marketing",
1029
+ "sales",
1030
+ "support",
1031
+ "finance",
1032
+ "infrastructure",
1033
+ "security"
1034
+ ]);
1035
+ var connectorDocSchema = z4.object({
1036
+ displayName: z4.string().min(1),
1037
+ category: connectorCategorySchema,
1038
+ tagline: z4.string().min(1),
1039
+ // Brand accent color (hex) for the connector's docs/landing card. The icon
1040
+ // itself is a committed `icon.svg` co-located in the connector package.
1041
+ brandColor: z4.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(),
1042
+ vendor: z4.object({
1043
+ name: z4.string().min(1),
1044
+ apiDocs: z4.url().optional(),
1045
+ website: z4.url().optional()
1046
+ }),
1047
+ auth: z4.object({
1048
+ summary: z4.string().min(1),
1049
+ setup: z4.array(z4.string().min(1))
1050
+ }),
1051
+ // Upstream API rate-limit / quota notes worth surfacing (e.g. "GA4 Data API:
1052
+ // 200k tokens/day per property"). Free text, rendered in docs.
1053
+ rateLimit: z4.string().min(1).optional(),
1054
+ // Operational caveats / out-of-scope notes (API ceilings, sampling, Cloud-only,
1055
+ // data revision windows, etc.).
1056
+ limitations: z4.array(z4.string().min(1)).optional()
1057
+ });
1058
+ function defineConnectorDoc(doc) {
1059
+ return connectorDocSchema.parse(doc);
1060
+ }
1061
+
1062
+ // src/resource.ts
1063
+ var SHAPES = /* @__PURE__ */ new Set([
1064
+ "entity",
1065
+ "event",
1066
+ "metric",
1067
+ "edge",
1068
+ "distribution"
1069
+ ]);
1070
+ function defineResources(defs) {
1071
+ for (const [name, def] of Object.entries(defs)) {
1072
+ if (!name) {
1073
+ throw new Error("Resource name must be a non-empty string.");
1074
+ }
1075
+ if (!SHAPES.has(def.shape)) {
1076
+ throw new Error(
1077
+ `Resource "${name}" has invalid shape "${def.shape}". Expected one of: ${[...SHAPES].join(", ")}.`
1078
+ );
1079
+ }
1080
+ if (!def.description || def.description.trim().length === 0) {
1081
+ throw new Error(`Resource "${name}" must have a non-empty description.`);
1082
+ }
1083
+ }
1084
+ return defs;
1085
+ }
1086
+ function schemasFromResources(defs) {
1087
+ const out = {};
1088
+ for (const [name, def] of Object.entries(defs)) {
1089
+ if (!def.responses) {
1090
+ continue;
1091
+ }
1092
+ for (const [tag, schema] of Object.entries(def.responses)) {
1093
+ if (out[tag]) {
1094
+ throw new Error(
1095
+ `Duplicate response schema tag "${tag}" (declared again on resource "${name}").`
1096
+ );
1097
+ }
1098
+ out[tag] = schema;
1099
+ }
1100
+ }
1101
+ return out;
1102
+ }
1103
+
821
1104
  // src/compute.ts
822
1105
  function matchesCondition(record, cond) {
823
1106
  const val = record[cond.field];
@@ -1072,39 +1355,181 @@ async function computeMetric(storage, metric) {
1072
1355
  return computeAgg(sorted, metric.field, metric.fn);
1073
1356
  }
1074
1357
 
1075
- // src/resolve-widget.ts
1076
- async function resolveWidget(widgetId, widget, connectors, storage) {
1358
+ // src/backfill-window.ts
1359
+ var WINDOW_UNIT_MS = {
1360
+ h: 36e5,
1361
+ d: 864e5,
1362
+ w: 6048e5,
1363
+ m: 2592e6
1364
+ };
1365
+ function parseWindowMs2(window) {
1366
+ const match = /^(\d+)(h|d|w|m)$/.exec(window);
1367
+ if (!match) {
1368
+ return void 0;
1369
+ }
1370
+ const unitMs = WINDOW_UNIT_MS[match[2]];
1371
+ if (unitMs === void 0) {
1372
+ return void 0;
1373
+ }
1374
+ return parseInt(match[1]) * unitMs;
1375
+ }
1376
+ function widgetWindow(widget) {
1377
+ switch (widget.kind) {
1378
+ case "stat":
1379
+ return widget.window ?? widget.metric.window;
1380
+ case "timeseries":
1381
+ case "distribution":
1382
+ return widget.window;
1383
+ case "status":
1384
+ return void 0;
1385
+ }
1386
+ }
1387
+ function widgetReference(widget) {
1077
1388
  if (widget.kind === "status") {
1078
- return {
1079
- widgetId,
1080
- connectorId: widget.source,
1081
- data: null,
1082
- cachedAt: null
1083
- };
1389
+ return { connectorName: widget.source, resourceName: void 0 };
1390
+ }
1391
+ return {
1392
+ connectorName: widget.metric.connectorId,
1393
+ resourceName: widget.metric.name ?? widget.metric.entityType
1394
+ };
1395
+ }
1396
+ function mergeWindow(existing, next) {
1397
+ if (next === void 0) {
1398
+ return existing;
1399
+ }
1400
+ if (existing === void 0) {
1401
+ return next;
1402
+ }
1403
+ return Math.max(existing, next);
1404
+ }
1405
+ function computeConnectorBackfill(config) {
1406
+ const result = /* @__PURE__ */ new Map();
1407
+ for (const dashboard of Object.values(config.dashboards)) {
1408
+ for (const widget of Object.values(dashboard.widgets)) {
1409
+ const { connectorName, resourceName } = widgetReference(widget);
1410
+ const windowStr = widgetWindow(widget);
1411
+ const windowMs = windowStr ? parseWindowMs2(windowStr) : void 0;
1412
+ let resources = result.get(connectorName);
1413
+ if (!resources) {
1414
+ resources = /* @__PURE__ */ new Map();
1415
+ result.set(connectorName, resources);
1416
+ }
1417
+ if (resourceName === void 0) {
1418
+ continue;
1419
+ }
1420
+ const existing = resources.get(resourceName);
1421
+ resources.set(resourceName, {
1422
+ requiredWindowMs: mergeWindow(existing?.requiredWindowMs, windowMs)
1423
+ });
1424
+ }
1425
+ }
1426
+ return result;
1427
+ }
1428
+
1429
+ // src/resolve-widget.ts
1430
+ var FAILING_CONNECTOR_STATUSES = /* @__PURE__ */ new Set(["error", "auth_failed", "paused"]);
1431
+ function deriveSyncStateFromHealth(health) {
1432
+ if (health.status === "syncing") {
1433
+ return "syncing";
1434
+ }
1435
+ if (FAILING_CONNECTOR_STATUSES.has(health.status)) {
1436
+ return "failing";
1437
+ }
1438
+ if (!health.lastSyncAt) {
1439
+ return "unsynced";
1440
+ }
1441
+ const ageMs = Date.now() - new Date(health.lastSyncAt).getTime();
1442
+ const windowMs = 2 * health.syncIntervalSeconds * 1e3;
1443
+ return ageMs <= windowMs ? "fresh" : "stale";
1444
+ }
1445
+ function buildMetaFromHealth(health) {
1446
+ const meta = { connectorStatus: health.status };
1447
+ if (health.lastError) {
1448
+ meta["lastError"] = health.lastError;
1084
1449
  }
1085
- const { connectorId } = widget.metric;
1086
- if (connectors !== void 0 && !isAllowedConnector(connectors, connectorId)) {
1450
+ return meta;
1451
+ }
1452
+ async function resolveWidget(dashboardId, widgetId, widget, connectors, storage) {
1453
+ const connectorId = widget.kind === "status" ? widget.source : widget.metric.connectorId;
1454
+ if (connectors !== void 0 && !connectors.includes(connectorId)) {
1087
1455
  return void 0;
1088
1456
  }
1089
1457
  const handle = storage.getStorageHandle(connectorId);
1090
- const data = await computeMetric(handle, widget.metric);
1458
+ const health = await handle.getHealth?.() ?? null;
1459
+ let data = null;
1460
+ if (widget.kind !== "status") {
1461
+ data = await computeMetric(handle, widget.metric);
1462
+ }
1463
+ let syncState;
1464
+ let meta;
1465
+ if (health) {
1466
+ syncState = deriveSyncStateFromHealth(health);
1467
+ meta = buildMetaFromHealth(health);
1468
+ } else if (data === null || data === void 0) {
1469
+ syncState = "unsynced";
1470
+ } else {
1471
+ syncState = "fresh";
1472
+ }
1091
1473
  return {
1092
1474
  widgetId,
1093
1475
  connectorId,
1094
1476
  data,
1095
- cachedAt: (await storage.getSyncState()).lastSyncAt
1477
+ cachedAt: health?.lastSyncAt ?? null,
1478
+ syncState,
1479
+ syncIntervalSeconds: health?.syncIntervalSeconds,
1480
+ meta
1096
1481
  };
1097
1482
  }
1098
- function isAllowedConnector(connectors, connectorId) {
1099
- if (connectors.length === 0) {
1100
- return false;
1483
+
1484
+ // src/widget-etag.ts
1485
+ function stableStringify(value) {
1486
+ if (value === null || typeof value !== "object") {
1487
+ return JSON.stringify(value) ?? "null";
1101
1488
  }
1102
- if (typeof connectors[0] === "string") {
1103
- return connectors.includes(connectorId);
1489
+ if (Array.isArray(value)) {
1490
+ return "[" + value.map(stableStringify).join(",") + "]";
1104
1491
  }
1105
- return connectors.some(
1106
- (e) => e.connector.id === connectorId
1492
+ const keys = Object.keys(value).sort();
1493
+ const parts = keys.map(
1494
+ (k) => JSON.stringify(k) + ":" + stableStringify(value[k])
1107
1495
  );
1496
+ return "{" + parts.join(",") + "}";
1497
+ }
1498
+ function hashWidgetConfig(widget) {
1499
+ const s = stableStringify(widget);
1500
+ let h = 2166136261;
1501
+ for (let i = 0; i < s.length; i++) {
1502
+ h ^= s.charCodeAt(i);
1503
+ h = Math.imul(h, 16777619);
1504
+ }
1505
+ return (h >>> 0).toString(16).padStart(8, "0");
1506
+ }
1507
+ function computeWidgetEtag(lastSyncAt, widget) {
1508
+ return `"${lastSyncAt ?? "null"}-${hashWidgetConfig(widget)}"`;
1509
+ }
1510
+
1511
+ // src/registry.ts
1512
+ function instantiateConnector(entry, registry, secretsResolver, logger) {
1513
+ const Cls = registry[entry.connectorId];
1514
+ if (!Cls) {
1515
+ throw new Error(
1516
+ `Unknown connector type "${entry.connectorId}" for instance "${entry.name}". Add it to the connectorRegistry.`
1517
+ );
1518
+ }
1519
+ const credSchema = Cls.credentials;
1520
+ const settings = {};
1521
+ const creds = {};
1522
+ for (const [key, value] of Object.entries(entry.config)) {
1523
+ if (credSchema && Object.prototype.hasOwnProperty.call(credSchema, key)) {
1524
+ creds[key] = value;
1525
+ } else {
1526
+ settings[key] = value;
1527
+ }
1528
+ }
1529
+ return new Cls(settings, credSchema ? creds : void 0, {
1530
+ secretsResolver,
1531
+ logger
1532
+ });
1108
1533
  }
1109
1534
 
1110
1535
  // src/storage-handle-guard.ts
@@ -1202,7 +1627,8 @@ function withAbortSignal(handle, signal) {
1202
1627
  queryEntities: (q) => handle.queryEntities(q),
1203
1628
  queryMetrics: (q) => handle.queryMetrics(q),
1204
1629
  traverse: (q) => handle.traverse(q),
1205
- queryDistributions: (q) => handle.queryDistributions(q)
1630
+ queryDistributions: (q) => handle.queryDistributions(q),
1631
+ ...handle.getHealth ? { getHealth: handle.getHealth.bind(handle) } : {}
1206
1632
  };
1207
1633
  }
1208
1634
 
@@ -1213,6 +1639,7 @@ var InMemoryStorage = class {
1213
1639
  metricStore = /* @__PURE__ */ new Map();
1214
1640
  edgeStore = /* @__PURE__ */ new Map();
1215
1641
  distributionStore = /* @__PURE__ */ new Map();
1642
+ lastWriteAt = /* @__PURE__ */ new Map();
1216
1643
  syncState = {
1217
1644
  status: "idle",
1218
1645
  queuedAt: null,
@@ -1225,6 +1652,9 @@ var InMemoryStorage = class {
1225
1652
  return options?.signal ? withAbortSignal(handle, options.signal) : handle;
1226
1653
  }
1227
1654
  buildHandle(connectorId) {
1655
+ const touch = () => {
1656
+ this.lastWriteAt.set(connectorId, (/* @__PURE__ */ new Date()).toISOString());
1657
+ };
1228
1658
  const getEntityMap = () => {
1229
1659
  if (!this.entityStore.has(connectorId)) {
1230
1660
  this.entityStore.set(connectorId, /* @__PURE__ */ new Map());
@@ -1268,24 +1698,29 @@ var InMemoryStorage = class {
1268
1698
  this.eventStore.set(connectorId, []);
1269
1699
  }
1270
1700
  this.eventStore.get(connectorId).push(e);
1701
+ touch();
1271
1702
  },
1272
1703
  entity: async (e) => {
1273
1704
  upsertEntities([e]);
1705
+ touch();
1274
1706
  },
1275
1707
  metric: async (m) => {
1276
1708
  if (!this.metricStore.has(connectorId)) {
1277
1709
  this.metricStore.set(connectorId, []);
1278
1710
  }
1279
1711
  this.metricStore.get(connectorId).push(m);
1712
+ touch();
1280
1713
  },
1281
1714
  edge: async (e) => {
1282
1715
  upsertEdges([e]);
1716
+ touch();
1283
1717
  },
1284
1718
  distribution: async (d) => {
1285
1719
  if (!this.distributionStore.has(connectorId)) {
1286
1720
  this.distributionStore.set(connectorId, []);
1287
1721
  }
1288
1722
  this.distributionStore.get(connectorId).push(d);
1723
+ touch();
1289
1724
  },
1290
1725
  events: async (es, scope) => {
1291
1726
  const names = new Set(scope?.names ?? es.map((e) => e.name));
@@ -1293,6 +1728,7 @@ var InMemoryStorage = class {
1293
1728
  (e) => !names.has(e.name)
1294
1729
  );
1295
1730
  this.eventStore.set(connectorId, [...kept, ...es]);
1731
+ touch();
1296
1732
  },
1297
1733
  entities: async (es, scope) => {
1298
1734
  const byType = getEntityMap();
@@ -1301,6 +1737,7 @@ var InMemoryStorage = class {
1301
1737
  byType.set(type, /* @__PURE__ */ new Map());
1302
1738
  }
1303
1739
  upsertEntities(es);
1740
+ touch();
1304
1741
  },
1305
1742
  metrics: async (ms, scope) => {
1306
1743
  const names = new Set(scope?.names ?? ms.map((m) => m.name));
@@ -1308,6 +1745,7 @@ var InMemoryStorage = class {
1308
1745
  (m) => !names.has(m.name)
1309
1746
  );
1310
1747
  this.metricStore.set(connectorId, [...kept, ...ms]);
1748
+ touch();
1311
1749
  },
1312
1750
  edges: async (es, scope) => {
1313
1751
  const kinds = new Set(scope?.kinds ?? es.map((e) => e.kind));
@@ -1316,6 +1754,7 @@ var InMemoryStorage = class {
1316
1754
  );
1317
1755
  this.edgeStore.set(connectorId, kept);
1318
1756
  upsertEdges(es);
1757
+ touch();
1319
1758
  },
1320
1759
  distributions: async (ds, scope) => {
1321
1760
  const names = new Set(scope?.names ?? ds.map((d) => d.name));
@@ -1323,6 +1762,7 @@ var InMemoryStorage = class {
1323
1762
  (d) => !names.has(d.name)
1324
1763
  );
1325
1764
  this.distributionStore.set(connectorId, [...kept, ...ds]);
1765
+ touch();
1326
1766
  },
1327
1767
  queryEvents: async (q) => {
1328
1768
  let results = this.eventStore.get(connectorId) ?? [];
@@ -1413,6 +1853,14 @@ var InMemoryStorage = class {
1413
1853
  `Unsupported shape for deleteOlderThan: ${String(shape)}`
1414
1854
  );
1415
1855
  }
1856
+ },
1857
+ getHealth: async () => {
1858
+ return {
1859
+ status: "idle",
1860
+ lastSyncAt: this.lastWriteAt.get(connectorId) ?? null,
1861
+ lastError: null,
1862
+ syncIntervalSeconds: 0
1863
+ };
1416
1864
  }
1417
1865
  };
1418
1866
  }
@@ -1464,34 +1912,34 @@ var InMemoryStorage = class {
1464
1912
  };
1465
1913
 
1466
1914
  // src/wire-config.ts
1467
- import { z as z3 } from "zod";
1468
- var wireConnectorSchema = z3.object({
1469
- name: z3.string(),
1470
- connectorId: z3.string(),
1471
- displayName: z3.string().optional(),
1472
- config: z3.record(z3.string(), z3.unknown()),
1473
- syncIntervalSeconds: z3.number().optional(),
1474
- enabled: z3.boolean().optional()
1915
+ import { z as z5 } from "zod";
1916
+ var wireConnectorSchema = z5.object({
1917
+ name: z5.string(),
1918
+ connectorId: z5.string(),
1919
+ displayName: z5.string().optional(),
1920
+ config: z5.record(z5.string(), z5.unknown()),
1921
+ syncIntervalSeconds: z5.number().optional(),
1922
+ enabled: z5.boolean().optional()
1475
1923
  });
1476
- var wireDashboardSchema = z3.object({
1477
- id: z3.string().optional(),
1478
- name: z3.string(),
1479
- slug: z3.string(),
1480
- config: z3.record(z3.string(), z3.unknown())
1924
+ var wireDashboardSchema = z5.object({
1925
+ id: z5.string().optional(),
1926
+ name: z5.string(),
1927
+ slug: z5.string(),
1928
+ config: z5.record(z5.string(), z5.unknown())
1481
1929
  });
1482
- var wireConfigSchema = z3.object({
1483
- connectors: z3.array(wireConnectorSchema).optional(),
1484
- dashboards: z3.array(wireDashboardSchema).optional()
1930
+ var wireConfigSchema = z5.object({
1931
+ connectors: z5.array(wireConnectorSchema).optional(),
1932
+ dashboards: z5.array(wireDashboardSchema).optional()
1485
1933
  });
1486
1934
  function toWireConfig(config) {
1487
1935
  return {
1488
- connectors: config.connectors.map(({ connector }) => ({
1489
- name: connector.id,
1490
- connectorId: connector.id,
1491
- displayName: connector.id,
1492
- config: connector.serializeConfig(),
1493
- syncIntervalSeconds: 300,
1494
- enabled: true
1936
+ connectors: config.connectors.map((entry) => ({
1937
+ name: entry.name,
1938
+ connectorId: entry.connectorId,
1939
+ displayName: entry.displayName ?? entry.name,
1940
+ config: entry.config,
1941
+ syncIntervalSeconds: entry.syncIntervalSeconds ?? 300,
1942
+ enabled: entry.enabled ?? true
1495
1943
  })),
1496
1944
  dashboards: Object.entries(config.dashboards).map(([id, dash]) => ({
1497
1945
  id,
@@ -1507,14 +1955,21 @@ export {
1507
1955
  EnvSecretsResolver,
1508
1956
  InMemoryStorage,
1509
1957
  aggFnSchema,
1958
+ computeConnectorBackfill,
1510
1959
  computeMetric,
1511
1960
  computeRetention,
1961
+ computeWidgetEtag,
1512
1962
  computedMetricSchema,
1963
+ connectorCategorySchema,
1964
+ connectorDocSchema,
1965
+ createDefaultConnectorLogger,
1513
1966
  defineConfig,
1514
1967
  defineConfigFields,
1515
1968
  defineConnector,
1969
+ defineConnectorDoc,
1516
1970
  defineDashboard,
1517
1971
  defineMetric,
1972
+ defineResources,
1518
1973
  distributionWidgetSchema,
1519
1974
  extractSecretNames,
1520
1975
  filterClauseSchema,
@@ -1522,12 +1977,19 @@ export {
1522
1977
  filterOperatorSchema,
1523
1978
  getWidgetSchema,
1524
1979
  groupBySchema,
1980
+ hashWidgetConfig,
1981
+ instantiateConnector,
1525
1982
  isSecret,
1526
1983
  isSyncActive,
1984
+ makeChunkedCursorGuard,
1985
+ noopConnectorLogger,
1527
1986
  paginateChunked,
1528
1987
  resolveSecrets,
1529
1988
  resolveWidget,
1989
+ schemasFromResources,
1530
1990
  secret,
1991
+ secretRefSchema,
1992
+ selectActivePhases,
1531
1993
  selectForDeletion,
1532
1994
  shapeSchema,
1533
1995
  statWidgetSchema,
@@ -1539,6 +2001,7 @@ export {
1539
2001
  wireConfigSchema,
1540
2002
  wireConnectorSchema,
1541
2003
  wireDashboardSchema,
1542
- withAbortSignal
2004
+ withAbortSignal,
2005
+ withSecretRef
1543
2006
  };
1544
2007
  //# sourceMappingURL=index.js.map