@cxtms/cx-schema 1.7.12 → 1.7.13

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.
@@ -12,6 +12,10 @@ Shared domain reference for CargoXplorer entities. Used by `cx-workflow` and `cx
12
12
 
13
13
  For CLI authentication, PAT tokens, org management, and publishing: see [ref-cli-auth.md](ref-cli-auth.md)
14
14
 
15
+ ## GraphQL Querying & Audit History
16
+
17
+ For running GraphQL queries via CLI, filter syntax (Lucene), pagination, audit change history, and field discovery: see [ref-graphql-query.md](ref-graphql-query.md)
18
+
15
19
  ## Feature File Layout
16
20
 
17
21
  All modules and workflows are organized under feature directories:
@@ -0,0 +1,320 @@
1
+ # GraphQL Query Reference
2
+
3
+ ## CLI Query Command
4
+
5
+ ```bash
6
+ npx cxtms query '<graphql-query>' # Inline query
7
+ npx cxtms query my-query.graphql # From file
8
+ npx cxtms query '<query>' --vars '{"key":"v"}' # With variables
9
+ ```
10
+
11
+ Requires active login (`npx cxtms login`). Uses the active organization.
12
+
13
+ ## Query Arguments
14
+
15
+ All root entity queries follow this pattern:
16
+
17
+ ```graphql
18
+ orders(
19
+ organizationId: Int! # Required — active org ID
20
+ filter: String # Lucene query syntax (see below)
21
+ search: String # Full-text search across key fields
22
+ orderBy: String # Comma-separated sort fields
23
+ take: Int # Page size (alias: limit)
24
+ skip: Int # Offset (alias: offset)
25
+ )
26
+ ```
27
+
28
+ Entity-specific extras:
29
+ - `orders` — `includeDraft: Boolean` (default false)
30
+
31
+ Available root queries: `orders`, `contacts`, `commodities`, `accountingTransactions`, `jobs`, and others.
32
+
33
+ ## Filter Syntax (Lucene Query)
34
+
35
+ Filters use **Lucene Query syntax** (not OData, not NCalc).
36
+
37
+ ### Basic field matching
38
+ ```
39
+ filter: "orderId:196596"
40
+ filter: "orderType:ParcelShipment"
41
+ filter: "status:Active"
42
+ ```
43
+
44
+ ### Wildcards
45
+ ```
46
+ filter: "name:*pending*" # Contains
47
+ filter: "code:ABC*" # Starts with
48
+ filter: "status:*Active" # Ends with
49
+ ```
50
+
51
+ ### Logical operators
52
+ ```
53
+ filter: "countryCode:US AND name:Test"
54
+ filter: "status:Active OR status:Pending"
55
+ filter: "NOT customValues.po:123"
56
+ filter: "orderType:ParcelShipment AND (status:Active OR status:Pending)"
57
+ ```
58
+
59
+ ### Nested paths (dot notation)
60
+ ```
61
+ filter: "orderEntities.entityType:Consignee"
62
+ filter: "orderEntities.contactId:7128"
63
+ filter: "jobs.orders.orderNumber:16*"
64
+ ```
65
+
66
+ ### Range queries
67
+ ```
68
+ filter: "lastModified:[\"2026-01-01\" TO \"2026-03-15\"]"
69
+ filter: "lastModified:[\"2026-01-01\" TO *]" # >= date
70
+ filter: "amount:[100 TO 500]"
71
+ ```
72
+
73
+ ### NULL checks
74
+ ```
75
+ filter: "customValues.po:NULL"
76
+ ```
77
+
78
+ ### CustomValues (JSONB fields)
79
+ ```
80
+ filter: "customValues.fieldName:value"
81
+ filter: "customValues.fieldName:\"exact match\""
82
+ filter: "customValues.fieldName:NULL"
83
+ ```
84
+
85
+ ### Filtered collections (bracket notation)
86
+ ```
87
+ filter: "children[category:CatA].name:test"
88
+ ```
89
+
90
+ ## OrderBy Syntax
91
+
92
+ ```
93
+ orderBy: "orderNumber" # Ascending
94
+ orderBy: "-orderNumber" # Descending
95
+ orderBy: "-created,orderNumber" # Multi-field
96
+ orderBy: "customValues.fieldName" # Custom field sort
97
+ orderBy: "orderNumber~ToInt32" # Type conversion during sort
98
+ ```
99
+
100
+ ## Pagination
101
+
102
+ ```graphql
103
+ {
104
+ orders(organizationId: 1, take: 50, skip: 0) {
105
+ items { orderId orderNumber }
106
+ totalCount
107
+ pageInfo { hasNextPage hasPreviousPage }
108
+ }
109
+ }
110
+ ```
111
+
112
+ ## Audit Change History
113
+
114
+ ### Entity-level: `changeHistory` computed field
115
+
116
+ Available on: **Order**, **Contact**, **Commodity**, **AccountingTransaction**
117
+
118
+ ```graphql
119
+ {
120
+ orders(organizationId: 1, filter: "orderId:196596", take: 1) {
121
+ items {
122
+ orderId
123
+ changeHistory(maxResults: 20) {
124
+ entityName
125
+ hasMoreRecords
126
+ continuationToken
127
+ changes {
128
+ state # "Added" | "Modified" | "Deleted"
129
+ userId
130
+ timestamp
131
+ user {
132
+ fullName
133
+ email
134
+ }
135
+ changedFields {
136
+ fieldName
137
+ originalValue
138
+ currentValue
139
+ fieldType
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }
146
+ ```
147
+
148
+ Arguments: `startDate: DateTime`, `endDate: DateTime`, `maxResults: Int` (default 10)
149
+
150
+ ### Root-level: `auditChanges` query
151
+
152
+ Query audit changes directly without fetching the entity first:
153
+
154
+ ```graphql
155
+ {
156
+ auditChanges(
157
+ organizationId: 1
158
+ filter: "entityName:Order AND primaryKey:196596"
159
+ take: 20
160
+ ) {
161
+ items {
162
+ state
163
+ userId
164
+ timestamp
165
+ user { fullName email }
166
+ changedFields {
167
+ fieldName
168
+ originalValue
169
+ currentValue
170
+ fieldType
171
+ }
172
+ }
173
+ }
174
+ }
175
+ ```
176
+
177
+ Arguments: `organizationId: Int!`, `filter: String`, `search: String`, `take: Int`, `skip: Int`, `orderBy: String`
178
+
179
+ Filter fields: `entityName`, `primaryKey`, `userId`, `state`
180
+
181
+ ### Root-level: `auditChangeSummaries` query
182
+
183
+ High-level summary of changes (grouped by change event):
184
+
185
+ ```graphql
186
+ {
187
+ auditChangeSummaries(
188
+ organizationId: 1
189
+ startDate: "2026-03-01T00:00:00Z"
190
+ endDate: "2026-03-15T23:59:59Z"
191
+ maxResults: 50
192
+ ) {
193
+ items {
194
+ changeId
195
+ userId
196
+ timestamp
197
+ changePaths
198
+ organizationId
199
+ }
200
+ continuationToken
201
+ hasMoreRecords
202
+ }
203
+ }
204
+ ```
205
+
206
+ ### Root-level: `auditChange` — single change detail
207
+
208
+ ```graphql
209
+ {
210
+ auditChange(filePath: "<s3-file-path>") {
211
+ entityName
212
+ state
213
+ primaryKey
214
+ userId
215
+ timestamp
216
+ originalValues
217
+ currentValues
218
+ fields {
219
+ fieldName
220
+ originalValue
221
+ currentValue
222
+ fieldType
223
+ }
224
+ }
225
+ }
226
+ ```
227
+
228
+ ## GraphQL Type Reference
229
+
230
+ ### EntityAuditHistoryLightResult
231
+ | Field | Type |
232
+ |-------|------|
233
+ | `entityName` | `String` |
234
+ | `primaryKey` | `String` |
235
+ | `organizationId` | `Int` |
236
+ | `changes` | `[AuditChangeEntry!]!` |
237
+ | `continuationToken` | `String` |
238
+ | `hasMoreRecords` | `Boolean!` |
239
+
240
+ ### AuditChangeEntry
241
+ | Field | Type | Notes |
242
+ |-------|------|-------|
243
+ | `state` | `String` | "Added", "Modified", "Deleted" |
244
+ | `userId` | `String` | |
245
+ | `timestamp` | `DateTime!` | |
246
+ | `user` | `AuditUser` | Lazy-loaded resolver |
247
+ | `changedFields` | `[AuditChangeField!]!` | Lazy-loaded resolver |
248
+
249
+ ### AuditChangeField
250
+ | Field | Type |
251
+ |-------|------|
252
+ | `fieldName` | `String!` |
253
+ | `originalValue` | `Any` |
254
+ | `currentValue` | `Any` |
255
+ | `fieldType` | `String!` |
256
+
257
+ ### AuditUser
258
+ | Field | Type |
259
+ |-------|------|
260
+ | `id` | `String` |
261
+ | `firstName` | `String` |
262
+ | `lastName` | `String` |
263
+ | `fullName` | `String` |
264
+ | `userName` | `String` |
265
+ | `email` | `String` |
266
+
267
+ ## Discovering Fields
268
+
269
+ **Always discover field names before building a query.** Do not guess field names — use the tools below.
270
+
271
+ ### CLI: `cxtms gql` — schema exploration commands
272
+
273
+ ```bash
274
+ # List all query root fields (what you can query)
275
+ npx cxtms gql queries
276
+ npx cxtms gql queries --filter order
277
+
278
+ # List all mutation root fields
279
+ npx cxtms gql mutations
280
+ npx cxtms gql mutations --filter order
281
+
282
+ # List all types (filter by name)
283
+ npx cxtms gql types --filter audit
284
+ npx cxtms gql types --filter order
285
+
286
+ # Inspect a specific type — shows fields, arguments, input fields, enum values
287
+ npx cxtms gql type OrderGqlDto
288
+ npx cxtms gql type AuditChangeEntry
289
+ npx cxtms gql type EntityAuditHistoryLightResult
290
+ npx cxtms gql type CreateOrderInput
291
+ ```
292
+
293
+ ### TMS MCP: `graphql_schema` / `graphql_type` — equivalent MCP tools
294
+
295
+ When TMS MCP is available, the same discovery is available via MCP tools:
296
+
297
+ ```
298
+ graphql_schema(section: "queries", filter: "order")
299
+ graphql_schema(section: "types", filter: "audit")
300
+ graphql_type(typeName: "OrderGqlDto")
301
+ graphql_execute(query: "{ orders(organizationId: 1, take: 1) { items { orderId } } }")
302
+ ```
303
+
304
+ ### Workflow: discover → query
305
+
306
+ 1. **Find the type** — `cxtms gql types --filter order` to find type names
307
+ 2. **Inspect the type** — `cxtms gql type OrderGqlDto` to see all fields and their types
308
+ 3. **Check query args** — `cxtms gql queries --filter order` to see arguments
309
+ 4. **Build and run** — `cxtms query '<graphql-query>'`
310
+
311
+ ### Fallback: error-driven discovery
312
+
313
+ GraphQL error messages reveal valid field/type names:
314
+ - "The field `foo` does not exist on the type `BarType`" — tells you the type name
315
+ - "The argument `bar` does not exist" — tells you valid argument patterns
316
+
317
+ ### Other sources
318
+
319
+ - `npx cxtms schema <name>` — shows workflow/module JSON schema fields (not GraphQL)
320
+ - Entity reference files (`ref-entity-*.md`) — document common fields, computed properties, and enums
package/dist/cli.js CHANGED
@@ -119,6 +119,7 @@ ${chalk_1.default.bold.yellow('COMMANDS:')}
119
119
  ${chalk_1.default.green('publish')} Publish all modules and workflows to a CX server
120
120
  ${chalk_1.default.green('app')} Manage app manifests (install/upgrade from git, publish to git, list)
121
121
  ${chalk_1.default.green('query')} Run a GraphQL query against the CX server
122
+ ${chalk_1.default.green('gql')} Explore GraphQL schema (types, queries, mutations)
122
123
  ${chalk_1.default.green('schema')} Show JSON schema for a component or task
123
124
  ${chalk_1.default.green('example')} Show example YAML for a component or task
124
125
  ${chalk_1.default.green('list')} List available schemas (modules, workflows, tasks)
@@ -336,6 +337,20 @@ ${chalk_1.default.bold.yellow('QUERY COMMANDS:')}
336
337
  ${chalk_1.default.gray('# Pass variables as JSON')}
337
338
  ${chalk_1.default.cyan(`${PROGRAM_NAME} query my-query.graphql --vars '{"id": 42}'`)}
338
339
 
340
+ ${chalk_1.default.bold.yellow('GRAPHQL SCHEMA EXPLORATION:')}
341
+ ${chalk_1.default.gray('# List all queries, mutations, and types')}
342
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} gql queries`)}
343
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} gql mutations`)}
344
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} gql types`)}
345
+
346
+ ${chalk_1.default.gray('# Filter by name')}
347
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} gql types --filter audit`)}
348
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} gql queries --filter order`)}
349
+
350
+ ${chalk_1.default.gray('# Inspect a specific type')}
351
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} gql type OrderGqlDto`)}
352
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} gql type AuditChangeEntry`)}
353
+
339
354
  ${chalk_1.default.bold.yellow('VALIDATION TYPES:')}
