@centrali-io/centrali-mcp 5.4.0 → 5.5.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/README.md +12 -9
- package/dist/tools/_register.d.ts +9 -0
- package/dist/tools/_register.js +9 -0
- package/dist/tools/describe.js +102 -86
- package/dist/tools/records.d.ts +35 -0
- package/dist/tools/records.js +226 -30
- package/dist/tools/smart-queries.js +58 -36
- package/package.json +3 -2
- package/src/tools/_register.ts +9 -0
- package/src/tools/describe.ts +106 -93
- package/src/tools/records.ts +273 -52
- package/src/tools/smart-queries.ts +70 -43
- package/tests/records.translator.test.cjs +177 -0
|
@@ -2,23 +2,42 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { CentraliSDK } from "@centrali-io/centrali-sdk";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { registerTool, formatError } from "./_register.js";
|
|
5
|
+
|
|
6
|
+
// Tool names below stay as `*_smart_query` to keep existing AI assistant
|
|
7
|
+
// configurations working — renaming would break public clients. Internally
|
|
8
|
+
// they route through `sdk.savedQueries.*` (the canonical namespace) and
|
|
9
|
+
// accept canonical query bodies.
|
|
10
|
+
|
|
11
|
+
const CANONICAL_QUERY_DEFINITION_HINT = `Canonical QueryDefinition body (saved queries store the inner shape — 'resource' is filled in from the collection slug arg).
|
|
12
|
+
|
|
13
|
+
Field paths use dotted strings ('data.status'); operators (no '$' prefix): eq, ne, gt, gte, lt, lte, in, nin, contains, startsWith, endsWith, hasAny, hasAll, exists. Boolean trees use 'and', 'or', 'not'.
|
|
14
|
+
|
|
15
|
+
Variables are referenced as '{{varName}}' inside operator values; the engine infers the variable list from the placeholders in the body. Callers pass concrete values via the 'variables' arg on execute_smart_query / test_smart_query. Example:
|
|
16
|
+
{
|
|
17
|
+
"where": { "data.status": { "eq": "{{statusFilter}}" } },
|
|
18
|
+
"sort": [{ "field": "createdAt", "direction": "desc" }],
|
|
19
|
+
"page": { "limit": 100 }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Do NOT use legacy '\$'-prefixed operators ('\$eq', '\$gte', …). The data service still translates them server-side during the deprecation window, but new saved queries must author canonical operators.`;
|
|
23
|
+
|
|
5
24
|
export function registerSmartQueryTools(server: McpServer, sdk: CentraliSDK) {
|
|
6
|
-
registerTool<any>(server,
|
|
25
|
+
registerTool<any>(server,
|
|
7
26
|
"list_smart_queries",
|
|
8
|
-
"List smart queries.
|
|
27
|
+
"List saved (a.k.a. smart) queries. Saved queries are reusable, parameterized queries defined in the Centrali console. Optionally filter by collection record slug.",
|
|
9
28
|
{
|
|
10
29
|
recordSlug: z
|
|
11
30
|
.string()
|
|
12
31
|
.optional()
|
|
13
32
|
.describe(
|
|
14
|
-
"Filter by collection record slug. If omitted, lists all
|
|
33
|
+
"Filter by collection record slug. If omitted, lists all saved queries in the workspace"
|
|
15
34
|
),
|
|
16
35
|
},
|
|
17
36
|
async ({ recordSlug }) => {
|
|
18
37
|
try {
|
|
19
38
|
const result = recordSlug
|
|
20
|
-
? await sdk.
|
|
21
|
-
: await sdk.
|
|
39
|
+
? await sdk.savedQueries.list(recordSlug)
|
|
40
|
+
: await sdk.savedQueries.listAll();
|
|
22
41
|
return {
|
|
23
42
|
content: [
|
|
24
43
|
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
@@ -29,7 +48,7 @@ export function registerSmartQueryTools(server: McpServer, sdk: CentraliSDK) {
|
|
|
29
48
|
content: [
|
|
30
49
|
{
|
|
31
50
|
type: "text",
|
|
32
|
-
text: formatError(error, "listing
|
|
51
|
+
text: formatError(error, "listing saved queries"),
|
|
33
52
|
},
|
|
34
53
|
],
|
|
35
54
|
isError: true,
|
|
@@ -38,25 +57,25 @@ export function registerSmartQueryTools(server: McpServer, sdk: CentraliSDK) {
|
|
|
38
57
|
}
|
|
39
58
|
);
|
|
40
59
|
|
|
41
|
-
registerTool<any>(server,
|
|
60
|
+
registerTool<any>(server,
|
|
42
61
|
"execute_smart_query",
|
|
43
|
-
"Execute a
|
|
62
|
+
"Execute a saved query by ID and return the canonical { data, meta } envelope. Saved queries can declare variables referenced as '{{varName}}' inside operator values.",
|
|
44
63
|
{
|
|
45
64
|
recordSlug: z
|
|
46
65
|
.string()
|
|
47
66
|
.describe("The collection's record slug the query belongs to"),
|
|
48
|
-
queryId: z.string().describe("The
|
|
67
|
+
queryId: z.string().describe("The saved query ID (UUID) to execute"),
|
|
49
68
|
variables: z
|
|
50
|
-
.record(z.string(), z.
|
|
69
|
+
.record(z.string(), z.any())
|
|
51
70
|
.optional()
|
|
52
71
|
.describe(
|
|
53
|
-
"Variables to substitute
|
|
72
|
+
"Variables to substitute (key-value pairs matching '{{varName}}' placeholders). Values must match the variable's declared type."
|
|
54
73
|
),
|
|
55
74
|
},
|
|
56
75
|
async ({ recordSlug, queryId, variables }) => {
|
|
57
76
|
try {
|
|
58
77
|
const options = variables ? { variables } : undefined;
|
|
59
|
-
const result = await sdk.
|
|
78
|
+
const result = await sdk.savedQueries.execute(
|
|
60
79
|
recordSlug,
|
|
61
80
|
queryId,
|
|
62
81
|
options
|
|
@@ -71,7 +90,7 @@ export function registerSmartQueryTools(server: McpServer, sdk: CentraliSDK) {
|
|
|
71
90
|
content: [
|
|
72
91
|
{
|
|
73
92
|
type: "text",
|
|
74
|
-
text: formatError(error, `executing
|
|
93
|
+
text: formatError(error, `executing saved query '${queryId}'`),
|
|
75
94
|
},
|
|
76
95
|
],
|
|
77
96
|
isError: true,
|
|
@@ -80,16 +99,16 @@ export function registerSmartQueryTools(server: McpServer, sdk: CentraliSDK) {
|
|
|
80
99
|
}
|
|
81
100
|
);
|
|
82
101
|
|
|
83
|
-
registerTool<any>(server,
|
|
102
|
+
registerTool<any>(server,
|
|
84
103
|
"get_smart_query",
|
|
85
|
-
"Get a
|
|
104
|
+
"Get a saved query by ID. Returns the full query definition including canonical 'where', 'sort', 'page', 'select', and variable declarations.",
|
|
86
105
|
{
|
|
87
106
|
recordSlug: z.string().describe("The collection's record slug the query belongs to"),
|
|
88
|
-
queryId: z.string().describe("The
|
|
107
|
+
queryId: z.string().describe("The saved query ID (UUID)"),
|
|
89
108
|
},
|
|
90
109
|
async ({ recordSlug, queryId }) => {
|
|
91
110
|
try {
|
|
92
|
-
const result = await sdk.
|
|
111
|
+
const result = await sdk.savedQueries.get(recordSlug, queryId);
|
|
93
112
|
return {
|
|
94
113
|
content: [
|
|
95
114
|
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
@@ -100,7 +119,7 @@ export function registerSmartQueryTools(server: McpServer, sdk: CentraliSDK) {
|
|
|
100
119
|
content: [
|
|
101
120
|
{
|
|
102
121
|
type: "text",
|
|
103
|
-
text: formatError(error, `getting
|
|
122
|
+
text: formatError(error, `getting saved query '${queryId}'`),
|
|
104
123
|
},
|
|
105
124
|
],
|
|
106
125
|
isError: true,
|
|
@@ -109,23 +128,27 @@ export function registerSmartQueryTools(server: McpServer, sdk: CentraliSDK) {
|
|
|
109
128
|
}
|
|
110
129
|
);
|
|
111
130
|
|
|
112
|
-
registerTool<any>(server,
|
|
131
|
+
registerTool<any>(server,
|
|
113
132
|
"create_smart_query",
|
|
114
|
-
|
|
133
|
+
`Create a new saved query for a collection.
|
|
134
|
+
|
|
135
|
+
${CANONICAL_QUERY_DEFINITION_HINT}`,
|
|
115
136
|
{
|
|
116
137
|
recordSlug: z.string().describe("The collection's record slug to create the query for"),
|
|
117
|
-
name: z.string().describe("Display name for the
|
|
138
|
+
name: z.string().describe("Display name for the saved query"),
|
|
118
139
|
description: z.string().optional().describe("Optional description"),
|
|
119
140
|
queryDefinition: z
|
|
120
141
|
.record(z.string(), z.any())
|
|
121
|
-
.describe(
|
|
142
|
+
.describe(
|
|
143
|
+
"Canonical inner QueryDefinition (without 'resource'). Use 'where', 'text', 'sort', 'page', 'select'. Variables are inferred from '{{varName}}' placeholders inside operator values."
|
|
144
|
+
),
|
|
122
145
|
},
|
|
123
146
|
async ({ recordSlug, name, description, queryDefinition }) => {
|
|
124
147
|
try {
|
|
125
148
|
const input: Record<string, any> = { name, queryDefinition };
|
|
126
149
|
if (description !== undefined) input.description = description;
|
|
127
150
|
|
|
128
|
-
const result = await sdk.
|
|
151
|
+
const result = await sdk.savedQueries.create(recordSlug, input as any);
|
|
129
152
|
return {
|
|
130
153
|
content: [
|
|
131
154
|
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
@@ -136,7 +159,7 @@ export function registerSmartQueryTools(server: McpServer, sdk: CentraliSDK) {
|
|
|
136
159
|
content: [
|
|
137
160
|
{
|
|
138
161
|
type: "text",
|
|
139
|
-
text: formatError(error, `creating
|
|
162
|
+
text: formatError(error, `creating saved query '${name}' for '${recordSlug}'`),
|
|
140
163
|
},
|
|
141
164
|
],
|
|
142
165
|
isError: true,
|
|
@@ -145,18 +168,20 @@ export function registerSmartQueryTools(server: McpServer, sdk: CentraliSDK) {
|
|
|
145
168
|
}
|
|
146
169
|
);
|
|
147
170
|
|
|
148
|
-
registerTool<any>(server,
|
|
171
|
+
registerTool<any>(server,
|
|
149
172
|
"update_smart_query",
|
|
150
|
-
|
|
173
|
+
`Update an existing saved query. Only include the fields you want to change.
|
|
174
|
+
|
|
175
|
+
${CANONICAL_QUERY_DEFINITION_HINT}`,
|
|
151
176
|
{
|
|
152
177
|
recordSlug: z.string().describe("The collection's record slug the query belongs to"),
|
|
153
|
-
queryId: z.string().describe("The
|
|
178
|
+
queryId: z.string().describe("The saved query ID (UUID) to update"),
|
|
154
179
|
name: z.string().optional().describe("Updated display name"),
|
|
155
180
|
description: z.string().optional().describe("Updated description"),
|
|
156
181
|
queryDefinition: z
|
|
157
182
|
.record(z.string(), z.any())
|
|
158
183
|
.optional()
|
|
159
|
-
.describe("Updated
|
|
184
|
+
.describe("Updated canonical inner QueryDefinition (without 'resource')."),
|
|
160
185
|
},
|
|
161
186
|
async ({ recordSlug, queryId, name, description, queryDefinition }) => {
|
|
162
187
|
try {
|
|
@@ -165,7 +190,7 @@ export function registerSmartQueryTools(server: McpServer, sdk: CentraliSDK) {
|
|
|
165
190
|
if (description !== undefined) input.description = description;
|
|
166
191
|
if (queryDefinition !== undefined) input.queryDefinition = queryDefinition;
|
|
167
192
|
|
|
168
|
-
const result = await sdk.
|
|
193
|
+
const result = await sdk.savedQueries.update(recordSlug, queryId, input as any);
|
|
169
194
|
return {
|
|
170
195
|
content: [
|
|
171
196
|
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
@@ -176,7 +201,7 @@ export function registerSmartQueryTools(server: McpServer, sdk: CentraliSDK) {
|
|
|
176
201
|
content: [
|
|
177
202
|
{
|
|
178
203
|
type: "text",
|
|
179
|
-
text: formatError(error, `updating
|
|
204
|
+
text: formatError(error, `updating saved query '${queryId}'`),
|
|
180
205
|
},
|
|
181
206
|
],
|
|
182
207
|
isError: true,
|
|
@@ -185,21 +210,21 @@ export function registerSmartQueryTools(server: McpServer, sdk: CentraliSDK) {
|
|
|
185
210
|
}
|
|
186
211
|
);
|
|
187
212
|
|
|
188
|
-
registerTool<any>(server,
|
|
213
|
+
registerTool<any>(server,
|
|
189
214
|
"delete_smart_query",
|
|
190
|
-
"Delete a
|
|
215
|
+
"Delete a saved query by ID.",
|
|
191
216
|
{
|
|
192
217
|
recordSlug: z.string().describe("The collection's record slug the query belongs to"),
|
|
193
|
-
queryId: z.string().describe("The
|
|
218
|
+
queryId: z.string().describe("The saved query ID (UUID) to delete"),
|
|
194
219
|
},
|
|
195
220
|
async ({ recordSlug, queryId }) => {
|
|
196
221
|
try {
|
|
197
|
-
await sdk.
|
|
222
|
+
await sdk.savedQueries.delete(recordSlug, queryId);
|
|
198
223
|
return {
|
|
199
224
|
content: [
|
|
200
225
|
{
|
|
201
226
|
type: "text",
|
|
202
|
-
text: `
|
|
227
|
+
text: `Saved query '${queryId}' deleted from '${recordSlug}'.`,
|
|
203
228
|
},
|
|
204
229
|
],
|
|
205
230
|
};
|
|
@@ -208,7 +233,7 @@ export function registerSmartQueryTools(server: McpServer, sdk: CentraliSDK) {
|
|
|
208
233
|
content: [
|
|
209
234
|
{
|
|
210
235
|
type: "text",
|
|
211
|
-
text: formatError(error, `deleting
|
|
236
|
+
text: formatError(error, `deleting saved query '${queryId}'`),
|
|
212
237
|
},
|
|
213
238
|
],
|
|
214
239
|
isError: true,
|
|
@@ -217,25 +242,27 @@ export function registerSmartQueryTools(server: McpServer, sdk: CentraliSDK) {
|
|
|
217
242
|
}
|
|
218
243
|
);
|
|
219
244
|
|
|
220
|
-
registerTool<any>(server,
|
|
245
|
+
registerTool<any>(server,
|
|
221
246
|
"test_smart_query",
|
|
222
|
-
|
|
247
|
+
`Test execute a query definition without saving it. Useful for validating syntax and previewing results before creating a saved query.
|
|
248
|
+
|
|
249
|
+
${CANONICAL_QUERY_DEFINITION_HINT}`,
|
|
223
250
|
{
|
|
224
251
|
recordSlug: z.string().describe("The collection's record slug to test against"),
|
|
225
252
|
queryDefinition: z
|
|
226
253
|
.record(z.string(), z.any())
|
|
227
|
-
.describe("
|
|
254
|
+
.describe("Canonical inner QueryDefinition to test (without 'resource')."),
|
|
228
255
|
variables: z
|
|
229
|
-
.record(z.string(), z.
|
|
256
|
+
.record(z.string(), z.any())
|
|
230
257
|
.optional()
|
|
231
|
-
.describe("Optional variables to substitute
|
|
258
|
+
.describe("Optional variables to substitute."),
|
|
232
259
|
},
|
|
233
260
|
async ({ recordSlug, queryDefinition, variables }) => {
|
|
234
261
|
try {
|
|
235
262
|
const input: Record<string, any> = { queryDefinition };
|
|
236
263
|
if (variables !== undefined) input.variables = variables;
|
|
237
264
|
|
|
238
|
-
const result = await sdk.
|
|
265
|
+
const result = await sdk.savedQueries.test(recordSlug, input as any);
|
|
239
266
|
return {
|
|
240
267
|
content: [
|
|
241
268
|
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
@@ -246,7 +273,7 @@ export function registerSmartQueryTools(server: McpServer, sdk: CentraliSDK) {
|
|
|
246
273
|
content: [
|
|
247
274
|
{
|
|
248
275
|
type: "text",
|
|
249
|
-
text: formatError(error, `test-executing
|
|
276
|
+
text: formatError(error, `test-executing saved query for '${recordSlug}'`),
|
|
250
277
|
},
|
|
251
278
|
],
|
|
252
279
|
isError: true,
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy 5.4.0 → canonical translator tests for `query_records`.
|
|
3
|
+
*
|
|
4
|
+
* Runs against the built `dist/` so we exercise the same module the published
|
|
5
|
+
* package ships. Uses Node's built-in `node:test` runner — no jest/ts-node
|
|
6
|
+
* needed. Wire via `npm test` (`tsc && node --test tests/`).
|
|
7
|
+
*/
|
|
8
|
+
const test = require("node:test");
|
|
9
|
+
const assert = require("node:assert/strict");
|
|
10
|
+
const { _internal } = require("../dist/tools/records.js");
|
|
11
|
+
|
|
12
|
+
const { translateQueryRecordsArgs, parseLegacyFilters, parseLegacySort } = _internal;
|
|
13
|
+
|
|
14
|
+
test("canonical args pass through unchanged", () => {
|
|
15
|
+
const out = translateQueryRecordsArgs({
|
|
16
|
+
resource: "orders",
|
|
17
|
+
where: { "data.status": { eq: "open" } },
|
|
18
|
+
page: { limit: 50 },
|
|
19
|
+
});
|
|
20
|
+
assert.deepEqual(out, {
|
|
21
|
+
resource: "orders",
|
|
22
|
+
definition: {
|
|
23
|
+
resource: "orders",
|
|
24
|
+
where: { "data.status": { eq: "open" } },
|
|
25
|
+
page: { limit: 50 },
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("full 5.4.0 legacy shape translates", () => {
|
|
31
|
+
const out = translateQueryRecordsArgs({
|
|
32
|
+
recordSlug: "orders",
|
|
33
|
+
filters: { "data.status": "open", "data.price[gte]": 100 },
|
|
34
|
+
sort: "-createdAt",
|
|
35
|
+
pageSize: 50,
|
|
36
|
+
page: 2,
|
|
37
|
+
});
|
|
38
|
+
assert.deepEqual(out, {
|
|
39
|
+
resource: "orders",
|
|
40
|
+
definition: {
|
|
41
|
+
resource: "orders",
|
|
42
|
+
where: {
|
|
43
|
+
and: [
|
|
44
|
+
{ "data.status": { eq: "open" } },
|
|
45
|
+
{ "data.price": { gte: 100 } },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
sort: [{ field: "createdAt", direction: "desc" }],
|
|
49
|
+
page: { limit: 50, offset: 50 },
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("filters: $-prefixed operator alias", () => {
|
|
55
|
+
const out = translateQueryRecordsArgs({
|
|
56
|
+
recordSlug: "orders",
|
|
57
|
+
filters: { "data.amount[$gte]": 100 },
|
|
58
|
+
});
|
|
59
|
+
assert.deepEqual(out.definition.where, { "data.amount": { gte: 100 } });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("filters: single condition omits 'and' wrapping", () => {
|
|
63
|
+
const out = translateQueryRecordsArgs({
|
|
64
|
+
recordSlug: "orders",
|
|
65
|
+
filters: { "data.status": "open" },
|
|
66
|
+
});
|
|
67
|
+
assert.deepEqual(out.definition.where, { "data.status": { eq: "open" } });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("filters + canonical where merge under 'and'", () => {
|
|
71
|
+
const out = translateQueryRecordsArgs({
|
|
72
|
+
resource: "orders",
|
|
73
|
+
where: { "data.region": { eq: "us-east" } },
|
|
74
|
+
filters: { "data.status": "open" },
|
|
75
|
+
});
|
|
76
|
+
assert.deepEqual(out.definition.where, {
|
|
77
|
+
and: [
|
|
78
|
+
{ "data.region": { eq: "us-east" } },
|
|
79
|
+
{ "data.status": { eq: "open" } },
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("dateWindow: from + to → and(gte, lte)", () => {
|
|
85
|
+
const out = translateQueryRecordsArgs({
|
|
86
|
+
recordSlug: "orders",
|
|
87
|
+
dateWindow: { field: "createdAt", from: "2026-01-01", to: "2026-04-01" },
|
|
88
|
+
});
|
|
89
|
+
assert.deepEqual(out.definition.where, {
|
|
90
|
+
and: [
|
|
91
|
+
{ createdAt: { gte: "2026-01-01" } },
|
|
92
|
+
{ createdAt: { lte: "2026-04-01" } },
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("dateWindow: from-only → single gte (no 'and')", () => {
|
|
98
|
+
const out = translateQueryRecordsArgs({
|
|
99
|
+
recordSlug: "orders",
|
|
100
|
+
dateWindow: { field: "createdAt", from: "2026-01-01" },
|
|
101
|
+
});
|
|
102
|
+
assert.deepEqual(out.definition.where, { createdAt: { gte: "2026-01-01" } });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("dateWindow: to-only → single lte (no 'and')", () => {
|
|
106
|
+
const out = translateQueryRecordsArgs({
|
|
107
|
+
recordSlug: "orders",
|
|
108
|
+
dateWindow: { field: "createdAt", to: "2026-04-01" },
|
|
109
|
+
});
|
|
110
|
+
assert.deepEqual(out.definition.where, { createdAt: { lte: "2026-04-01" } });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("expand: comma-list → include array; empty → omit", () => {
|
|
114
|
+
const ok = translateQueryRecordsArgs({
|
|
115
|
+
recordSlug: "orders",
|
|
116
|
+
expand: "customer,product",
|
|
117
|
+
});
|
|
118
|
+
assert.deepEqual(ok.definition.include, [
|
|
119
|
+
{ relation: "customer" },
|
|
120
|
+
{ relation: "product" },
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
const empty = translateQueryRecordsArgs({ recordSlug: "orders", expand: "" });
|
|
124
|
+
assert.equal(empty.definition.include, undefined);
|
|
125
|
+
|
|
126
|
+
const allEmpty = translateQueryRecordsArgs({ recordSlug: "orders", expand: ",,," });
|
|
127
|
+
assert.equal(allEmpty.definition.include, undefined);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("sort: multi-sort comma-separated mixes asc/desc", () => {
|
|
131
|
+
assert.deepEqual(parseLegacySort("name,-createdAt"), [
|
|
132
|
+
{ field: "name", direction: "asc" },
|
|
133
|
+
{ field: "createdAt", direction: "desc" },
|
|
134
|
+
]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("page without pageSize defaults to limit 50", () => {
|
|
138
|
+
const out = translateQueryRecordsArgs({ recordSlug: "orders", page: 3 });
|
|
139
|
+
assert.deepEqual(out.definition.page, { limit: 50, offset: 100 });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("pageSize without page → limit only", () => {
|
|
143
|
+
const out = translateQueryRecordsArgs({ recordSlug: "orders", pageSize: 25 });
|
|
144
|
+
assert.deepEqual(out.definition.page, { limit: 25 });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("includeTotal silently dropped (meta.total always returned)", () => {
|
|
148
|
+
const out = translateQueryRecordsArgs({ recordSlug: "orders", includeTotal: true });
|
|
149
|
+
assert.deepEqual(out.definition, { resource: "orders" });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("includeDeleted: true throws clear error (no silent privacy regression)", () => {
|
|
153
|
+
assert.throws(
|
|
154
|
+
() => translateQueryRecordsArgs({ recordSlug: "orders", includeDeleted: true }),
|
|
155
|
+
/includeDeleted.*not supported in Phase 1/,
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("missing both resource and recordSlug throws", () => {
|
|
160
|
+
assert.throws(
|
|
161
|
+
() => translateQueryRecordsArgs({ filters: { "data.x": 1 } }),
|
|
162
|
+
/resource.*recordSlug/,
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("parseLegacyFilters: hostile inputs return undefined, no crash", () => {
|
|
167
|
+
assert.equal(parseLegacyFilters(null), undefined);
|
|
168
|
+
assert.equal(parseLegacyFilters(undefined), undefined);
|
|
169
|
+
assert.equal(parseLegacyFilters([]), undefined);
|
|
170
|
+
assert.equal(parseLegacyFilters("string"), undefined);
|
|
171
|
+
assert.equal(parseLegacyFilters(42), undefined);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("resource takes precedence over recordSlug", () => {
|
|
175
|
+
const out = translateQueryRecordsArgs({ resource: "canonical", recordSlug: "legacy" });
|
|
176
|
+
assert.equal(out.resource, "canonical");
|
|
177
|
+
});
|