@dyrected/core 2.7.0 → 2.8.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.
Files changed (62) hide show
  1. package/dist/app-config-7G-3tF_I.d.cts +2259 -0
  2. package/dist/app-config-7G-3tF_I.d.ts +2259 -0
  3. package/dist/app-config-B3ByBjCN.d.cts +2222 -0
  4. package/dist/app-config-B3ByBjCN.d.ts +2222 -0
  5. package/dist/app-config-BIkjQWzU.d.cts +2218 -0
  6. package/dist/app-config-BIkjQWzU.d.ts +2218 -0
  7. package/dist/app-config-BfpeE3bF.d.cts +2100 -0
  8. package/dist/app-config-BfpeE3bF.d.ts +2100 -0
  9. package/dist/app-config-CCe-yq9q.d.cts +2222 -0
  10. package/dist/app-config-CCe-yq9q.d.ts +2222 -0
  11. package/dist/app-config-CPOs7THF.d.cts +2217 -0
  12. package/dist/app-config-CPOs7THF.d.ts +2217 -0
  13. package/dist/app-config-Ctp454zB.d.cts +2214 -0
  14. package/dist/app-config-Ctp454zB.d.ts +2214 -0
  15. package/dist/app-config-DJmNYgYh.d.cts +2305 -0
  16. package/dist/app-config-DJmNYgYh.d.ts +2305 -0
  17. package/dist/app-config-DO3laV58.d.cts +2397 -0
  18. package/dist/app-config-DO3laV58.d.ts +2397 -0
  19. package/dist/app-config-DZf_mmi9.d.cts +2211 -0
  20. package/dist/app-config-DZf_mmi9.d.ts +2211 -0
  21. package/dist/app-config-SkUoN5JU.d.cts +2192 -0
  22. package/dist/app-config-SkUoN5JU.d.ts +2192 -0
  23. package/dist/app-config-oz8H_MOX.d.cts +2224 -0
  24. package/dist/app-config-oz8H_MOX.d.ts +2224 -0
  25. package/dist/chunk-2UXLA4YN.js +3139 -0
  26. package/dist/chunk-3TRRTN6U.js +3441 -0
  27. package/dist/chunk-5U2WRCNH.js +3140 -0
  28. package/dist/chunk-5ZIA5COH.js +3414 -0
  29. package/dist/chunk-6TWQ6GTD.js +3480 -0
  30. package/dist/chunk-AKYYGMFS.js +3365 -0
  31. package/dist/chunk-AQO4HI2Y.js +3204 -0
  32. package/dist/chunk-DFBLNYUH.js +3402 -0
  33. package/dist/chunk-EJG6AMYB.js +3537 -0
  34. package/dist/chunk-FRSJJO27.js +519 -0
  35. package/dist/chunk-G43REXPX.js +3147 -0
  36. package/dist/chunk-GFKBPBJJ.js +3637 -0
  37. package/dist/chunk-KEKOEAX6.js +3139 -0
  38. package/dist/chunk-LFGSRQLX.js +3568 -0
  39. package/dist/chunk-NNZRYF3U.js +3537 -0
  40. package/dist/chunk-PRQ2RF5C.js +3485 -0
  41. package/dist/chunk-PYE2JSY4.js +3568 -0
  42. package/dist/chunk-PZMDK3MH.js +3203 -0
  43. package/dist/chunk-QCXLDKZO.js +3272 -0
  44. package/dist/chunk-SM32ND5M.js +3472 -0
  45. package/dist/chunk-UKUEKTAO.js +513 -0
  46. package/dist/chunk-VM43G3IG.js +506 -0
  47. package/dist/chunk-VWJKFZQU.js +3139 -0
  48. package/dist/chunk-XX7N64NL.js +3550 -0
  49. package/dist/chunk-YSSHLGQ6.js +3139 -0
  50. package/dist/chunk-Z7CFUYVP.js +3653 -0
  51. package/dist/index.cjs +1421 -82
  52. package/dist/index.d.cts +362 -3
  53. package/dist/index.d.ts +362 -3
  54. package/dist/index.js +119 -1
  55. package/dist/observability-CNM4QFE4.js +34 -0
  56. package/dist/observability-KCMTV7GO.js +34 -0
  57. package/dist/observability-L5BORBB2.js +34 -0
  58. package/dist/server.cjs +1879 -1247
  59. package/dist/server.d.cts +52 -8
  60. package/dist/server.d.ts +52 -8
  61. package/dist/server.js +449 -387
  62. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,13 +30,527 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
