@dyrected/core 2.9.0 → 2.10.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
@@ -101,7 +101,7 @@ import {
101
101
  validatePreviewUrlExpression,
102
102
  wordCount,
103
103
  workflowCapabilities
104
- } from "./chunk-XRIONGRY.js";
104
+ } from "./chunk-TYPDQTE2.js";
105
105
  import "./chunk-JSBRDJBE.js";
106
106
 
107
107
  // src/types/workflows.ts
@@ -114,44 +114,17 @@ var LIFECYCLE_EVENT_NAMES = [
114
114
 
115
115
  // src/types/views.ts
116
116
  function defineView(config) {
117
- return {
118
- slug: config.slug,
119
- label: config.label,
120
- icon: config.icon,
121
- layout: config.layout ?? "table",
122
- filter: config.filter,
123
- groupBy: config.groupBy,
124
- dateField: config.dateField,
125
- startDateField: config.startDateField,
126
- endDateField: config.endDateField,
127
- columns: config.columns,
128
- sort: config.sort,
129
- actions: config.actions,
130
- features: config.features,
131
- actionOrder: config.actionOrder,
132
- metrics: config.metrics,
133
- access: config.access
134
- };
117
+ return config;
135
118
  }
136
119
  function defineAction(config) {
137
- return {
138
- name: config.name,
139
- label: config.label,
140
- icon: config.icon,
141
- type: config.type ?? "row",
142
- confirm: config.confirm,
143
- fields: config.fields,
144
- mutation: config.mutation,
145
- handler: config.handler,
146
- access: config.access
147
- };
120
+ return config;
148
121
  }
149
122
 
150
123
  // src/utils/parse-where.ts
151
124
  function assertNever(op, context) {
152
125
  throw new Error(`[dyrected/core] Unhandled where operator "${op}" in ${context}`);
153
126
  }
154
- function parseSqlWhere(where, getJsonField, placeholder = "?", likeOperator = "LIKE") {
127
+ function parseSqlWhere(where, getJsonField, placeholder = "?", likeOperator = placeholder === "pg" ? "ILIKE" : "LIKE") {
155
128
  const params = [];
156
129
  let pgIndex = 1;
157
130
  function next() {
@@ -162,8 +135,16 @@ function parseSqlWhere(where, getJsonField, placeholder = "?", likeOperator = "L
162
135
  }
163
136
  function buildOperator(field, value) {
164
137
  const c = col(field);
138
+ const isJsonExtract = c.startsWith("data->>") || c.startsWith("json_extract(") || c.startsWith("JSON_UNQUOTE(");
165
139
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
166
- params.push(typeof value === "boolean" ? String(value) : value);
140
+ if (value === null) {
141
+ return `${c} IS NULL`;
142
+ }
143
+ if (value === "" && placeholder === "pg") {
144
+ params.push("");
145
+ return `(${c})::text = ${next()}`;
146
+ }
147
+ params.push(typeof value === "boolean" && isJsonExtract ? String(value) : value);
167
148
  return `${c} = ${next()}`;
168
149
  }
169
150
  const entries = Object.entries(value);
@@ -174,18 +155,63 @@ function parseSqlWhere(where, getJsonField, placeholder = "?", likeOperator = "L
174
155
  return buildSingleOp(c, op, operand);
175
156
  }
176
157
  function buildSingleOp(c, op, operand) {
158
+ const isJsonExtract = c.startsWith("data->>") || c.startsWith("json_extract(") || c.startsWith("JSON_UNQUOTE(");
177
159
  switch (op) {
178
160
  case "equals":
179
- params.push(typeof operand === "boolean" ? String(operand) : operand);
161
+ if (operand === null) {
162
+ return `${c} IS NULL`;
163
+ }
164
+ if (operand === "" && placeholder === "pg") {
165
+ params.push("");
166
+ return `(${c})::text = ${next()}`;
167
+ }
168
+ if (typeof operand === "boolean") {
169
+ if (placeholder === "?") {
170
+ params.push(operand ? 1 : 0);
171
+ } else if (isJsonExtract) {
172
+ params.push(String(operand));
173
+ } else {
174
+ params.push(operand);
175
+ }
176
+ } else {
177
+ params.push(operand);
178
+ }
180
179
  return `${c} = ${next()}`;
181
180
  case "not_equals":
182
- params.push(typeof operand === "boolean" ? String(operand) : operand);
181
+ if (operand === null) {
182
+ return `${c} IS NOT NULL`;
183
+ }
184
+ if (operand === "" && placeholder === "pg") {
185
+ params.push("");
186
+ return `(${c})::text != ${next()}`;
187
+ }
188
+ if (typeof operand === "boolean") {
189
+ if (placeholder === "?") {
190
+ params.push(operand ? 1 : 0);
191
+ } else if (isJsonExtract) {
192
+ params.push(String(operand));
193
+ } else {
194
+ params.push(operand);
195
+ }
196
+ } else {
197
+ params.push(operand);
198
+ }
183
199
  return `${c} != ${next()}`;
184
200
  case "in": {
185
201
  const vals = Array.isArray(operand) ? operand : [operand];
186
202
  if (vals.length === 0) return "1=0";
187
203
  const placeholders = vals.map((v) => {
188
- params.push(typeof v === "boolean" ? String(v) : v);
204
+ if (typeof v === "boolean") {
205
+ if (placeholder === "?") {
206
+ params.push(v ? 1 : 0);
207
+ } else if (isJsonExtract) {
208
+ params.push(String(v));
209
+ } else {
210
+ params.push(v);
211
+ }
212
+ } else {
213
+ params.push(v);
214
+ }
189
215
  return next();
190
216
  });
191
217
  return `${c} IN (${placeholders.join(", ")})`;
@@ -194,7 +220,7 @@ function parseSqlWhere(where, getJsonField, placeholder = "?", likeOperator = "L
194
220
  const vals = Array.isArray(operand) ? operand : [operand];
195
221
  if (vals.length === 0) return "1=1";
196
222
  const placeholders = vals.map((v) => {
197
- params.push(typeof v === "boolean" ? String(v) : v);
223
+ params.push(typeof v === "boolean" && isJsonExtract ? String(v) : v);
198
224
  return next();
199
225
  });
200
226
  return `${c} NOT IN (${placeholders.join(", ")})`;
@@ -220,10 +246,10 @@ function parseSqlWhere(where, getJsonField, placeholder = "?", likeOperator = "L
220
246
  case "contains":
221
247
  case "like":
222
248
  params.push(`%${operand}%`);
223
- return `${c} ${likeOperator} ${next()}`;
249
+ return placeholder === "pg" ? `(${c})::text ${likeOperator} ${next()}` : `${c} ${likeOperator} ${next()}`;
224
250
  case "starts_with":
225
251
  params.push(`${operand}%`);
226
- return `${c} ${likeOperator} ${next()}`;
252
+ return placeholder === "pg" ? `(${c})::text ${likeOperator} ${next()}` : `${c} ${likeOperator} ${next()}`;
227
253
  case "exists":
228
254
  return operand ? `${c} IS NOT NULL` : `${c} IS NULL`;
229
255
  default:
package/dist/server.cjs CHANGED
@@ -6247,9 +6247,23 @@ var CollectionController = class {
6247
6247
  400
6248
6248
  );
6249
6249
  }
6250
+ let rawAggregates;
6251
+ let groupBy;
6252
+ if ("aggregates" in body && typeof body.aggregates === "object" && body.aggregates !== null && !Array.isArray(body.aggregates)) {
6253
+ rawAggregates = body.aggregates;
6254
+ if (typeof body.groupBy === "string" && body.groupBy.trim()) {
6255
+ groupBy = body.groupBy.trim();
6256
+ }
6257
+ } else {
6258
+ rawAggregates = body;
6259
+ const queryGroupBy = c.req.query("groupBy");
6260
+ if (typeof queryGroupBy === "string" && queryGroupBy.trim()) {
6261
+ groupBy = queryGroupBy.trim();
6262
+ }
6263
+ }
6250
6264
  const { sanitizeWhereClause: sanitizeWhereClause2 } = await Promise.resolve().then(() => (init_where_sanitizer(), where_sanitizer_exports));
6251
6265
  const sanitizedAggregates = {};
6252
- for (const [key, op] of Object.entries(body)) {
6266
+ for (const [key, op] of Object.entries(rawAggregates)) {
6253
6267
  if (!op || typeof op !== "object" || Array.isArray(op)) {
6254
6268
  return c.json(
6255
6269
  {
@@ -6274,7 +6288,8 @@ var CollectionController = class {
6274
6288
  }
6275
6289
  const result = await db.aggregate({
6276
6290
  collection: this.collection.slug,
6277
- aggregates: sanitizedAggregates
6291
+ aggregates: sanitizedAggregates,
6292
+ ...groupBy ? { groupBy } : {}
6278
6293
  });
6279
6294
  return c.json(result);
6280
6295
  }
@@ -8959,39 +8974,94 @@ function generateOpenApi(config) {
8959
8974
  post: {
8960
8975
  tags: [collectionTag],
8961
8976
  summary: `Aggregate ${labels.plural}`,
8962
- description: `Compute statistical aggregations (count, sum, avg, min, max) across ${labels.plural}.`,
8977
+ description: `Compute statistical aggregations (count, countDistinct, distinct, sum, avg, min, max, groupBy) across ${labels.plural}.`,
8978
+ parameters: [
8979
+ {
8980
+ name: "groupBy",
8981
+ in: "query",
8982
+ description: "Optional field name to compute aggregates grouped by distinct values",
8983
+ required: false,
8984
+ schema: { type: "string" }
8985
+ }
8986
+ ],
8963
8987
  requestBody: {
8964
8988
  required: true,
8965
8989
  content: {
8966
8990
  "application/json": {
8967
8991
  schema: {
8968
- type: "object",
8969
- additionalProperties: {
8970
- type: "object",
8971
- properties: {
8972
- count: { type: "string", example: "*" },
8973
- sum: { type: "string", description: "Field to sum" },
8974
- avg: { type: "string", description: "Field to average" },
8975
- min: { type: "string", description: "Field for minimum" },
8976
- max: { type: "string", description: "Field for maximum" },
8977
- cast: {
8978
- type: "string",
8979
- enum: [
8980
- "number",
8981
- "integer",
8982
- "float",
8983
- "boolean",
8984
- "date",
8985
- "string"
8986
- ]
8987
- },
8988
- where: {
8992
+ oneOf: [
8993
+ {
8994
+ type: "object",
8995
+ description: "Flat aggregate operations dictionary",
8996
+ additionalProperties: {
8989
8997
  type: "object",
8990
- description: "Per-aggregate filter condition",
8991
- additionalProperties: true
8998
+ properties: {
8999
+ count: { type: "string", example: "*" },
9000
+ countDistinct: { type: "string", description: "Field to count unique non-null values" },
9001
+ distinct: { type: "string", description: "Field to extract unique non-null values" },
9002
+ sum: { type: "string", description: "Field to sum" },
9003
+ avg: { type: "string", description: "Field to average" },
9004
+ min: { type: "string", description: "Field for minimum" },
9005
+ max: { type: "string", description: "Field for maximum" },
9006
+ cast: {
9007
+ type: "string",
9008
+ enum: [
9009
+ "number",
9010
+ "integer",
9011
+ "float",
9012
+ "boolean",
9013
+ "date",
9014
+ "string"
9015
+ ]
9016
+ },
9017
+ where: {
9018
+ type: "object",
9019
+ description: "Per-aggregate filter condition",
9020
+ additionalProperties: true
9021
+ }
9022
+ }
8992
9023
  }
9024
+ },
9025
+ {
9026
+ type: "object",
9027
+ description: "Grouped aggregate payload with explicit groupBy property",
9028
+ properties: {
9029
+ groupBy: { type: "string", description: "Field name to group aggregates by" },
9030
+ aggregates: {
9031
+ type: "object",
9032
+ additionalProperties: {
9033
+ type: "object",
9034
+ properties: {
9035
+ count: { type: "string", example: "*" },
9036
+ countDistinct: { type: "string", description: "Field to count unique non-null values" },
9037
+ distinct: { type: "string", description: "Field to extract unique non-null values" },
9038
+ sum: { type: "string", description: "Field to sum" },
9039
+ avg: { type: "string", description: "Field to average" },
9040
+ min: { type: "string", description: "Field for minimum" },
9041
+ max: { type: "string", description: "Field for maximum" },
9042
+ cast: {
9043
+ type: "string",
9044
+ enum: [
9045
+ "number",
9046
+ "integer",
9047
+ "float",
9048
+ "boolean",
9049
+ "date",
9050
+ "string"
9051
+ ]
9052
+ },
9053
+ where: {
9054
+ type: "object",
9055
+ description: "Per-aggregate filter condition",
9056
+ additionalProperties: true
9057
+ }
9058
+ }
9059
+ }
9060
+ }
9061
+ },
9062
+ required: ["aggregates"]
8993
9063
  }
8994
- }
9064
+ ]
8995
9065
  }
8996
9066
  }
8997
9067
  }
@@ -9002,11 +9072,36 @@ function generateOpenApi(config) {
9002
9072
  content: {
9003
9073
  "application/json": {
9004
9074
  schema: {
9005
- type: "object",
9006
- additionalProperties: {
9007
- type: "number",
9008
- nullable: true
9009
- }
9075
+ oneOf: [
9076
+ {
9077
+ type: "object",
9078
+ description: "Flat aggregate results map",
9079
+ additionalProperties: {
9080
+ oneOf: [
9081
+ { type: "number", nullable: true },
9082
+ { type: "array", items: { type: "string" } }
9083
+ ]
9084
+ }
9085
+ },
9086
+ {
9087
+ type: "object",
9088
+ description: "Grouped aggregate results breakdown",
9089
+ properties: {
9090
+ groups: {
9091
+ type: "object",
9092
+ additionalProperties: {
9093
+ type: "object",
9094
+ additionalProperties: {
9095
+ oneOf: [
9096
+ { type: "number", nullable: true },
9097
+ { type: "array", items: { type: "string" } }
9098
+ ]
9099
+ }
9100
+ }
9101
+ }
9102
+ }
9103
+ }
9104
+ ]
9010
9105
  }
9011
9106
  }
9012
9107
  }
@@ -9753,6 +9848,8 @@ async function serializeViewForApi(view, serializeAccess) {
9753
9848
  columns: view.columns,
9754
9849
  sort: view.sort,
9755
9850
  metrics: view.metrics,
9851
+ features: view.features,
9852
+ actionOrder: view.actionOrder,
9756
9853
  actions: await Promise.all(
9757
9854
  (view.actions || []).map(async (action) => ({
9758
9855
  name: action.name,
@@ -9829,6 +9926,7 @@ function registerRoutes(app, config) {
9829
9926
  auth: !!col.auth,
9830
9927
  audit: !!col.audit,
9831
9928
  drafts: !!col.drafts,
9929
+ defaultView: col.defaultView ?? col.admin?.defaultView,
9832
9930
  views: await Promise.all((col.views || []).map((view) => serializeViewForApi(view, serializeAccess))),
9833
9931
  admin: col.admin,
9834
9932
  detail: serializeDetailForApi(col.detail),
package/dist/server.d.cts CHANGED
@@ -2,7 +2,7 @@ import * as hono_types from 'hono/types';
2
2
  import * as hono from 'hono';
3
3
  import { Context, Hono } from 'hono';
4
4
  import { Logger } from 'pino';
5
- import { D as DyrectedConfig, b2 as DyrectedObservabilityConfig, C as CollectionConfig, G as GlobalConfig } from './app-config-gEQJFvnN.cjs';
5
+ import { D as DyrectedConfig, b4 as DyrectedObservabilityConfig, C as CollectionConfig, G as GlobalConfig } from './app-config-DQhcrGDE.cjs';
6
6
  import { JWTPayload } from 'jose';
7
7
  import { Tracer, Counter, Attributes, Histogram, Span } from '@opentelemetry/api';
8
8
  import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
@@ -498,7 +498,7 @@ declare class CollectionController {
498
498
  }, 403, "json">) | (Response & hono.TypedResponse<{
499
499
  message: string;
500
500
  }, 400, "json">) | (Response & hono.TypedResponse<{
501
- [x: string]: number | null;
501
+ [x: string]: any;
502
502
  }, hono_utils_http_status.ContentfulStatusCode, "json">)>;
503
503
  }
504
504
 
package/dist/server.d.ts CHANGED
@@ -2,7 +2,7 @@ import * as hono_types from 'hono/types';
2
2
  import * as hono from 'hono';
3
3
  import { Context, Hono } from 'hono';
4
4
  import { Logger } from 'pino';
5
- import { D as DyrectedConfig, b2 as DyrectedObservabilityConfig, C as CollectionConfig, G as GlobalConfig } from './app-config-gEQJFvnN.js';
5
+ import { D as DyrectedConfig, b4 as DyrectedObservabilityConfig, C as CollectionConfig, G as GlobalConfig } from './app-config-DQhcrGDE.js';
6
6
  import { JWTPayload } from 'jose';
7
7
  import { Tracer, Counter, Attributes, Histogram, Span } from '@opentelemetry/api';
8
8
  import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
@@ -498,7 +498,7 @@ declare class CollectionController {
498
498
  }, 403, "json">) | (Response & hono.TypedResponse<{
499
499
  message: string;
500
500
  }, 400, "json">) | (Response & hono.TypedResponse<{
501
- [x: string]: number | null;
501
+ [x: string]: any;
502
502
  }, hono_utils_http_status.ContentfulStatusCode, "json">)>;
503
503
  }
504
504
 
package/dist/server.js CHANGED
@@ -30,7 +30,7 @@ import {
30
30
  touchAuthSession,
31
31
  transitionWorkflow,
32
32
  verifyCollectionToken
33
- } from "./chunk-XRIONGRY.js";
33
+ } from "./chunk-TYPDQTE2.js";
34
34
  import {
35
35
  attachRequestMetrics,
36
36
  bindObservabilityRuntime,
@@ -2200,9 +2200,23 @@ var CollectionController = class {
2200
2200
  400
2201
2201
  );
2202
2202
  }
2203
+ let rawAggregates;
2204
+ let groupBy;
2205
+ if ("aggregates" in body && typeof body.aggregates === "object" && body.aggregates !== null && !Array.isArray(body.aggregates)) {
2206
+ rawAggregates = body.aggregates;
2207
+ if (typeof body.groupBy === "string" && body.groupBy.trim()) {
2208
+ groupBy = body.groupBy.trim();
2209
+ }
2210
+ } else {
2211
+ rawAggregates = body;
2212
+ const queryGroupBy = c.req.query("groupBy");
2213
+ if (typeof queryGroupBy === "string" && queryGroupBy.trim()) {
2214
+ groupBy = queryGroupBy.trim();
2215
+ }
2216
+ }
2203
2217
  const { sanitizeWhereClause } = await import("./where-sanitizer-3MNFMYA2.js");
2204
2218
  const sanitizedAggregates = {};
2205
- for (const [key, op] of Object.entries(body)) {
2219
+ for (const [key, op] of Object.entries(rawAggregates)) {
2206
2220
  if (!op || typeof op !== "object" || Array.isArray(op)) {
2207
2221
  return c.json(
2208
2222
  {
@@ -2227,7 +2241,8 @@ var CollectionController = class {
2227
2241
  }
2228
2242
  const result = await db.aggregate({
2229
2243
  collection: this.collection.slug,
2230
- aggregates: sanitizedAggregates
2244
+ aggregates: sanitizedAggregates,
2245
+ ...groupBy ? { groupBy } : {}
2231
2246
  });
2232
2247
  return c.json(result);
2233
2248
  }
@@ -4564,6 +4579,8 @@ async function serializeViewForApi(view, serializeAccess) {
4564
4579
  columns: view.columns,
4565
4580
  sort: view.sort,
4566
4581
  metrics: view.metrics,
4582
+ features: view.features,
4583
+ actionOrder: view.actionOrder,
4567
4584
  actions: await Promise.all(
4568
4585
  (view.actions || []).map(async (action) => ({
4569
4586
  name: action.name,
@@ -4640,6 +4657,7 @@ function registerRoutes(app, config) {
4640
4657
  auth: !!col.auth,
4641
4658
  audit: !!col.audit,
4642
4659
  drafts: !!col.drafts,
4660
+ defaultView: col.defaultView ?? col.admin?.defaultView,
4643
4661
  views: await Promise.all((col.views || []).map((view) => serializeViewForApi(view, serializeAccess))),
4644
4662
  admin: col.admin,
4645
4663
  detail: serializeDetailForApi(col.detail),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dyrected/core",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",