340
355
  ${chalk_1.default.bold('module')} - CargoXplorer UI module definitions (components, routes, entities)
341
356
  ${chalk_1.default.bold('workflow')} - CargoXplorer workflow definitions (activities, tasks, triggers)
@@ -3188,6 +3203,169 @@ async function runQuery(queryArg, variables) {
3188
3203
  console.log(JSON.stringify(data, null, 2));
3189
3204
  }
3190
3205
  // ============================================================================
3206
+ // GQL Schema Exploration Command
3207
+ // ============================================================================
3208
+ async function runGql(sub, filter) {
3209
+ if (!sub) {
3210
+ console.error(chalk_1.default.red('Error: subcommand required'));
3211
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} gql <queries|mutations|types|type> [name] [--filter <text>]`));
3212
+ process.exit(2);
3213
+ }
3214
+ const session = resolveSession();
3215
+ if (sub === 'type') {
3216
+ if (!filter) {
3217
+ console.error(chalk_1.default.red('Error: type name required'));
3218
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} gql type <TypeName>`));
3219
+ process.exit(2);
3220
+ }
3221
+ await runGqlType(session, filter);
3222
+ }
3223
+ else if (sub === 'queries') {
3224
+ await runGqlRootFields(session, 'queryType', filter);
3225
+ }
3226
+ else if (sub === 'mutations') {
3227
+ await runGqlRootFields(session, 'mutationType', filter);
3228
+ }
3229
+ else if (sub === 'types') {
3230
+ await runGqlTypes(session, filter);
3231
+ }
3232
+ else {
3233
+ console.error(chalk_1.default.red(`Unknown gql subcommand: ${sub}`));
3234
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} gql <queries|mutations|types|type> [--filter <text>]`));
3235
+ process.exit(2);
3236
+ }
3237
+ }
3238
+ function formatGqlType(t) {
3239
+ if (!t)
3240
+ return 'unknown';
3241
+ if (t.kind === 'NON_NULL')
3242
+ return `${formatGqlType(t.ofType)}!`;
3243
+ if (t.kind === 'LIST')
3244
+ return `[${formatGqlType(t.ofType)}]`;
3245
+ return t.name || 'unknown';
3246
+ }
3247
+ async function runGqlType(session, typeName) {
3248
+ const query = `{
3249
+ __type(name: "${typeName}") {
3250
+ name kind description
3251
+ fields { name description type { name kind ofType { name kind ofType { name kind ofType { name kind } } } } args { name type { name kind ofType { name kind ofType { name kind } } } defaultValue } }
3252
+ inputFields { name type { name kind ofType { name kind ofType { name kind } } } defaultValue }
3253
+ enumValues { name description }
3254
+ }
3255
+ }`;
3256
+ const data = await graphqlRequest(session.domain, session.access_token, query, {});
3257
+ const type = data.__type;
3258
+ if (!type) {
3259
+ console.error(chalk_1.default.red(`Type "${typeName}" not found`));
3260
+ process.exit(1);
3261
+ }
3262
+ console.log(chalk_1.default.bold.cyan(`${type.name}`) + chalk_1.default.gray(` (${type.kind})`));
3263
+ if (type.description)
3264
+ console.log(chalk_1.default.gray(type.description));
3265
+ console.log('');
3266
+ if (type.fields && type.fields.length > 0) {
3267
+ console.log(chalk_1.default.bold.yellow('Fields:'));
3268
+ for (const f of type.fields) {
3269
+ const typeStr = formatGqlType(f.type);
3270
+ let line = ` ${chalk_1.default.green(f.name)}: ${chalk_1.default.cyan(typeStr)}`;
3271
+ if (f.args && f.args.length > 0) {
3272
+ const argsStr = f.args.map((a) => {
3273
+ const argType = formatGqlType(a.type);
3274
+ return a.defaultValue ? `${a.name}: ${argType} = ${a.defaultValue}` : `${a.name}: ${argType}`;
3275
+ }).join(', ');
3276
+ line += chalk_1.default.gray(` (${argsStr})`);
3277
+ }
3278
+ if (f.description)
3279
+ line += chalk_1.default.gray(` — ${f.description}`);
3280
+ console.log(line);
3281
+ }
3282
+ }
3283
+ if (type.inputFields && type.inputFields.length > 0) {
3284
+ console.log(chalk_1.default.bold.yellow('Input Fields:'));
3285
+ for (const f of type.inputFields) {
3286
+ const typeStr = formatGqlType(f.type);
3287
+ let line = ` ${chalk_1.default.green(f.name)}: ${chalk_1.default.cyan(typeStr)}`;
3288
+ if (f.defaultValue)
3289
+ line += chalk_1.default.gray(` = ${f.defaultValue}`);
3290
+ console.log(line);
3291
+ }
3292
+ }
3293
+ if (type.enumValues && type.enumValues.length > 0) {
3294
+ console.log(chalk_1.default.bold.yellow('Enum Values:'));
3295
+ for (const v of type.enumValues) {
3296
+ let line = ` ${chalk_1.default.green(v.name)}`;
3297
+ if (v.description)
3298
+ line += chalk_1.default.gray(` — ${v.description}`);
3299
+ console.log(line);
3300
+ }
3301
+ }
3302
+ }
3303
+ async function runGqlRootFields(session, rootType, filter) {
3304
+ const query = `{
3305
+ __schema {
3306
+ ${rootType} {
3307
+ fields { name description args { name type { name kind ofType { name kind ofType { name kind } } } defaultValue } type { name kind ofType { name kind ofType { name kind } } } }
3308
+ }
3309
+ }
3310
+ }`;
3311
+ const data = await graphqlRequest(session.domain, session.access_token, query, {});
3312
+ const fields = data.__schema?.[rootType]?.fields || [];
3313
+ const filtered = filter
3314
+ ? fields.filter((f) => f.name.toLowerCase().includes(filter.toLowerCase()))
3315
+ : fields;
3316
+ const label = rootType === 'queryType' ? 'Queries' : 'Mutations';
3317
+ console.log(chalk_1.default.bold.yellow(`${label}${filter ? ` (filter: "${filter}")` : ''}:`));
3318
+ console.log('');
3319
+ for (const f of filtered) {
3320
+ const returnType = formatGqlType(f.type);
3321
+ console.log(` ${chalk_1.default.green(f.name)}: ${chalk_1.default.cyan(returnType)}`);
3322
+ if (f.description)
3323
+ console.log(` ${chalk_1.default.gray(f.description)}`);
3324
+ if (f.args && f.args.length > 0) {
3325
+ for (const a of f.args) {
3326
+ const argType = formatGqlType(a.type);
3327
+ const def = a.defaultValue ? chalk_1.default.gray(` = ${a.defaultValue}`) : '';
3328
+ console.log(` ${chalk_1.default.white(a.name)}: ${chalk_1.default.cyan(argType)}${def}`);
3329
+ }
3330
+ }
3331
+ console.log('');
3332
+ }
3333
+ console.log(chalk_1.default.gray(`${filtered.length} ${label.toLowerCase()} found`));
3334
+ }
3335
+ async function runGqlTypes(session, filter) {
3336
+ const query = `{
3337
+ __schema {
3338
+ types { name kind description }
3339
+ }
3340
+ }`;
3341
+ const data = await graphqlRequest(session.domain, session.access_token, query, {});
3342
+ const types = (data.__schema?.types || [])
3343
+ .filter((t) => !t.name.startsWith('__'))
3344
+ .filter((t) => !filter || t.name.toLowerCase().includes(filter.toLowerCase()));
3345
+ const grouped = {};
3346
+ for (const t of types) {
3347
+ const kind = t.kind || 'OTHER';
3348
+ if (!grouped[kind])
3349
+ grouped[kind] = [];
3350
+ grouped[kind].push(t);
3351
+ }
3352
+ const kindOrder = ['OBJECT', 'INPUT_OBJECT', 'ENUM', 'INTERFACE', 'UNION', 'SCALAR'];
3353
+ for (const kind of kindOrder) {
3354
+ const items = grouped[kind];
3355
+ if (!items || items.length === 0)
3356
+ continue;
3357
+ console.log(chalk_1.default.bold.yellow(`${kind} (${items.length}):`));
3358
+ for (const t of items.sort((a, b) => a.name.localeCompare(b.name))) {
3359
+ let line = ` ${chalk_1.default.green(t.name)}`;
3360
+ if (t.description)
3361
+ line += chalk_1.default.gray(` — ${t.description}`);
3362
+ console.log(line);
3363
+ }
3364
+ console.log('');
3365
+ }
3366
+ console.log(chalk_1.default.gray(`${types.length} types found${filter ? ` matching "${filter}"` : ''}`));
3367
+ }
3368
+ // ============================================================================
3191
3369
  // Extract Command
3192
3370
  // ============================================================================
3193
3371
  function runExtract(sourceFile, componentName, targetFile, copy) {
@@ -3398,7 +3576,7 @@ function parseArgs(args) {
3398
3576
  reportFormat: 'json'
3399
3577
  };
3400
3578
  // Check for commands
3401
- const commands = ['validate', 'schema', 'example', 'list', 'help', 'version', 'report', 'init', 'create', 'extract', 'sync-schemas', 'install-skills', 'update', 'setup-claude', 'login', 'logout', 'pat', 'appmodule', 'orgs', 'workflow', 'publish', 'query', 'app'];
3579
+ const commands = ['validate', 'schema', 'example', 'list', 'help', 'version', 'report', 'init', 'create', 'extract', 'sync-schemas', 'install-skills', 'update', 'setup-claude', 'login', 'logout', 'pat', 'appmodule', 'orgs', 'workflow', 'publish', 'query', 'gql', 'app'];
3402
3580
  if (args.length > 0 && commands.includes(args[0])) {
3403
3581
  command = args[0];
3404
3582
  args = args.slice(1);
@@ -3509,6 +3687,9 @@ function parseArgs(args) {
3509
3687
  options.file = [];
3510
3688
  options.file.push(args[++i]);
3511
3689
  }
3690
+ else if (arg === '--filter') {
3691
+ options.filter = args[++i];
3692
+ }
3512
3693
  else if (arg === '--force') {
3513
3694
  options.force = true;
3514
3695
  }
@@ -4435,6 +4616,14 @@ async function main() {
4435
4616
  }
4436
4617
  process.exit(0);
4437
4618
  }
4619
+ // Handle gql command (no schemas needed)
4620
+ if (command === 'gql') {
4621
+ const sub = files[0];
4622
+ // For 'gql type <name>', the type name is in files[1] — use it as filter
4623
+ const filterArg = sub === 'type' ? (files[1] || options.filter) : options.filter;
4624
+ await runGql(sub, filterArg);
4625
+ process.exit(0);
4626
+ }
4438
4627
  // Handle query command (no schemas needed)
4439
4628
  if (command === 'query') {
4440
4629
  await runQuery(files[0], options.vars);