33
+ // src/observability.ts
34
+ var observability_exports = {};
35
+ __export(observability_exports, {
36
+ attachRequestMetrics: () => attachRequestMetrics,
37
+ bindObservabilityRuntime: () => bindObservabilityRuntime,
38
+ buildRequestLogger: () => buildRequestLogger,
39
+ captureRequestBody: () => captureRequestBody,
40
+ createObservabilityRuntime: () => createObservabilityRuntime,
41
+ createRequestSpan: () => createRequestSpan,
42
+ endRequestSpan: () => endRequestSpan,
43
+ getConfigLogger: () => getConfigLogger,
44
+ getObservabilityRuntime: () => getObservabilityRuntime,
45
+ getRequestLogger: () => getRequestLogger,
46
+ redactBody: () => redactBody,
47
+ redactHeaders: () => redactHeaders,
48
+ renderPrometheusMetrics: () => renderPrometheusMetrics,
49
+ resolveObservabilityConfig: () => resolveObservabilityConfig,
50
+ shouldSampleRequest: () => shouldSampleRequest
51
+ });
52
+ function resolveObservabilityConfig(config) {
53
+ const requestLogging = config?.requestLogging;
54
+ const sampling = config?.sampling;
55
+ const tracing = config?.tracing;
56
+ const metrics = config?.metrics;
57
+ return {
58
+ requestLogging: {
59
+ enabled: requestLogging?.enabled ?? true,
60
+ logBodies: requestLogging?.logBodies ?? false,
61
+ maxBodyBytes: requestLogging?.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES,
62
+ redactPaths: uniqueValues([
63
+ ...DEFAULT_REDACT_PATHS,
64
+ ...requestLogging?.redactPaths ?? []
65
+ ]),
66
+ includeHeaders: normalizeHeaderNames(requestLogging?.includeHeaders ?? []),
67
+ redactHeaders: uniqueValues(
68
+ normalizeHeaderNames([
69
+ ...DEFAULT_REDACT_HEADERS,
70
+ ...requestLogging?.redactHeaders ?? []
71
+ ])
72
+ )
73
+ },
74
+ sampling: {
75
+ successRate: clampRate(sampling?.successRate ?? DEFAULT_SUCCESS_SAMPLE_RATE),
76
+ traceSuccessRate: clampRate(
77
+ sampling?.traceSuccessRate ?? sampling?.successRate ?? DEFAULT_SUCCESS_SAMPLE_RATE
78
+ ),
79
+ bodySuccessRate: clampRate(
80
+ sampling?.bodySuccessRate ?? Math.min(DEFAULT_BODY_SAMPLE_RATE, sampling?.successRate ?? DEFAULT_SUCCESS_SAMPLE_RATE)
81
+ ),
82
+ alwaysKeep4xx: sampling?.alwaysKeep4xx ?? true,
83
+ alwaysKeep5xx: sampling?.alwaysKeep5xx ?? true
84
+ },
85
+ tracing: {
86
+ enabled: tracing?.enabled ?? false,
87
+ serviceName: tracing?.serviceName ?? "dyrected",
88
+ exporter: tracing?.exporter ?? "none",
89
+ headers: tracing?.headers ?? {},
90
+ endpoint: tracing?.endpoint
91
+ },
92
+ metrics: {
93
+ enabled: metrics?.enabled ?? false,
94
+ exporter: metrics?.exporter ?? "none",
95
+ endpoint: metrics?.endpoint,
96
+ path: metrics?.path ?? "/metrics"
97
+ },
98
+ transports: {
99
+ targets: config?.transports?.targets && config.transports.targets.length > 0 ? config.transports.targets : [{ type: "stdout" }]
100
+ }
101
+ };
102
+ }
103
+ function createObservabilityRuntime(config) {
104
+ const resolved = resolveObservabilityConfig(config.observability);
105
+ const logger = createRootLogger(config.logger, resolved);
106
+ const shutdownTasks = [];
107
+ const meterRuntime = resolved.metrics.enabled || resolved.metrics.exporter === "prometheus" ? createMeterProvider(resolved) : void 0;
108
+ const tracerProvider = resolved.tracing.enabled ? createTracerProvider(resolved) : void 0;
109
+ const tracer = tracerProvider?.getTracer("dyrected");
110
+ const meter = meterRuntime?.provider.getMeter("dyrected");
111
+ const metrics = meter ? {
112
+ requestCount: meter.createCounter("dyrected_http_requests_total", {
113
+ description: "Total HTTP requests served by Dyrected"
114
+ }),
115
+ authFailureCount: meter.createCounter("dyrected_auth_failures_total", {
116
+ description: "Authentication failures"
117
+ }),
118
+ uncaughtErrorCount: meter.createCounter("dyrected_uncaught_errors_total", {
119
+ description: "Uncaught application errors"
120
+ }),
121
+ auditWriteFailureCount: meter.createCounter("dyrected_audit_write_failures_total", {
122
+ description: "Audit log persistence failures"
123
+ }),
124
+ emailSendFailureCount: meter.createCounter("dyrected_email_send_failures_total", {
125
+ description: "Transactional email delivery failures"
126
+ }),
127
+ workflowHookFailureCount: meter.createCounter("dyrected_workflow_hook_failures_total", {
128
+ description: "Workflow hook failures isolated after successful writes"
129
+ }),
130
+ requestDuration: meter.createHistogram("dyrected_http_request_duration_ms", {
131
+ description: "HTTP request duration in milliseconds",
132
+ unit: "ms"
133
+ })
134
+ } : void 0;
135
+ if (meterRuntime) {
136
+ shutdownTasks.push(() => meterRuntime.provider.shutdown());
137
+ }
138
+ if (tracerProvider) {
139
+ shutdownTasks.push(() => tracerProvider.shutdown());
140
+ }
141
+ return {
142
+ logger,
143
+ config: resolved,
144
+ tracer,
145
+ metrics,
146
+ prometheusExporter: meterRuntime?.prometheusExporter,
147
+ shutdown: async () => {
148
+ for (const task of shutdownTasks) {
149
+ await task();
150
+ }
151
+ },
152
+ recordAuthFailure: (attributes) => metrics?.authFailureCount.add(1, attributes),
153
+ recordUncaughtError: (attributes) => metrics?.uncaughtErrorCount.add(1, attributes),
154
+ recordAuditWriteFailure: (attributes) => metrics?.auditWriteFailureCount.add(1, attributes),
155
+ recordEmailSendFailure: (attributes) => metrics?.emailSendFailureCount.add(1, attributes),
156
+ recordWorkflowHookFailure: (attributes) => metrics?.workflowHookFailureCount.add(1, attributes)
157
+ };
158
+ }
159
+ function bindObservabilityRuntime(config, runtime) {
160
+ runtimeByConfig.set(config, runtime);
161
+ }
162
+ function getObservabilityRuntime(config) {
163
+ if (!config || typeof config !== "object") return void 0;
164
+ return runtimeByConfig.get(config);
165
+ }
166
+ function createRootLogger(loggerConfig, observability) {
167
+ if (loggerConfig && !("options" in loggerConfig)) {
168
+ return loggerConfig;
169
+ }
170
+ const options = loggerConfig?.options ?? {};
171
+ const enabled = options.enabled ?? process.env.DISABLE_LOGGING !== "true";
172
+ const name = options.name ?? "dyrected";
173
+ if (loggerConfig?.destination) {
174
+ return (0, import_pino.default)(
175
+ {
176
+ ...options,
177
+ enabled,
178
+ name,
179
+ timestamp: options.timestamp ?? import_pino.stdTimeFunctions.isoTime
180
+ },
181
+ loggerConfig.destination
182
+ );
183
+ }
184
+ const streams = buildTransportStreams(observability);
185
+ const destination = streams.length === 1 ? streams[0] : (0, import_pino.multistream)(streams.map((stream) => ({ stream })));
186
+ return (0, import_pino.default)(
187
+ {
188
+ ...options,
189
+ enabled,
190
+ name,
191
+ timestamp: options.timestamp ?? import_pino.stdTimeFunctions.isoTime
192
+ },
193
+ destination
194
+ );
195
+ }
196
+ function buildTransportStreams(observability) {
197
+ const targets = observability.transports.targets;
198
+ if (targets.length === 0) {
199
+ return [defaultLoggerDestination()];
200
+ }
201
+ return targets.map((target) => {
202
+ switch (target.type) {
203
+ case "stdout":
204
+ return defaultLoggerDestination();
205
+ case "stderr":
206
+ return (0, import_pino.destination)(2);
207
+ case "file":
208
+ return (0, import_pino.destination)(target.path);
209
+ case "otlp":
210
+ return new OtlpLogWritable(target.endpoint, target.headers);
211
+ }
212
+ });
213
+ }
214
+ function defaultLoggerDestination() {
215
+ if (process.env.NODE_ENV !== "production" && typeof window === "undefined") {
216
+ try {
217
+ const req = globalThis.require;
218
+ if (req) {
219
+ const pinoPretty = req("pino-pretty");
220
+ return pinoPretty({
221
+ colorize: true,
222
+ ignore: "pid,hostname",
223
+ translateTime: "SYS:HH:MM:ss",
224
+ destination: 1,
225
+ sync: true
226
+ });
227
+ }
228
+ } catch {
229
+ }
230
+ }
231
+ return (0, import_pino.destination)(1);
232
+ }
233
+ function createTracerProvider(observability) {
234
+ const exporter = createTraceExporter(observability);
235
+ const provider = new import_sdk_trace_base.BasicTracerProvider({
236
+ resource: (0, import_resources.resourceFromAttributes)({
237
+ "service.name": observability.tracing.serviceName
238
+ }),
239
+ spanProcessors: exporter ? [new import_sdk_trace_base.BatchSpanProcessor(exporter)] : []
240
+ });
241
+ import_api.trace.setGlobalTracerProvider(provider);
242
+ return provider;
243
+ }
244
+ function createTraceExporter(observability) {
245
+ switch (observability.tracing.exporter) {
246
+ case "console":
247
+ return new import_sdk_trace_base.ConsoleSpanExporter();
248
+ case "otlp":
249
+ return new import_exporter_trace_otlp_http.OTLPTraceExporter({
250
+ url: observability.tracing.endpoint,
251
+ headers: observability.tracing.headers
252
+ });
253
+ default:
254
+ return void 0;
255
+ }
256
+ }
257
+ function createMeterProvider(observability) {
258
+ const readers = [];
259
+ let prometheusExporter;
260
+ switch (observability.metrics.exporter) {
261
+ case "otlp":
262
+ readers.push(
263
+ new import_sdk_metrics.PeriodicExportingMetricReader({
264
+ exporter: new import_exporter_metrics_otlp_http.OTLPMetricExporter({
265
+ url: observability.metrics.endpoint
266
+ })
267
+ })
268
+ );
269
+ break;
270
+ case "prometheus":
271
+ prometheusExporter = new import_exporter_prometheus.PrometheusExporter({
272
+ endpoint: observability.metrics.path,
273
+ preventServerStart: true
274
+ });
275
+ readers.push(prometheusExporter);
276
+ break;
277
+ }
278
+ const provider = new import_sdk_metrics.MeterProvider({
279
+ resource: (0, import_resources.resourceFromAttributes)({
280
+ "service.name": observability.tracing.serviceName
281
+ }),
282
+ readers
283
+ });
284
+ return { provider, prometheusExporter };
285
+ }
286
+ function getConfigLogger(config, component) {
287
+ const base = config?.logger && !("options" in config.logger) ? config.logger : fallbackLogger;
288
+ return base.child({ component });
289
+ }
290
+ function getRequestLogger(c, component) {
291
+ const base = c.get("logger") ?? fallbackLogger;
292
+ return component ? base.child({ component }) : base;
293
+ }
294
+ function buildRequestLogger(runtime, vars, requestId) {
295
+ const trace2 = vars.requestTrace;
296
+ return runtime.logger.child({
297
+ requestId,
298
+ siteId: vars.siteId,
299
+ workspaceId: vars.workspaceId,
300
+ traceId: trace2?.traceId,
301
+ spanId: trace2?.spanId
302
+ });
303
+ }
304
+ function shouldSampleRequest(requestId, statusCode, sampling, kind) {
305
+ if (statusCode >= 500 && sampling.alwaysKeep5xx) return true;
306
+ if (statusCode >= 400 && sampling.alwaysKeep4xx) return true;
307
+ const rate = kind === "trace" ? sampling.traceSuccessRate : kind === "body" ? sampling.bodySuccessRate : sampling.successRate;
308
+ return deterministicSample(requestId ?? "no-request-id", rate);
309
+ }
310
+ function redactHeaders(headers, observability) {
311
+ const includeHeaders = new Set(observability.requestLogging.includeHeaders);
312
+ const redactHeaders2 = new Set(observability.requestLogging.redactHeaders);
313
+ const output = {};
314
+ for (const [key, value] of Object.entries(headers)) {
315
+ const normalizedKey = key.toLowerCase();
316
+ if (includeHeaders.size > 0 && !includeHeaders.has(normalizedKey)) continue;
317
+ output[normalizedKey] = redactHeaders2.has(normalizedKey) ? REDACTED_VALUE : value;
318
+ }
319
+ return output;
320
+ }
321
+ async function captureRequestBody(request, observability) {
322
+ const contentType = request.headers.get("content-type") ?? void 0;
323
+ const contentLength = toNumber(request.headers.get("content-length"));
324
+ const jsonBody = contentType?.includes("application/json");
325
+ if (!jsonBody) {
326
+ return {
327
+ attempted: false,
328
+ contentType,
329
+ contentLength
330
+ };
331
+ }
332
+ try {
333
+ const text = await request.clone().text();
334
+ if (!text) {
335
+ return { attempted: true, contentType, contentLength };
336
+ }
337
+ const truncated = text.length > observability.requestLogging.maxBodyBytes;
338
+ const raw = truncated ? text.slice(0, observability.requestLogging.maxBodyBytes) : text;
339
+ try {
340
+ const parsed = JSON.parse(raw);
341
+ return {
342
+ attempted: true,
343
+ contentType,
344
+ contentLength: contentLength ?? text.length,
345
+ truncated,
346
+ body: redactBody(parsed, observability.requestLogging.redactPaths)
347
+ };
348
+ } catch {
349
+ return {
350
+ attempted: true,
351
+ contentType,
352
+ contentLength: contentLength ?? text.length,
353
+ truncated,
354
+ parseFailed: true
355
+ };
356
+ }
357
+ } catch {
358
+ return {
359
+ attempted: true,
360
+ contentType,
361
+ contentLength,
362
+ parseFailed: true
363
+ };
364
+ }
365
+ }
366
+ function redactBody(value, redactPaths) {
367
+ const cloned = deepClone(value);
368
+ for (const path of redactPaths) {
369
+ applyRedaction(cloned, path.split("."));
370
+ }
371
+ return cloned;
372
+ }
373
+ function applyRedaction(target, segments) {
374
+ if (!target || segments.length === 0) return;
375
+ const [segment, ...rest] = segments;
376
+ if (Array.isArray(target)) {
377
+ for (const item of target) {
378
+ if (segment === "*") {
379
+ applyRedaction(item, rest);
380
+ } else {
381
+ applyRedaction(item, segments);
382
+ }
383
+ }
384
+ return;
385
+ }
386
+ if (typeof target !== "object") return;
387
+ const record = target;
388
+ if (segment === "*") {
389
+ for (const value of Object.values(record)) {
390
+ applyRedaction(value, rest);
391
+ }
392
+ return;
393
+ }
394
+ if (!(segment in record)) return;
395
+ if (rest.length === 0) {
396
+ record[segment] = REDACTED_VALUE;
397
+ return;
398
+ }
399
+ applyRedaction(record[segment], rest);
400
+ }
401
+ function deepClone(value) {
402
+ if (typeof structuredClone === "function") {
403
+ return structuredClone(value);
404
+ }
405
+ return JSON.parse(JSON.stringify(value));
406
+ }
407
+ function createRequestSpan(runtime, name, attributes) {
408
+ if (!runtime.tracer) return void 0;
409
+ const span = runtime.tracer.startSpan(name, { attributes }, import_api.context.active());
410
+ const spanContext = span.spanContext();
411
+ return {
412
+ span,
413
+ traceId: spanContext.traceId,
414
+ spanId: spanContext.spanId,
415
+ sampled: spanContext.traceFlags === import_api.TraceFlags.SAMPLED
416
+ };
417
+ }
418
+ function endRequestSpan(requestTrace, statusCode, error) {
419
+ if (!requestTrace) return;
420
+ requestTrace.span.setAttribute("http.status_code", statusCode);
421
+ if (error) {
422
+ requestTrace.span.recordException(error instanceof Error ? error : new Error(String(error)));
423
+ }
424
+ requestTrace.span.end();
425
+ }
426
+ function attachRequestMetrics(runtime, args) {
427
+ runtime.metrics?.requestCount.add(1, {
428
+ method: args.method,
429
+ route: args.route,
430
+ status_class: `${Math.floor(args.statusCode / 100)}xx`
431
+ });
432
+ runtime.metrics?.requestDuration.record(args.durationMs, {
433
+ method: args.method,
434
+ route: args.route,
435
+ status_class: `${Math.floor(args.statusCode / 100)}xx`
436
+ });
437
+ }
438
+ async function renderPrometheusMetrics(exporter) {
439
+ let statusCode = 200;
440
+ const headers = {};
441
+ let body = "";
442
+ await new Promise((resolve) => {
443
+ const response = {
444
+ statusCode,
445
+ setHeader(name, value) {
446
+ headers[name.toLowerCase()] = value;
447
+ },
448
+ end(chunk) {
449
+ if (chunk) body += chunk;
450
+ statusCode = this.statusCode;
451
+ resolve();
452
+ }
453
+ };
454
+ exporter.getMetricsRequestHandler({}, response);
455
+ });
456
+ return { body, headers, statusCode };
457
+ }
458
+ function normalizeHeaderNames(headers) {
459
+ return headers.map((value) => value.toLowerCase());
460
+ }
461
+ function uniqueValues(values) {
462
+ return [...new Set(values)];
463
+ }
464
+ function clampRate(value) {
465
+ if (Number.isNaN(value)) return 0;
466
+ return Math.max(0, Math.min(1, value));
467
+ }
468
+ function deterministicSample(seed, rate) {
469
+ if (rate >= 1) return true;
470
+ if (rate <= 0) return false;
471
+ let hash = 0;
472
+ for (let index = 0; index < seed.length; index += 1) {
473
+ hash = hash * 31 + seed.charCodeAt(index) >>> 0;
474
+ }
475
+ return hash / 4294967295 <= rate;
476
+ }
477
+ function toNumber(value) {
478
+ if (!value) return void 0;
479
+ const parsed = Number(value);
480
+ return Number.isFinite(parsed) ? parsed : void 0;
481
+ }
482
+ var import_api, import_exporter_metrics_otlp_http, import_exporter_prometheus, import_exporter_trace_otlp_http, import_resources, import_sdk_metrics, import_sdk_trace_base, import_pino, import_node_stream, DEFAULT_REDACT_HEADERS, DEFAULT_REDACT_PATHS, DEFAULT_MAX_BODY_BYTES, DEFAULT_SUCCESS_SAMPLE_RATE, DEFAULT_BODY_SAMPLE_RATE, REDACTED_VALUE, runtimeByConfig, fallbackLogger, OtlpLogWritable;
483
+ var init_observability = __esm({
484
+ "src/observability.ts"() {
485
+ "use strict";
486
+ import_api = require("@opentelemetry/api");
487
+ import_exporter_metrics_otlp_http = require("@opentelemetry/exporter-metrics-otlp-http");
488
+ import_exporter_prometheus = require("@opentelemetry/exporter-prometheus");
489
+ import_exporter_trace_otlp_http = require("@opentelemetry/exporter-trace-otlp-http");
490
+ import_resources = require("@opentelemetry/resources");
491
+ import_sdk_metrics = require("@opentelemetry/sdk-metrics");
492
+ import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
493
+ import_pino = __toESM(require("pino"), 1);
494
+ import_node_stream = require("stream");
495
+ DEFAULT_REDACT_HEADERS = ["authorization", "cookie", "set-cookie", "x-api-key"];
496
+ DEFAULT_REDACT_PATHS = [
497
+ "password",
498
+ "currentPassword",
499
+ "newPassword",
500
+ "confirmPassword",
501
+ "token",
502
+ "refreshToken",
503
+ "accessToken",
504
+ "secret",
505
+ "apiKey",
506
+ "inviteToken",
507
+ "resetToken"
508
+ ];
509
+ DEFAULT_MAX_BODY_BYTES = 8192;
510
+ DEFAULT_SUCCESS_SAMPLE_RATE = 0.1;
511
+ DEFAULT_BODY_SAMPLE_RATE = 0.02;
512
+ REDACTED_VALUE = "[REDACTED]";
513
+ runtimeByConfig = /* @__PURE__ */ new WeakMap();
514
+ fallbackLogger = (0, import_pino.default)({
515
+ name: "dyrected",
516
+ enabled: process.env.DISABLE_LOGGING !== "true",
517
+ timestamp: import_pino.stdTimeFunctions.isoTime
518
+ });
519
+ OtlpLogWritable = class extends import_node_stream.Writable {
520
+ constructor(endpoint, headers = {}) {
521
+ super();
522
+ this.endpoint = endpoint;
523
+ this.headers = headers;
524
+ }
525
+ endpoint;
526
+ headers;
527
+ _write(chunk, _encoding, callback) {
528
+ const payload = chunk.toString();
529
+ void fetch(this.endpoint, {
530
+ method: "POST",
531
+ headers: {
532
+ "content-type": "application/json",
533
+ ...this.headers
534
+ },
535
+ body: JSON.stringify({ logs: payload.trim().split("\n").filter(Boolean) })
536
+ }).then(() => callback()).catch(
537
+ (error) => callback(error instanceof Error ? error : new Error(String(error)))
538
+ );
539
+ }
540
+ };
541
+ }
542
+ });
543
+
30
544
  // src/index.ts
31
545
  var index_exports = {};
32
546
  __export(index_exports, {
547
+ BUILTIN_JEXL_HELPERS: () => BUILTIN_JEXL_HELPERS,
33
548
  ConfigValidationError: () => ConfigValidationError,
34
549
  LIFECYCLE_EVENTS_COLLECTION: () => LIFECYCLE_EVENTS_COLLECTION,
35
550
  LIFECYCLE_EVENT_NAMES: () => LIFECYCLE_EVENT_NAMES,
36
551
  WORKFLOW_HISTORY_COLLECTION: () => WORKFLOW_HISTORY_COLLECTION,
552
+ abs: () => abs,
553
+ addDays: () => addDays,
37
554
  assertValidAdminConditionsInConfig: () => assertValidAdminConditionsInConfig,
38
555
  assertValidDeclarativeAccessInConfig: () => assertValidDeclarativeAccessInConfig,
39
556
  assertValidDeclarativeHookExpression: () => assertValidDeclarativeHookExpression,
@@ -43,13 +560,20 @@ __export(index_exports, {
43
560
  availableWorkflowTransitions: () => availableWorkflowTransitions,
44
561
  buildSqlOrderBy: () => buildSqlOrderBy,
45
562
  canViewWorkflowDraft: () => canViewWorkflowDraft,
563
+ capitalize: () => capitalize,
564
+ ceil: () => ceil,
565
+ clamp: () => clamp,
566
+ coalesce: () => coalesce,
46
567
  collectAdminConditionValidationIssues: () => collectAdminConditionValidationIssues,
47
568
  collectConfigDiagnostics: () => collectConfigDiagnostics,
48
569
  collectDeclarativeAccessValidationIssues: () => collectDeclarativeAccessValidationIssues,
49
570
  collectDeclarativeHookValidationIssues: () => collectDeclarativeHookValidationIssues,
50
571
  collectPreviewUrlValidationIssues: () => collectPreviewUrlValidationIssues,
572
+ compact: () => compact,
573
+ count: () => count,
51
574
  createLifecycleEvent: () => createLifecycleEvent,
52
575
  createWorkflowDocument: () => createWorkflowDocument,
576
+ defaultValue: () => defaultValue,
53
577
  defineArrayField: () => defineArrayField,
54
578
  defineBlock: () => defineBlock,
55
579
  defineBlocksField: () => defineBlocksField,
@@ -79,33 +603,82 @@ __export(index_exports, {
79
603
  defineTextareaField: () => defineTextareaField,
80
604
  defineTimeField: () => defineTimeField,
81
605
  defineUrlField: () => defineUrlField,
606
+ diffDays: () => diffDays,
82
607
  dispatchLifecycleEvent: () => dispatchLifecycleEvent,
83
608
  dispatchPendingLifecycleEvents: () => dispatchPendingLifecycleEvents,
609
+ displayComputed: () => displayComputed,
610
+ displayCustom: () => displayCustom,
611
+ displayCustomComponent: () => displayCustomComponent,
612
+ displayDivider: () => displayDivider,
613
+ displayField: () => displayField,
614
+ displayGrid: () => displayGrid,
615
+ displayRepeat: () => displayRepeat,
616
+ displaySection: () => displaySection,
617
+ displayTab: () => displayTab,
618
+ displayTabs: () => displayTabs,
619
+ displayText: () => displayText,
620
+ endsWith: () => endsWith,
621
+ evaluateDetailComputed: () => evaluateDetailComputed,
622
+ evaluateJexl: () => evaluateJexl,
623
+ evaluateJexlSync: () => evaluateJexlSync,
84
624
  executeFieldAfterRead: () => executeFieldAfterRead,
85
625
  executeFieldBeforeChange: () => executeFieldBeforeChange,
626
+ first: () => first,
627
+ floor: () => floor,
86
628
  formatConfigDiagnostics: () => formatConfigDiagnostics,
629
+ formatDate: () => formatDate,
630
+ generateDefaultDetailSchema: () => generateDefaultDetailSchema,
87
631
  generateOpenApi: () => generateOpenApi,
632
+ get: () => get,
88
633
  getAdminAuthCollection: () => getAdminAuthCollection,
89
634
  getPublicAdminAuthConfig: () => getPublicAdminAuthConfig,
635
+ includes: () => includes,
90
636
  initializeWorkflowDocument: () => initializeWorkflowDocument,
91
637
  isConfigValidationError: () => isConfigValidationError,
638
+ isDetailItemVisible: () => isDetailItemVisible,
639
+ isEmpty: () => isEmpty,
640
+ isFuture: () => isFuture,
641
+ isPast: () => isPast,
642
+ join: () => join,
643
+ last: () => last,
644
+ length: () => length,
645
+ lower: () => lower,
92
646
  materializeWorkflowDocument: () => materializeWorkflowDocument,
647
+ max: () => max,
648
+ min: () => min,
93
649
  normalizeConfig: () => normalizeConfig,
650
+ normalizeDetailItem: () => normalizeDetailItem,
651
+ now: () => now,
94
652
  numericFieldNames: () => numericFieldNames,
95
653
  parseMongoWhere: () => parseMongoWhere,
96
654
  parseSort: () => parseSort,
97
655
  parseSqlWhere: () => parseSqlWhere,
656
+ pow: () => pow,
98
657
  publishedStateName: () => publishedStateName,
99
658
  publishingWorkflow: () => publishingWorkflow,
659
+ readingTime: () => readingTime,
660
+ registerJexlHelpers: () => registerJexlHelpers,
661
+ replace: () => replace,
100
662
  resolveAdminAuthCollection: () => resolveAdminAuthCollection,
663
+ round: () => round,
101
664
  runCollectionHooks: () => runCollectionHooks,
102
665
  saveWorkflowDraft: () => saveWorkflowDraft,
103
666
  simplePublishingWorkflow: () => simplePublishingWorkflow,
667
+ slugify: () => slugify,
668
+ sqrt: () => sqrt,
669
+ startsWith: () => startsWith,
670
+ sum: () => sum,
671
+ today: () => today,
104
672
  transitionWorkflow: () => transitionWorkflow,
673
+ trim: () => trim,
674
+ truncate: () => truncate,
675
+ unique: () => unique,
676
+ upper: () => upper,
105
677
  validateAdminConditionExpression: () => validateAdminConditionExpression,
106
678
  validateDeclarativeAccessExpression: () => validateDeclarativeAccessExpression,
107
679
  validateDeclarativeHookExpression: () => validateDeclarativeHookExpression,
108
680
  validatePreviewUrlExpression: () => validatePreviewUrlExpression,
681
+ wordCount: () => wordCount,
109
682
  workflowCapabilities: () => workflowCapabilities
110
683
  });
111
684
  module.exports = __toCommonJS(index_exports);
@@ -120,37 +693,10 @@ var LIFECYCLE_EVENT_NAMES = [
120
693
 
121
694
  // src/auth/token.ts
122
695
  var import_jose = require("jose");
123
- var import_node_util = require("util");
124
696
 
125
697
  // src/auth/sessions.ts
126
698
  var AUTH_SESSIONS_COLLECTION = "__auth_sessions";
127
699
 
128
- // src/observability.ts
129
- var import_api = require("@opentelemetry/api");
130
- var import_exporter_metrics_otlp_http = require("@opentelemetry/exporter-metrics-otlp-http");
131
- var import_exporter_prometheus = require("@opentelemetry/exporter-prometheus");
132
- var import_exporter_trace_otlp_http = require("@opentelemetry/exporter-trace-otlp-http");
133
- var import_resources = require("@opentelemetry/resources");
134
- var import_sdk_metrics = require("@opentelemetry/sdk-metrics");
135
- var import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
136
- var import_pino = __toESM(require("pino"), 1);
137
- var import_pino_pretty = __toESM(require("pino-pretty"), 1);
138
- var import_node_stream = require("stream");
139
- var runtimeByConfig = /* @__PURE__ */ new WeakMap();
140
- var fallbackLogger = (0, import_pino.default)({
141
- name: "dyrected",
142
- enabled: process.env.DISABLE_LOGGING !== "true",
143
- timestamp: import_pino.stdTimeFunctions.isoTime
144
- });
145
- function getObservabilityRuntime(config) {
146
- if (!config || typeof config !== "object") return void 0;
147
- return runtimeByConfig.get(config);
148
- }
149
- function getConfigLogger(config, component) {
150
- const base = config?.logger && !("options" in config.logger) ? config.logger : fallbackLogger;
151
- return base.child({ component });
152
- }
153
-
154
700
  // src/workflows.ts
155
701
  var WORKFLOW_HISTORY_COLLECTION = "__workflow_history";
156
702
  var LIFECYCLE_EVENTS_COLLECTION = "__lifecycle_events";
@@ -168,10 +714,36 @@ function definePublishingWorkflow(options = {}) {
168
714
  { name: "published", label: "Published", color: "success", published: true }
169
715
  ],
170
716
  transitions: [
171
- { name: "submit", label: "Submit for review", from: "draft", to: "in_review", requiredCapabilities: ["entry.submit"] },
172
- { name: "publish", label: "Publish", from: "in_review", to: "published", requiredCapabilities: ["entry.publish"] },
173
- { name: "reject", label: "Request changes", from: "in_review", to: "draft", requiredCapabilities: ["entry.publish"], requireComment: true },
174
- { name: "unpublish", label: "Unpublish", from: "published", to: "draft", requiredCapabilities: ["entry.unpublish"], unpublish: true }
717
+ {
718
+ name: "submit",
719
+ label: "Submit for review",
720
+ from: "draft",
721
+ to: "in_review",
722
+ requiredCapabilities: ["entry.submit"]
723
+ },
724
+ {
725
+ name: "publish",
726
+ label: "Publish",
727
+ from: "in_review",
728
+ to: "published",
729
+ requiredCapabilities: ["entry.publish"]
730
+ },
731
+ {
732
+ name: "reject",
733
+ label: "Request changes",
734
+ from: "in_review",
735
+ to: "draft",
736
+ requiredCapabilities: ["entry.publish"],
737
+ requireComment: true
738
+ },
739
+ {
740
+ name: "unpublish",
741
+ label: "Unpublish",
742
+ from: "published",
743
+ to: "draft",
744
+ requiredCapabilities: ["entry.unpublish"],
745
+ unpublish: true
746
+ }
175
747
  ],
176
748
  roles: [
177
749
  ...editors.map((role) => ({ role, capabilities: [...EDITOR_CAPABILITIES] })),
@@ -191,7 +763,7 @@ function simplePublishingWorkflow() {
191
763
  { name: "published", label: "Published", color: "success", published: true }
192
764
  ],
193
765
  transitions: [
194
- { name: "publish", label: "Publish", from: "draft", to: "published" },
766
+ { name: "publish", label: "Publish", from: ["draft", "published"], to: "published" },
195
767
  { name: "unpublish", label: "Unpublish", from: "published", to: "draft", unpublish: true }
196
768
  ]
197
769
  // Intentionally no `roles`: this lightweight workflow (synthesized from
@@ -298,7 +870,11 @@ async function dispatchLifecycleEvent(config, event) {
298
870
  const retryDelayMs = config.events.retryDelayMs ?? 1e3;
299
871
  const attempts = event.attempts + 1;
300
872
  try {
301
- await db.update({ collection: LIFECYCLE_EVENTS_COLLECTION, id: event.id, data: { status: "processing", attempts } });
873
+ await db.update({
874
+ collection: LIFECYCLE_EVENTS_COLLECTION,
875
+ id: event.id,
876
+ data: { status: "processing", attempts }
877
+ });
302
878
  for (const handler of config.events.handlers) await handler({ ...event, status: "processing", attempts });
303
879
  await db.update({
304
880
  collection: LIFECYCLE_EVENTS_COLLECTION,
@@ -328,9 +904,9 @@ async function dispatchPendingLifecycleEvents(config, limit = 50) {
328
904
  sort: "occurredAt",
329
905
  limit
330
906
  });
331
- const now = Date.now();
907
+ const now2 = Date.now();
332
908
  const due = result.docs.filter(
333
- (doc) => Number(doc.attempts ?? 0) < maxAttempts && (!doc.nextAttemptAt || new Date(doc.nextAttemptAt).getTime() <= now)
909
+ (doc) => Number(doc.attempts ?? 0) < maxAttempts && (!doc.nextAttemptAt || new Date(doc.nextAttemptAt).getTime() <= now2)
334
910
  );
335
911
  for (const event of due) await dispatchLifecycleEvent(config, event);
336
912
  return due.length;
@@ -405,12 +981,16 @@ async function transitionWorkflow(args) {
405
981
  const meta = original.__workflow;
406
982
  if (!meta) throw Object.assign(new Error("Entry has no workflow metadata"), { statusCode: 409 });
407
983
  if (expectedRevision !== void 0 && expectedRevision !== meta.revision) {
408
- throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), { statusCode: 409 });
984
+ throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), {
985
+ statusCode: 409
986
+ });
409
987
  }
410
988
  const transition = workflow.transitions.find((item) => item.name === transitionName);
411
989
  const fromStates = transition ? Array.isArray(transition.from) ? transition.from : [transition.from] : [];
412
990
  if (!transition || !fromStates.includes(meta.state)) {
413
- throw Object.assign(new Error(`Transition "${transitionName}" is not valid from "${meta.state}".`), { statusCode: 409 });
991
+ throw Object.assign(new Error(`Transition "${transitionName}" is not valid from "${meta.state}".`), {
992
+ statusCode: 409
993
+ });
414
994
  }
415
995
  if (!availableWorkflowTransitions(workflow, meta.state, user).some((item) => item.name === transition.name)) {
416
996
  throw Object.assign(new Error(`You do not have permission to perform "${transition.label}".`), { statusCode: 403 });
@@ -420,32 +1000,52 @@ async function transitionWorkflow(args) {
420
1000
  }
421
1001
  const hookContext = { transition, from: meta.state, to: transition.to, doc: original, user, comment, req, db };
422
1002
  for (const hook of workflow.hooks?.beforeTransition ?? []) await hook(hookContext);
423
- const events = [createLifecycleEvent({
424
- name: "workflow.transitioned",
425
- collection: collection.slug,
426
- documentId: id,
427
- actorId: user?.sub,
428
- payload: { transition: transition.name, from: meta.state, to: transition.to, revision: meta.revision, comment }
429
- })];
1003
+ const events = [
1004
+ createLifecycleEvent({
1005
+ name: "workflow.transitioned",
1006
+ collection: collection.slug,
1007
+ documentId: id,
1008
+ actorId: user?.sub,
1009
+ payload: { transition: transition.name, from: meta.state, to: transition.to, revision: meta.revision, comment }
1010
+ })
1011
+ ];
430
1012
  const targetState = workflow.states.find((state) => state.name === transition.to);
431
1013
  if (targetState.published) {
432
- events.push(createLifecycleEvent({ name: "entry.published", collection: collection.slug, documentId: id, actorId: user?.sub, payload: { revision: meta.revision } }));
1014
+ events.push(
1015
+ createLifecycleEvent({
1016
+ name: "entry.published",
1017
+ collection: collection.slug,
1018
+ documentId: id,
1019
+ actorId: user?.sub,
1020
+ payload: { revision: meta.revision }
1021
+ })
1022
+ );
433
1023
  } else if (transition.unpublish) {
434
- events.push(createLifecycleEvent({ name: "entry.unpublished", collection: collection.slug, documentId: id, actorId: user?.sub, payload: { revision: meta.publishedRevision } }));
1024
+ events.push(
1025
+ createLifecycleEvent({
1026
+ name: "entry.unpublished",
1027
+ collection: collection.slug,
1028
+ documentId: id,
1029
+ actorId: user?.sub,
1030
+ payload: { revision: meta.publishedRevision }
1031
+ })
1032
+ );
435
1033
  }
436
1034
  const updated = await db.transaction(async (tx) => {
437
1035
  const locked = await tx.findOne({ collection: collection.slug, id });
438
1036
  if (!locked) throw Object.assign(new Error("Not Found"), { statusCode: 404 });
439
1037
  const lockedMeta = locked.__workflow;
440
1038
  if (lockedMeta.revision !== meta.revision || lockedMeta.state !== meta.state || expectedRevision !== void 0 && lockedMeta.revision !== expectedRevision) {
441
- throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), { statusCode: 409 });
1039
+ throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), {
1040
+ statusCode: 409
1041
+ });
442
1042
  }
443
- const now = (/* @__PURE__ */ new Date()).toISOString();
1043
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
444
1044
  const { __published: _published, __workflow: _workflow, id: _id, ...working } = locked;
445
1045
  const nextMeta = {
446
1046
  ...lockedMeta,
447
1047
  state: transition.to,
448
- ...targetState.published ? { publishedRevision: lockedMeta.revision, publishedAt: now, publishedBy: user?.sub } : {},
1048
+ ...targetState.published ? { publishedRevision: lockedMeta.revision, publishedAt: now2, publishedBy: user?.sub } : {},
449
1049
  ...transition.unpublish ? { publishedRevision: void 0, publishedAt: void 0, publishedBy: void 0 } : {}
450
1050
  };
451
1051
  const data = { __workflow: nextMeta };
@@ -454,11 +1054,36 @@ async function transitionWorkflow(args) {
454
1054
  const next = await tx.update({ collection: collection.slug, id, data });
455
1055
  await tx.create({
456
1056
  collection: WORKFLOW_HISTORY_COLLECTION,
457
- data: { collection: collection.slug, documentId: id, transition: transition.name, from: lockedMeta.state, to: transition.to, revision: lockedMeta.revision, comment: comment ?? null, actorId: user?.sub ?? null, createdAt: now }
1057
+ data: {
1058
+ collection: collection.slug,
1059
+ documentId: id,
1060
+ transition: transition.name,
1061
+ from: lockedMeta.state,
1062
+ to: transition.to,
1063
+ revision: lockedMeta.revision,
1064
+ comment: comment ?? null,
1065
+ actorId: user?.sub ?? null,
1066
+ createdAt: now2
1067
+ }
458
1068
  });
459
1069
  for (const event of events) await persistEvent(tx, event);
460
1070
  if (collection.audit) {
461
- await tx.create({ collection: "__audit", data: { collection: collection.slug, documentId: id, operation: "workflow.transition", user: user?.sub ?? null, timestamp: now, changes: JSON.stringify({ transition: transition.name, from: lockedMeta.state, to: transition.to, revision: lockedMeta.revision }) } });
1071
+ await tx.create({
1072
+ collection: "__audit",
1073
+ data: {
1074
+ collection: collection.slug,
1075
+ documentId: id,
1076
+ operation: "workflow.transition",
1077
+ user: user?.sub ?? null,
1078
+ timestamp: now2,
1079
+ changes: JSON.stringify({
1080
+ transition: transition.name,
1081
+ from: lockedMeta.state,
1082
+ to: transition.to,
1083
+ revision: lockedMeta.revision
1084
+ })
1085
+ }
1086
+ });
462
1087
  }
463
1088
  return next;
464
1089
  });
@@ -467,17 +1092,22 @@ async function transitionWorkflow(args) {
467
1092
  try {
468
1093
  await hook({ ...hookContext, doc: updated, event: events[0] });
469
1094
  } catch (error) {
470
- getObservabilityRuntime(config)?.recordWorkflowHookFailure({
471
- collection: collection.slug,
472
- transition: transition.name
473
- });
474
- getConfigLogger(config, "workflow").error({
475
- err: error,
476
- msg: "afterTransition hook failed",
477
- collection: collection.slug,
478
- transition: transition.name,
479
- documentId: id
480
- });
1095
+ try {
1096
+ const { getConfigLogger: getConfigLogger2, getObservabilityRuntime: getObservabilityRuntime2 } = await Promise.resolve().then(() => (init_observability(), observability_exports));
1097
+ getObservabilityRuntime2(config)?.recordWorkflowHookFailure({
1098
+ collection: collection.slug,
1099
+ transition: transition.name
1100
+ });
1101
+ getConfigLogger2(config, "workflow").error({
1102
+ err: error,
1103
+ msg: "afterTransition hook failed",
1104
+ collection: collection.slug,
1105
+ transition: transition.name,
1106
+ documentId: id
1107
+ });
1108
+ } catch {
1109
+ console.error("afterTransition hook failed:", error);
1110
+ }
481
1111
  }
482
1112
  }
483
1113
  return updated;
@@ -611,25 +1241,370 @@ function normalizeSchemaFragment(fragment) {
611
1241
  }
612
1242
 
613
1243
  // src/utils/declarative-hooks.ts
1244
+ var import_jexl2 = __toESM(require("jexl"), 1);
1245
+
1246
+ // src/utils/jexl-helpers.ts
614
1247
  var import_jexl = __toESM(require("jexl"), 1);
1248
+ function slugify(val) {
1249
+ if (val === void 0 || val === null) return "";
1250
+ return String(val).normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
1251
+ }
1252
+ function lower(val) {
1253
+ if (val === void 0 || val === null) return "";
1254
+ return String(val).toLowerCase();
1255
+ }
1256
+ function upper(val) {
1257
+ if (val === void 0 || val === null) return "";
1258
+ return String(val).toUpperCase();
1259
+ }
1260
+ function trim(val) {
1261
+ if (val === void 0 || val === null) return "";
1262
+ return String(val).trim();
1263
+ }
1264
+ function capitalize(val) {
1265
+ if (val === void 0 || val === null) return "";
1266
+ const str = String(val).trim();
1267
+ if (!str) return "";
1268
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
1269
+ }
1270
+ function truncate(val, length2, ellipsis = "...") {
1271
+ if (val === void 0 || val === null) return "";
1272
+ const str = String(val);
1273
+ const maxLen = typeof length2 === "number" && length2 > 0 ? length2 : 100;
1274
+ if (str.length <= maxLen) return str;
1275
+ return str.slice(0, maxLen).trim() + String(ellipsis ?? "...");
1276
+ }
1277
+ function readingTime(val, wpm = 200) {
1278
+ if (val === void 0 || val === null) return 1;
1279
+ const str = String(val).trim();
1280
+ if (!str) return 1;
1281
+ const words = str.split(/\s+/).filter(Boolean).length;
1282
+ return Math.max(1, Math.ceil(words / (typeof wpm === "number" && wpm > 0 ? wpm : 200)));
1283
+ }
1284
+ function wordCount(val) {
1285
+ if (val === void 0 || val === null) return 0;
1286
+ const str = String(val).trim();
1287
+ if (!str) return 0;
1288
+ return str.split(/\s+/).filter(Boolean).length;
1289
+ }
1290
+ function replace(val, search, replacement) {
1291
+ if (val === void 0 || val === null) return "";
1292
+ return String(val).replace(new RegExp(String(search || ""), "g"), String(replacement ?? ""));
1293
+ }
1294
+ function startsWith(str, prefix) {
1295
+ if (str === void 0 || str === null) return false;
1296
+ return String(str).startsWith(String(prefix ?? ""));
1297
+ }
1298
+ function endsWith(str, suffix) {
1299
+ if (str === void 0 || str === null) return false;
1300
+ return String(str).endsWith(String(suffix ?? ""));
1301
+ }
1302
+ function now() {
1303
+ return (/* @__PURE__ */ new Date()).toISOString();
1304
+ }
1305
+ function today() {
1306
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1307
+ }
1308
+ function formatDate(val, style = "short") {
1309
+ if (!val) return "";
1310
+ const d = new Date(val);
1311
+ if (Number.isNaN(d.getTime())) return "";
1312
+ const styleStr = String(style).toLowerCase();
1313
+ if (styleStr === "iso" || styleStr === "date") return d.toISOString().slice(0, 10);
1314
+ if (styleStr === "datetime" || styleStr === "full") return d.toISOString();
1315
+ return d.toLocaleDateString("en-US", {
1316
+ year: "numeric",
1317
+ month: "short",
1318
+ day: "numeric"
1319
+ });
1320
+ }
1321
+ function addDays(val, days) {
1322
+ const d = val ? new Date(val) : /* @__PURE__ */ new Date();
1323
+ if (Number.isNaN(d.getTime())) return "";
1324
+ const daysNum = typeof days === "number" ? days : Number(days) || 0;
1325
+ d.setDate(d.getDate() + daysNum);
1326
+ return d.toISOString();
1327
+ }
1328
+ function diffDays(dateA, dateB) {
1329
+ const dA = dateA ? new Date(dateA) : /* @__PURE__ */ new Date();
1330
+ const dB = dateB ? new Date(dateB) : /* @__PURE__ */ new Date();
1331
+ if (Number.isNaN(dA.getTime()) || Number.isNaN(dB.getTime())) return 0;
1332
+ const diffMs = Math.abs(dA.getTime() - dB.getTime());
1333
+ return Math.floor(diffMs / (1e3 * 60 * 60 * 24));
1334
+ }
1335
+ function isPast(val) {
1336
+ if (!val) return false;
1337
+ const d = new Date(val);
1338
+ if (Number.isNaN(d.getTime())) return false;
1339
+ return d.getTime() < Date.now();
1340
+ }
1341
+ function isFuture(val) {
1342
+ if (!val) return false;
1343
+ const d = new Date(val);
1344
+ if (Number.isNaN(d.getTime())) return false;
1345
+ return d.getTime() > Date.now();
1346
+ }
1347
+ function includes(arrOrStr, item) {
1348
+ if (Array.isArray(arrOrStr)) {
1349
+ return arrOrStr.includes(item);
1350
+ }
1351
+ if (typeof arrOrStr === "string") {
1352
+ return arrOrStr.includes(String(item ?? ""));
1353
+ }
1354
+ return false;
1355
+ }
1356
+ function join(arr, separator = ", ") {
1357
+ if (!Array.isArray(arr)) return "";
1358
+ return arr.filter((item) => item !== void 0 && item !== null).join(String(separator));
1359
+ }
1360
+ function first(arr) {
1361
+ if (Array.isArray(arr)) return arr[0] ?? null;
1362
+ return null;
1363
+ }
1364
+ function last(arr) {
1365
+ if (Array.isArray(arr)) return arr[arr.length - 1] ?? null;
1366
+ return null;
1367
+ }
1368
+ function compact(arr) {
1369
+ if (!Array.isArray(arr)) return [];
1370
+ return arr.filter((item) => item !== void 0 && item !== null && item !== "");
1371
+ }
1372
+ function unique(arr) {
1373
+ if (!Array.isArray(arr)) return [];
1374
+ return Array.from(new Set(arr));
1375
+ }
1376
+ function length(val) {
1377
+ if (Array.isArray(val) || typeof val === "string") return val.length;
1378
+ if (val && typeof val === "object") return Object.keys(val).length;
1379
+ return 0;
1380
+ }
1381
+ function round(val, decimals = 0) {
1382
+ const num = Number(val);
1383
+ if (Number.isNaN(num)) return 0;
1384
+ const dec = typeof decimals === "number" ? decimals : Number(decimals) || 0;
1385
+ const factor = Math.pow(10, dec);
1386
+ return Math.round(num * factor) / factor;
1387
+ }
1388
+ function clamp(val, minVal, maxVal) {
1389
+ const num = Number(val) || 0;
1390
+ const min2 = Number(minVal) || 0;
1391
+ const max2 = Number(maxVal) || 0;
1392
+ return Math.min(Math.max(num, min2), max2);
1393
+ }
1394
+ function ceil(val) {
1395
+ return Math.ceil(Number(val) || 0);
1396
+ }
1397
+ function floor(val) {
1398
+ return Math.floor(Number(val) || 0);
1399
+ }
1400
+ function abs(val) {
1401
+ return Math.abs(Number(val) || 0);
1402
+ }
1403
+ function min(...args) {
1404
+ const nums = args.flat().map((n) => Number(n) || 0);
1405
+ return nums.length ? Math.min(...nums) : 0;
1406
+ }
1407
+ function max(...args) {
1408
+ const nums = args.flat().map((n) => Number(n) || 0);
1409
+ return nums.length ? Math.max(...nums) : 0;
1410
+ }
1411
+ function pow(base, exp) {
1412
+ return Math.pow(Number(base) || 0, Number(exp) || 0);
1413
+ }
1414
+ function sqrt(val) {
1415
+ return Math.sqrt(Number(val) || 0);
1416
+ }
1417
+ function sum(arr) {
1418
+ if (!Array.isArray(arr)) return Number(arr) || 0;
1419
+ return arr.reduce((acc, val) => acc + (Number(val) || 0), 0);
1420
+ }
1421
+ function count(val) {
1422
+ if (Array.isArray(val)) return val.length;
1423
+ if (typeof val === "string") return val.length;
1424
+ if (val !== void 0 && val !== null && val !== false) return 1;
1425
+ return 0;
1426
+ }
1427
+ function defaultValue(val, fallback) {
1428
+ if (val === void 0 || val === null || val === "") return fallback;
1429
+ return val;
1430
+ }
1431
+ function coalesce(...args) {
1432
+ for (const arg of args) {
1433
+ if (arg !== void 0 && arg !== null && arg !== "") return arg;
1434
+ }
1435
+ return null;
1436
+ }
1437
+ function isEmpty(val) {
1438
+ if (val === void 0 || val === null || val === "") return true;
1439
+ if (Array.isArray(val)) return val.length === 0;
1440
+ if (typeof val === "object") return Object.keys(val).length === 0;
1441
+ return false;
1442
+ }
1443
+ function get(obj, path, fallback = null) {
1444
+ if (obj === void 0 || obj === null || typeof obj !== "object") return fallback;
1445
+ if (typeof path !== "string") return fallback;
1446
+ const parts = path.split(".").filter(Boolean);
1447
+ let curr = obj;
1448
+ for (const part of parts) {
1449
+ if (curr === void 0 || curr === null || typeof curr !== "object") return fallback;
1450
+ curr = curr[part];
1451
+ }
1452
+ return curr === void 0 || curr === null ? fallback : curr;
1453
+ }
1454
+ function registerJexlHelpers(jexlInstance = import_jexl.default) {
1455
+ const helpers = {
1456
+ // Strings
1457
+ slugify,
1458
+ lower,
1459
+ upper,
1460
+ trim,
1461
+ capitalize,
1462
+ truncate,
1463
+ readingTime,
1464
+ wordCount,
1465
+ replace,
1466
+ startsWith,
1467
+ endsWith,
1468
+ // Dates
1469
+ now,
1470
+ today,
1471
+ formatDate,
1472
+ addDays,
1473
+ diffDays,
1474
+ isPast,
1475
+ isFuture,
1476
+ // Arrays
1477
+ includes,
1478
+ join,
1479
+ first,
1480
+ last,
1481
+ compact,
1482
+ unique,
1483
+ length,
1484
+ // Math
1485
+ round,
1486
+ clamp,
1487
+ ceil,
1488
+ floor,
1489
+ abs,
1490
+ min,
1491
+ max,
1492
+ pow,
1493
+ sqrt,
1494
+ sum,
1495
+ count,
1496
+ // Objects & Logical
1497
+ default: defaultValue,
1498
+ coalesce,
1499
+ isEmpty,
1500
+ get
1501
+ };
1502
+ for (const [name, fn] of Object.entries(helpers)) {
1503
+ try {
1504
+ jexlInstance.addFunction(name, fn);
1505
+ } catch {
1506
+ }
1507
+ try {
1508
+ jexlInstance.addTransform(name, fn);
1509
+ } catch {
1510
+ }
1511
+ }
1512
+ try {
1513
+ jexlInstance.addBinaryOp("and", 20, (left, right) => Boolean(left && right));
1514
+ jexlInstance.addBinaryOp("or", 10, (left, right) => Boolean(left || right));
1515
+ } catch {
1516
+ }
1517
+ }
1518
+ registerJexlHelpers(import_jexl.default);
1519
+ async function evaluateJexl(expression, context = {}) {
1520
+ if (!expression || typeof expression !== "string") return void 0;
1521
+ try {
1522
+ const evalContext = {
1523
+ null: null,
1524
+ undefined: void 0,
1525
+ ...context,
1526
+ math: Math,
1527
+ Math
1528
+ };
1529
+ return await import_jexl.default.eval(expression, evalContext);
1530
+ } catch (_err) {
1531
+ return void 0;
1532
+ }
1533
+ }
1534
+ function evaluateJexlSync(expression, context = {}) {
1535
+ if (!expression || typeof expression !== "string") return void 0;
1536
+ try {
1537
+ const evalContext = {
1538
+ null: null,
1539
+ undefined: void 0,
1540
+ ...context,
1541
+ math: Math,
1542
+ Math
1543
+ };
1544
+ return import_jexl.default.evalSync(expression, evalContext);
1545
+ } catch (_err) {
1546
+ return void 0;
1547
+ }
1548
+ }
1549
+ var BUILTIN_JEXL_HELPERS = [
1550
+ // Strings
1551
+ "slugify",
1552
+ "lower",
1553
+ "upper",
1554
+ "trim",
1555
+ "capitalize",
1556
+ "truncate",
1557
+ "readingTime",
1558
+ "wordCount",
1559
+ "replace",
1560
+ "startsWith",
1561
+ "endsWith",
1562
+ // Dates
1563
+ "now",
1564
+ "today",
1565
+ "formatDate",
1566
+ "addDays",
1567
+ "diffDays",
1568
+ "isPast",
1569
+ "isFuture",
1570
+ // Arrays
1571
+ "includes",
1572
+ "join",
1573
+ "first",
1574
+ "last",
1575
+ "compact",
1576
+ "unique",
1577
+ "length",
1578
+ // Math
1579
+ "round",
1580
+ "clamp",
1581
+ // Objects & Logical
1582
+ "default",
1583
+ "coalesce",
1584
+ "isEmpty",
1585
+ "get"
1586
+ ];
1587
+
1588
+ // src/utils/declarative-hooks.ts
1589
+ registerJexlHelpers(import_jexl2.default);
615
1590
  var SURFACE_ROOTS = {
616
- "collection.beforeRead": ["req", "user", "query"],
617
- "collection.afterRead": ["req", "user", "doc"],
618
- "collection.beforeChange": ["req", "user", "data", "doc", "operation"],
619
- "global.beforeRead": ["req", "user", "query"],
620
- "global.afterRead": ["req", "user", "doc"],
621
- "global.beforeChange": ["req", "user", "data", "doc", "operation"],
622
- "field.beforeChange": ["value", "data", "originalDoc", "user"],
623
- "field.admin.onChange": ["value", "siblingData", "data"]
1591
+ "collection.beforeRead": ["req", "user", "query", ...BUILTIN_JEXL_HELPERS],
1592
+ "collection.afterRead": ["req", "user", "doc", ...BUILTIN_JEXL_HELPERS],
1593
+ "collection.beforeChange": ["req", "user", "data", "doc", "operation", ...BUILTIN_JEXL_HELPERS],
1594
+ "global.beforeRead": ["req", "user", "query", ...BUILTIN_JEXL_HELPERS],
1595
+ "global.afterRead": ["req", "user", "doc", ...BUILTIN_JEXL_HELPERS],
1596
+ "global.beforeChange": ["req", "user", "data", "doc", "operation", ...BUILTIN_JEXL_HELPERS],
1597
+ "field.beforeChange": ["value", "data", "originalDoc", "user", ...BUILTIN_JEXL_HELPERS],
1598
+ "field.admin.onChange": ["value", "siblingData", "data", ...BUILTIN_JEXL_HELPERS]
624
1599
  };
625
- var ACCESS_ROOTS = ["user", "req", "doc", "data", "id", "config"];
626
- var ADMIN_CONDITION_RESERVED_ROOTS = ["data", "siblingData", "user", "id"];
627
- var PREVIEW_URL_RESERVED_ROOTS = ["siteUrl", "id"];
1600
+ var ACCESS_ROOTS = ["user", "req", "doc", "data", "id", "config", ...BUILTIN_JEXL_HELPERS];
1601
+ var ADMIN_CONDITION_RESERVED_ROOTS = ["data", "siblingData", "user", "id", ...BUILTIN_JEXL_HELPERS];
1602
+ var PREVIEW_URL_RESERVED_ROOTS = ["siteUrl", "id", ...BUILTIN_JEXL_HELPERS];
628
1603
  var COMPILE_CACHE = /* @__PURE__ */ new Map();
629
1604
  function compileExpression(expression) {
630
1605
  const cached = COMPILE_CACHE.get(expression);
631
1606
  if (cached) return cached;
632
- const compiled = import_jexl.default.compile(expression);
1607
+ const compiled = import_jexl2.default.compile(expression);
633
1608
  const next = { ast: compiled._ast };
634
1609
  COMPILE_CACHE.set(expression, next);
635
1610
  return next;
@@ -1152,13 +2127,16 @@ function collectConfigDiagnostics(config) {
1152
2127
  function formatConfigDiagnostics(source, diagnostics, options = {}) {
1153
2128
  const color = options.color ?? false;
1154
2129
  const header = `${colorize("Config validation failed", 31, color)} in ${colorize(source, 36, color)}`;
2130
+ const docLink = `${colorize(" Learn how to write declarative rules:", 90, color)} ${colorize("https://docs.dyrected.com/docs/editor-experience/form-and-field-hooks", 34, color)}`;
1155
2131
  const lines = diagnostics.flatMap((issue, index) => [
1156
2132
  `${colorize(`${index + 1}.`, 31, color)} ${colorize(issue.path, 1, color)}`,
1157
2133
  ` ${issue.message}`,
1158
2134
  ` ${colorize("surface:", 90, color)} ${issue.surface}`
1159
2135
  ]);
1160
2136
  return `${header}
1161
- ${lines.join("\n")}`;
2137
+ ${lines.join("\n")}
2138
+
2139
+ ${docLink}`;
1162
2140
  }
1163
2141
  var ConfigValidationError = class extends Error {
1164
2142
  diagnostics;
@@ -1728,7 +2706,7 @@ function buildSqlOrderBy(sort, existingColumns, numericFields, dialect) {
1728
2706
  }
1729
2707
 
1730
2708
  // src/utils/hooks.ts
1731
- var import_jexl2 = __toESM(require("jexl"), 1);
2709
+ var import_jexl3 = __toESM(require("jexl"), 1);
1732
2710
  async function runCollectionHooks(hooks, args, options = {}) {
1733
2711
  if (!hooks || hooks.length === 0) {
1734
2712
  return args.data ?? args.doc ?? args.query ?? void 0;
@@ -1749,7 +2727,7 @@ async function runCollectionHooks(hooks, args, options = {}) {
1749
2727
  options.path ?? options.surface
1750
2728
  );
1751
2729
  }
1752
- return import_jexl2.default.eval(hook, hookArgs);
2730
+ return import_jexl3.default.eval(hook, hookArgs);
1753
2731
  })() : await hook(hookArgs);
1754
2732
  if (result !== void 0) {
1755
2733
  if (typeof hook === "string" && options.surface) {
@@ -1766,10 +2744,7 @@ async function runCollectionHooks(hooks, args, options = {}) {
1766
2744
  }
1767
2745
  } catch (err) {
1768
2746
  if (options.isolated) {
1769
- getConfigLogger(void 0, "hooks").error({
1770
- err,
1771
- msg: "Side-effect hook failed after a successful write"
1772
- });
2747
+ console.error("Side-effect hook failed after a successful write", err);
1773
2748
  } else {
1774
2749
  throw err;
1775
2750
  }
@@ -1801,7 +2776,7 @@ async function executeFieldBeforeChange(fields, data, originalDoc, user, db, pat
1801
2776
  "field.beforeChange",
1802
2777
  fieldPath
1803
2778
  );
1804
- return import_jexl2.default.eval(hook, hookArgs);
2779
+ return import_jexl3.default.eval(hook, hookArgs);
1805
2780
  })() : await hook(hookArgs);
1806
2781
  }
1807
2782
  result[field.name] = updatedValue;
@@ -2367,6 +3342,71 @@ function generateOpenApi(config) {
2367
3342
  responses: { 200: { description: "Deleted and failed document IDs" } }
2368
3343
  }
2369
3344
  };
3345
+ spec.paths[`${path}/aggregate`] = {
3346
+ post: {
3347
+ tags: [collectionTag],
3348
+ summary: `Aggregate ${labels.plural}`,
3349
+ description: `Compute statistical aggregations (count, sum, avg, min, max) across ${labels.plural}.`,
3350
+ requestBody: {
3351
+ required: true,
3352
+ content: {
3353
+ "application/json": {
3354
+ schema: {
3355
+ type: "object",
3356
+ additionalProperties: {
3357
+ type: "object",
3358
+ properties: {
3359
+ count: { type: "string", example: "*" },
3360
+ sum: { type: "string", description: "Field to sum" },
3361
+ avg: { type: "string", description: "Field to average" },
3362
+ min: { type: "string", description: "Field for minimum" },
3363
+ max: { type: "string", description: "Field for maximum" },
3364
+ cast: {
3365
+ type: "string",
3366
+ enum: [
3367
+ "number",
3368
+ "integer",
3369
+ "float",
3370
+ "boolean",
3371
+ "date",
3372
+ "string"
3373
+ ]
3374
+ },
3375
+ where: {
3376
+ type: "object",
3377
+ description: "Per-aggregate filter condition",
3378
+ additionalProperties: true
3379
+ }
3380
+ }
3381
+ }
3382
+ }
3383
+ }
3384
+ }
3385
+ },
3386
+ responses: {
3387
+ 200: {
3388
+ description: "Success",
3389
+ content: {
3390
+ "application/json": {
3391
+ schema: {
3392
+ type: "object",
3393
+ additionalProperties: {
3394
+ type: "number",
3395
+ nullable: true
3396
+ }
3397
+ }
3398
+ }
3399
+ }
3400
+ },
3401
+ 400: {
3402
+ description: "Bad Request - Invalid aggregate input"
3403
+ },
3404
+ 403: {
3405
+ description: "Forbidden - Access denied by read rule"
3406
+ }
3407
+ }
3408
+ }
3409
+ };
2370
3410
  if (collection.audit) {
2371
3411
  spec.paths[`${path}/__audit`] = {
2372
3412
  get: {
@@ -2894,6 +3934,246 @@ function fieldToSchema(field) {
2894
3934
  return schema;
2895
3935
  }
2896
3936
 
3937
+ // src/detail.ts
3938
+ function normalizeDetailItem(item) {
3939
+ if (typeof item === "string") {
3940
+ return {
3941
+ type: "field",
3942
+ field: item
3943
+ };
3944
+ }
3945
+ return item;
3946
+ }
3947
+ function displaySection(title, items, options) {
3948
+ return {
3949
+ type: "section",
3950
+ title,
3951
+ items,
3952
+ options
3953
+ };
3954
+ }
3955
+ function displayTab(label, items, options) {
3956
+ return {
3957
+ type: "tab",
3958
+ label,
3959
+ items,
3960
+ options
3961
+ };
3962
+ }
3963
+ function displayTabs(tabs, options) {
3964
+ return {
3965
+ type: "tabs",
3966
+ tabs,
3967
+ options
3968
+ };
3969
+ }
3970
+ function displayGrid(columns, items, options) {
3971
+ return {
3972
+ type: "grid",
3973
+ columns,
3974
+ items,
3975
+ options
3976
+ };
3977
+ }
3978
+ function displayField(fieldName, options) {
3979
+ return {
3980
+ type: "field",
3981
+ field: fieldName,
3982
+ options
3983
+ };
3984
+ }
3985
+ function displayRepeat(fieldName, items, options) {
3986
+ return {
3987
+ type: "repeat",
3988
+ field: fieldName,
3989
+ items,
3990
+ options
3991
+ };
3992
+ }
3993
+ function displayComputed(label, expressionOrOptionsOrHandler, options) {
3994
+ let expression;
3995
+ let handler;
3996
+ let finalOptions = options;
3997
+ if (typeof expressionOrOptionsOrHandler === "string") {
3998
+ expression = expressionOrOptionsOrHandler;
3999
+ } else if (typeof expressionOrOptionsOrHandler === "function") {
4000
+ handler = expressionOrOptionsOrHandler;
4001
+ } else if (expressionOrOptionsOrHandler && typeof expressionOrOptionsOrHandler === "object") {
4002
+ expression = expressionOrOptionsOrHandler.expression;
4003
+ handler = expressionOrOptionsOrHandler.handler;
4004
+ finalOptions = {
4005
+ ...expressionOrOptionsOrHandler,
4006
+ ...options
4007
+ };
4008
+ }
4009
+ const id = finalOptions?.id || label.toLowerCase().trim().replace(/[^a-z0-9]+/g, "_");
4010
+ return {
4011
+ type: "computed",
4012
+ id,
4013
+ label,
4014
+ expression,
4015
+ handler,
4016
+ options: finalOptions
4017
+ };
4018
+ }
4019
+ function displayDivider(options) {
4020
+ return {
4021
+ type: "divider",
4022
+ options
4023
+ };
4024
+ }
4025
+ function displayText(content, options) {
4026
+ return {
4027
+ type: "text",
4028
+ content,
4029
+ options
4030
+ };
4031
+ }
4032
+ function displayCustom(name, options) {
4033
+ return {
4034
+ type: "custom",
4035
+ name,
4036
+ options
4037
+ };
4038
+ }
4039
+ function displayCustomComponent(name, options) {
4040
+ return displayCustom(name, options);
4041
+ }
4042
+ async function evaluateDetailComputed(detail, doc, user, db) {
4043
+ if (!doc || !detail || !Array.isArray(detail)) {
4044
+ return doc;
4045
+ }
4046
+ const computedItems = [];
4047
+ const extractComputed = (items) => {
4048
+ for (const rawItem of items) {
4049
+ if (!rawItem) continue;
4050
+ const item = normalizeDetailItem(rawItem);
4051
+ if (item.type === "computed") {
4052
+ const id = item.id || item.label?.toLowerCase().replace(/[^a-z0-9]+/g, "_");
4053
+ if (id) {
4054
+ computedItems.push({
4055
+ id,
4056
+ label: item.label,
4057
+ expression: item.expression,
4058
+ handler: item.handler
4059
+ });
4060
+ }
4061
+ } else if (item.type === "section" || item.type === "grid" || item.type === "repeat") {
4062
+ if (item.items && Array.isArray(item.items)) {
4063
+ extractComputed(item.items);
4064
+ }
4065
+ } else if (item.type === "tabs" && item.tabs && Array.isArray(item.tabs)) {
4066
+ for (const tab of item.tabs) {
4067
+ if (tab.items) extractComputed(tab.items);
4068
+ }
4069
+ }
4070
+ }
4071
+ };
4072
+ extractComputed(detail);
4073
+ if (computedItems.length === 0) return doc;
4074
+ const evaluated = {};
4075
+ for (const item of computedItems) {
4076
+ try {
4077
+ if (typeof item.handler === "function") {
4078
+ evaluated[item.id] = await item.handler({ doc, user, db });
4079
+ } else if (item.expression) {
4080
+ evaluated[item.id] = await evaluateJexl(item.expression, { doc, user });
4081
+ }
4082
+ } catch (_err) {
4083
+ evaluated[item.id] = null;
4084
+ }
4085
+ }
4086
+ return {
4087
+ ...doc,
4088
+ _meta: {
4089
+ ...doc._meta || {},
4090
+ computed: evaluated
4091
+ }
4092
+ };
4093
+ }
4094
+ var SENSITIVE_DETAIL_FIELDS = /* @__PURE__ */ new Set([
4095
+ "password",
4096
+ "salt",
4097
+ "resetPasswordToken",
4098
+ "resetPasswordExpires",
4099
+ "loginAttempts",
4100
+ "lockUntil",
4101
+ "apiKey",
4102
+ "secret"
4103
+ ]);
4104
+ function generateDefaultDetailSchema(schema) {
4105
+ const fields = schema.fields || [];
4106
+ const sidebarFieldTypes = /* @__PURE__ */ new Set([
4107
+ "select",
4108
+ "radio",
4109
+ "boolean",
4110
+ "date",
4111
+ "datetime",
4112
+ "relationship"
4113
+ ]);
4114
+ const mainFields = [];
4115
+ const sidebarFields = [];
4116
+ for (const field of fields) {
4117
+ if (!field.name) continue;
4118
+ if (SENSITIVE_DETAIL_FIELDS.has(field.name)) continue;
4119
+ if (field.hidden) continue;
4120
+ if (field.admin?.hidden) continue;
4121
+ const isSidebar = sidebarFieldTypes.has(field.type) && field.type !== "relationship" && !field.hasMany;
4122
+ if (isSidebar && sidebarFields.length < 5) {
4123
+ sidebarFields.push(
4124
+ displayField(field.name, {
4125
+ span: 12,
4126
+ display: field.type === "select" || field.type === "radio" ? "badge" : void 0
4127
+ })
4128
+ );
4129
+ } else {
4130
+ const span = field.type === "richText" || field.type === "blocks" || field.type === "textarea" || field.type === "array" || field.type === "json" || field.type === "object" ? 12 : 6;
4131
+ mainFields.push(
4132
+ displayField(field.name, {
4133
+ span
4134
+ })
4135
+ );
4136
+ }
4137
+ }
4138
+ const sections = [];
4139
+ const mainTitle = schema.labels?.singular || schema.label || "Details";
4140
+ if (sidebarFields.length > 0) {
4141
+ sections.push(
4142
+ displaySection(mainTitle, mainFields, {
4143
+ span: 8
4144
+ })
4145
+ );
4146
+ sections.push(
4147
+ displaySection("Overview", sidebarFields, {
4148
+ span: 4
4149
+ })
4150
+ );
4151
+ } else {
4152
+ sections.push(
4153
+ displaySection(mainTitle, mainFields, {
4154
+ span: 12
4155
+ })
4156
+ );
4157
+ }
4158
+ return sections;
4159
+ }
4160
+ function isDetailItemVisible(item, doc, user) {
4161
+ if (!item) return true;
4162
+ const it = normalizeDetailItem(item);
4163
+ const visible = it.options?.visible;
4164
+ if (visible === void 0 || visible === null) return true;
4165
+ if (typeof visible === "boolean") return visible;
4166
+ if (typeof visible === "string") {
4167
+ if (visible.trim().length === 0) return true;
4168
+ try {
4169
+ return Boolean(evaluateJexlSync(visible, { doc, user }));
4170
+ } catch {
4171
+ return true;
4172
+ }
4173
+ }
4174
+ return true;
4175
+ }
4176
+
2897
4177
  // src/index.ts
2898
4178
  function defineCollection(config) {
2899
4179
  return config;
@@ -2943,10 +4223,13 @@ function defineTab(args) {
2943
4223
  }
2944
4224
  // Annotate the CommonJS export names for ESM import in node:
2945
4225
  0 && (module.exports = {
4226
+ BUILTIN_JEXL_HELPERS,
2946
4227
  ConfigValidationError,
2947
4228
  LIFECYCLE_EVENTS_COLLECTION,
2948
4229
  LIFECYCLE_EVENT_NAMES,
2949
4230
  WORKFLOW_HISTORY_COLLECTION,
4231
+ abs,
4232
+ addDays,
2950
4233
  assertValidAdminConditionsInConfig,
2951
4234
  assertValidDeclarativeAccessInConfig,
2952
4235
  assertValidDeclarativeHookExpression,
@@ -2956,13 +4239,20 @@ function defineTab(args) {
2956
4239
  availableWorkflowTransitions,
2957
4240
  buildSqlOrderBy,
2958
4241
  canViewWorkflowDraft,
4242
+ capitalize,
4243
+ ceil,
4244
+ clamp,
4245
+ coalesce,
2959
4246
  collectAdminConditionValidationIssues,
2960
4247
  collectConfigDiagnostics,
2961
4248
  collectDeclarativeAccessValidationIssues,
2962
4249
  collectDeclarativeHookValidationIssues,
2963
4250
  collectPreviewUrlValidationIssues,
4251
+ compact,
4252
+ count,
2964
4253
  createLifecycleEvent,
2965
4254
  createWorkflowDocument,
4255
+ defaultValue,
2966
4256
  defineArrayField,
2967
4257
  defineBlock,
2968
4258
  defineBlocksField,
@@ -2992,32 +4282,81 @@ function defineTab(args) {
2992
4282
  defineTextareaField,
2993
4283
  defineTimeField,
2994
4284
  defineUrlField,
4285
+ diffDays,
2995
4286
  dispatchLifecycleEvent,
2996
4287
  dispatchPendingLifecycleEvents,
4288
+ displayComputed,
4289
+ displayCustom,
4290
+ displayCustomComponent,
4291
+ displayDivider,
4292
+ displayField,
4293
+ displayGrid,
4294
+ displayRepeat,
4295
+ displaySection,
4296
+ displayTab,
4297
+ displayTabs,
4298
+ displayText,
4299
+ endsWith,
4300
+ evaluateDetailComputed,
4301
+ evaluateJexl,
4302
+ evaluateJexlSync,
2997
4303
  executeFieldAfterRead,
2998
4304
  executeFieldBeforeChange,
4305
+ first,
4306
+ floor,
2999
4307
  formatConfigDiagnostics,
4308
+ formatDate,
4309
+ generateDefaultDetailSchema,
3000
4310
  generateOpenApi,
4311
+ get,
3001
4312
  getAdminAuthCollection,
3002
4313
  getPublicAdminAuthConfig,
4314
+ includes,
3003
4315
  initializeWorkflowDocument,
3004
4316
  isConfigValidationError,
4317
+ isDetailItemVisible,
4318
+ isEmpty,
4319
+ isFuture,
4320
+ isPast,
4321
+ join,
4322
+ last,
4323
+ length,
4324
+ lower,
3005
4325
  materializeWorkflowDocument,
4326
+ max,
4327
+ min,
3006
4328
  normalizeConfig,
4329
+ normalizeDetailItem,
4330
+ now,
3007
4331
  numericFieldNames,
3008
4332
  parseMongoWhere,
3009
4333
  parseSort,
3010
4334
  parseSqlWhere,
4335
+ pow,
3011
4336
  publishedStateName,
3012
4337
  publishingWorkflow,
4338
+ readingTime,
4339
+ registerJexlHelpers,
4340
+ replace,
3013
4341
  resolveAdminAuthCollection,
4342
+ round,
3014
4343
  runCollectionHooks,
3015
4344
  saveWorkflowDraft,
3016
4345
  simplePublishingWorkflow,
4346
+ slugify,
4347
+ sqrt,
4348
+ startsWith,
4349
+ sum,
4350
+ today,
3017
4351
  transitionWorkflow,
4352
+ trim,
4353
+ truncate,
4354
+ unique,
4355
+ upper,
3018
4356
  validateAdminConditionExpression,
3019
4357
  validateDeclarativeAccessExpression,
3020
4358
  validateDeclarativeHookExpression,
3021
4359
  validatePreviewUrlExpression,
4360
+ wordCount,
3022
4361
  workflowCapabilities
3023
4362
  });