@datanimbus/dnio-mcp 1.0.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 (59) hide show
  1. package/Dockerfile +20 -0
  2. package/docs/README.md +35 -0
  3. package/docs/architecture.md +171 -0
  4. package/docs/authentication.md +74 -0
  5. package/docs/tools/apps.md +59 -0
  6. package/docs/tools/connectors.md +76 -0
  7. package/docs/tools/data-pipes.md +286 -0
  8. package/docs/tools/data-services.md +105 -0
  9. package/docs/tools/deployment-groups.md +152 -0
  10. package/docs/tools/plugins.md +94 -0
  11. package/docs/tools/records.md +97 -0
  12. package/docs/workflows.md +195 -0
  13. package/env.example +16 -0
  14. package/package.json +43 -0
  15. package/readme.md +144 -0
  16. package/src/clients/api-keys.js +10 -0
  17. package/src/clients/apps.js +13 -0
  18. package/src/clients/base-client.js +78 -0
  19. package/src/clients/bots.js +10 -0
  20. package/src/clients/connectors.js +30 -0
  21. package/src/clients/data-formats.js +40 -0
  22. package/src/clients/data-pipes.js +33 -0
  23. package/src/clients/deployment-groups.js +59 -0
  24. package/src/clients/formulas.js +10 -0
  25. package/src/clients/functions.js +10 -0
  26. package/src/clients/plugins.js +39 -0
  27. package/src/clients/records.js +51 -0
  28. package/src/clients/services.js +63 -0
  29. package/src/clients/user-groups.js +10 -0
  30. package/src/clients/users.js +10 -0
  31. package/src/examples/ai-sdk-client.js +165 -0
  32. package/src/examples/claude_desktop_config.json +34 -0
  33. package/src/examples/express-integration.js +181 -0
  34. package/src/index.js +283 -0
  35. package/src/schemas/schema-converter.js +179 -0
  36. package/src/services/auth-manager.js +277 -0
  37. package/src/services/dnio-client.js +40 -0
  38. package/src/services/service-registry.js +150 -0
  39. package/src/services/session-manager.js +161 -0
  40. package/src/stdio-bridge.js +185 -0
  41. package/src/tools/_helpers.js +32 -0
  42. package/src/tools/api-keys.js +5 -0
  43. package/src/tools/apps.js +185 -0
  44. package/src/tools/bots.js +5 -0
  45. package/src/tools/connectors.js +165 -0
  46. package/src/tools/data-formats.js +806 -0
  47. package/src/tools/data-pipes.js +1305 -0
  48. package/src/tools/data-service-registry.js +500 -0
  49. package/src/tools/deployment-groups.js +511 -0
  50. package/src/tools/formulas.js +5 -0
  51. package/src/tools/functions.js +5 -0
  52. package/src/tools/mcp-tools-registry.js +38 -0
  53. package/src/tools/plugins.js +250 -0
  54. package/src/tools/records.js +217 -0
  55. package/src/tools/services.js +476 -0
  56. package/src/tools/user-groups.js +5 -0
  57. package/src/tools/users.js +5 -0
  58. package/src/utils/constants.js +135 -0
  59. package/src/utils/logger.js +63 -0
@@ -0,0 +1,476 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const {z} = require('zod');
5
+ const {toolError} = require('./_helpers');
6
+ const {DATA_SERVICE_CREATION_SPEC} = require('../utils/constants');
7
+
8
+ // Pre-validate a data service name against the platform's expected pattern.
9
+ // Returns an error message string, or null if valid.
10
+ // - 2–40 chars
11
+ // - Must start with a letter
12
+ // - Letters, digits, underscore only (no hyphens, spaces, special chars)
13
+ function _validateServiceName(name) {
14
+ if (typeof name !== 'string' || name.length === 0) {
15
+ return "'name' is required and must be a non-empty string.";
16
+ }
17
+ if (name.length < 2 || name.length > 40) {
18
+ return `'${name}' length is ${name.length}; must be 2–40 characters.`;
19
+ }
20
+ if (!/^[A-Za-z]/.test(name)) {
21
+ return `'${name}' must start with a letter.`;
22
+ }
23
+ if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(name)) {
24
+ return `'${name}' contains invalid characters. Allowed: letters, digits, underscore. No hyphens, spaces, or special chars. Use camelCase (e.g. 'companyEmployees') or snake_case ('company_employees').`;
25
+ }
26
+ return null;
27
+ }
28
+
29
+ // ─── Definition normalization ───────────────────────────────────────────────
30
+ // Platform expects every field to carry a fixed set of properties (fieldLength,
31
+ // paddingChar, paddingPosition, _typeChanged, dataPath, dataPathSegs). LLM
32
+ // often emits sparse properties — this normalizer fills the defaults, wraps
33
+ // Array children in the required `_self` wrapper, and computes dataPaths.
34
+
35
+ const _PRIMITIVE_TYPES = new Set(['String', 'Number', 'Boolean', 'Date']);
36
+
37
+ // Wrap an Array's children in the `_self` wrapper if missing.
38
+ // - 1 primitive child → rename child to `_self` (its type becomes the array item type).
39
+ // - Multiple children OR object children → wrap them in a single `_self` Object.
40
+ function _wrapArrayChildren(node) {
41
+ if (node.type !== 'Array') return;
42
+ const def = Array.isArray(node.definition) ? node.definition : [];
43
+ if (def.length === 1 && def[0].key === '_self') return; // already wrapped
44
+
45
+ if (def.length === 0) {
46
+ node.definition = [{
47
+ key: '_self', type: 'String',
48
+ properties: {name: '_self'}
49
+ }];
50
+ return;
51
+ }
52
+
53
+ if (def.length === 1 && _PRIMITIVE_TYPES.has(def[0].type)) {
54
+ const child = def[0];
55
+ child.key = '_self';
56
+ child.properties = child.properties || {};
57
+ if (!child.properties.name) child.properties.name = '_self';
58
+ node.definition = [child];
59
+ return;
60
+ }
61
+
62
+ // Children represent fields of the array's item — wrap them in a _self Object.
63
+ node.definition = [{
64
+ key: '_self',
65
+ type: 'Object',
66
+ definition: def,
67
+ properties: {name: '_self'}
68
+ }];
69
+ }
70
+
71
+ // Recursive normalize: fill universal property defaults, compute dataPath, recurse.
72
+ function _normalizeNode(node, parentSegs) {
73
+ const isSelf = node.key === '_self';
74
+ const segs = isSelf ? [...parentSegs, '[#]'] : [...parentSegs, node.key];
75
+ const dataPath = segs.join('.').replace(/\.\[#\]/g, '[#]');
76
+
77
+ node.properties = node.properties || {};
78
+ const props = node.properties;
79
+
80
+ // Universal defaults present on every field in the working sample.
81
+ if (!('fieldLength' in props)) props.fieldLength = 10;
82
+ if (!('paddingChar' in props)) props.paddingChar = ' ';
83
+ if (!('paddingPosition' in props)) props.paddingPosition = 'suffix';
84
+ if (!props._typeChanged) props._typeChanged = node.type;
85
+
86
+ // Primitives carry an explicit default null when not set.
87
+ if (_PRIMITIVE_TYPES.has(node.type)) {
88
+ if (!('default' in props)) props.default = null;
89
+ }
90
+
91
+ // Date carries timezone metadata.
92
+ if (node.type === 'Date') {
93
+ if (!('defaultTimezone' in props)) props.defaultTimezone = 'zulu';
94
+ if (!('supportedTimezones' in props)) props.supportedTimezones = [];
95
+ }
96
+
97
+ // Computed paths are single source of truth — overwrite whatever LLM sent.
98
+ props.dataPath = dataPath;
99
+ props.dataPathSegs = segs;
100
+
101
+ if (node.type === 'Array') {
102
+ _wrapArrayChildren(node);
103
+ }
104
+ if (Array.isArray(node.definition)) {
105
+ for (const child of node.definition) {
106
+ _normalizeNode(child, segs);
107
+ }
108
+ }
109
+ }
110
+
111
+ // _id field gets the full property set the platform expects on top-level ID.
112
+ function _normalizeIdField(idNode) {
113
+ idNode.properties = idNode.properties || {};
114
+ const e = idNode.properties;
115
+ idNode.properties = {
116
+ label: e.label ?? null,
117
+ readonly: e.readonly ?? false,
118
+ errorMessage: e.errorMessage ?? null,
119
+ name: e.name ?? 'ID',
120
+ required: e.required ?? false,
121
+ disabled: e.disabled ?? false,
122
+ fieldLength: e.fieldLength ?? 10,
123
+ paddingChar: e.paddingChar ?? ' ',
124
+ paddingPosition: e.paddingPosition ?? 'suffix',
125
+ _description: e._description ?? null,
126
+ _typeChanged: 'id',
127
+ _isParrentArray: e._isParrentArray ?? null,
128
+ _isGrpParentArray: e._isGrpParentArray ?? null,
129
+ _detailedType: e._detailedType ?? '',
130
+ dataPath: '_id',
131
+ dataPathSegs: ['_id']
132
+ };
133
+ }
134
+
135
+ // Top-level normalize entrypoint — first field must be _id, rest pass through _normalizeNode.
136
+ function _normalizeServiceDefinition(definition) {
137
+ if (!Array.isArray(definition) || definition.length === 0) return;
138
+ for (let i = 0; i < definition.length; i++) {
139
+ const node = definition[i];
140
+ if (i === 0 && node.key === '_id') {
141
+ _normalizeIdField(node);
142
+ } else {
143
+ _normalizeNode(node, []);
144
+ }
145
+ }
146
+ }
147
+
148
+ // Inject `payload.role` — the platform's role configuration containing the three
149
+ // standard roles (No Access / Manage / View) with freshly generated IDs, plus a
150
+ // `fields` permission map mirroring `definition`. Skips if `payload.role` is already set.
151
+ function _ensureRolesAndFields(payload) {
152
+ if (payload.role && Array.isArray(payload.role.roles) && payload.role.fields) return null;
153
+
154
+ const rid = (prefix) => `${prefix}${crypto.randomInt(1_000_000_000, 9_999_999_999)}`;
155
+
156
+ const roles = [
157
+ {
158
+ noAccessRole: true,
159
+ id: rid('PNA_'),
160
+ name: 'No Access',
161
+ operations: [],
162
+ description: 'This role removes all access to the record'
163
+ },
164
+ {
165
+ manageRole: true,
166
+ id: rid('P'),
167
+ name: 'Manage',
168
+ operations: [{method: 'POST'}, {method: 'PUT'}, {method: 'DELETE'}, {method: 'GET'}],
169
+ description: 'This role entitles an authorized user to create, update or delete a record'
170
+ },
171
+ {
172
+ viewRole: true,
173
+ id: rid('P'),
174
+ name: 'View',
175
+ operations: [{method: 'GET'}],
176
+ description: 'This role entitles an authorized user to view the record'
177
+ }
178
+ ];
179
+
180
+ const permMap = {};
181
+ for (const r of roles) permMap[r.id] = 'R';
182
+ const fields = _buildFields(payload.definition || [], permMap);
183
+
184
+ payload.role = {roles, fields};
185
+
186
+ return {
187
+ injected: true,
188
+ roleIds: roles.map(r => ({name: r.name, id: r.id}))
189
+ };
190
+ }
191
+
192
+ function _buildFields(definition, permMap) {
193
+ const out = {};
194
+ for (const field of definition) {
195
+ if (!field || !field.key) continue;
196
+ if (field.type === 'Object' && Array.isArray(field.definition)) {
197
+ out[field.key] = _buildFields(field.definition, permMap);
198
+ } else {
199
+ out[field.key] = {_t: field.type, _p: permMap};
200
+ }
201
+ }
202
+ return out;
203
+ }
204
+
205
+ // Auto-fill default connectors on a data service create payload if missing.
206
+ // Returns null if the user already supplied both, otherwise an object describing what was filled.
207
+ async function _ensureConnectors(payload, dnioClient, appName) {
208
+ payload.connectors = payload.connectors || {};
209
+ const needsData = !payload.connectors.data?._id;
210
+ const needsFile = !payload.connectors.file?._id;
211
+ if (!needsData && !needsFile) return null;
212
+
213
+ const connectors = await dnioClient.connectors.list(appName);
214
+ const list = Array.isArray(connectors) ? connectors : [];
215
+ const pickDefault = (category) =>
216
+ list.find(c => c.category === category && c.options?.default && c.options?.isValid !== false)
217
+ || list.find(c => c.category === category);
218
+
219
+ const filled = {};
220
+ if (needsData) {
221
+ const db = pickDefault('DB');
222
+ if (!db) throw new Error('No DB connector available in this app — run list_connectors to inspect.');
223
+ payload.connectors.data = {_id: db._id, options: {}};
224
+ filled.data = {_id: db._id, name: db.name, type: db.type};
225
+ }
226
+ if (needsFile) {
227
+ const storage = pickDefault('STORAGE');
228
+ if (!storage) throw new Error('No STORAGE connector available in this app — run list_connectors to inspect.');
229
+ payload.connectors.file = {_id: storage._id};
230
+ filled.file = {_id: storage._id, name: storage.name, type: storage.type};
231
+ }
232
+ return filled;
233
+ }
234
+
235
+ module.exports = function registerServicesTools({server, dnioClient, registry, userContext}) {
236
+ server.registerTool(
237
+ 'get_data_service_spec',
238
+ {
239
+ title: 'Get Data Service Creation Spec',
240
+ description: 'Returns the schema spec for creating a data service — field types, hook formats, workflow config options, and a full example payload. MUST be called before create_data_service.',
241
+ inputSchema: {},
242
+ annotations: {readOnlyHint: true, idempotentHint: true}
243
+ },
244
+ async () => {
245
+ return {
246
+ content: [{
247
+ type: 'text',
248
+ text: JSON.stringify(DATA_SERVICE_CREATION_SPEC, null, 2)
249
+ }]
250
+ };
251
+ }
252
+ );
253
+
254
+ server.registerTool(
255
+ 'create_data_service',
256
+ {
257
+ title: 'Create Data Service',
258
+ description: `Create a new data service. The full creation grammar is in get_data_service_spec — fetch it for nuance — but the critical rules are inlined here so you don't need a round-trip first:
259
+
260
+ NAME RULES (platform-enforced; tool pre-validates and rejects on mismatch):
261
+ - 2–40 characters, must START with a letter.
262
+ - Allowed: letters, digits, underscore. NO hyphens, NO spaces, NO other special chars.
263
+ - Convention: camelCase (e.g. 'companyEmployees') or snake_case ('company_employees').
264
+ - 'company-employees' / 'Company Employees' / '1_employees' will be rejected.
265
+
266
+ DEFINITION REQUIREMENTS:
267
+ - First entry MUST be the _id field: { key: '_id', type: 'String', prefix: '<3-letter-prefix>', counter: 1001, properties: { name: 'ID', fieldLength: 10, _typeChanged: 'id' } }.
268
+ - Every field's properties MUST include _typeChanged (= field.type, except _id which uses 'id') and may include _description.
269
+ - Primitive variant flags (live in properties): richText: true, email: true, enum: [...], precision: <int>, currency: true, dateType: 'date'|'datetime-local'.
270
+ - Object: nested 'definition' array with child fields.
271
+ - Array: nested 'definition' array describing the item schema.
272
+
273
+ AUTO-INJECTED BY THIS TOOL (do not bother sending — they will be overridden):
274
+ - 'app' = currently selected app.
275
+ - 'connectors' = platform default DB + Storage if absent.
276
+ - 'role' = standard No Access / Manage / View roles + per-field permission map if absent.
277
+
278
+ Pass everything else (definition, hooks, workflowConfig, stateModel, defaults) per the spec. On 'Name is invalid' platform errors, check the rule above first.`,
279
+ inputSchema: {
280
+ data: z.string().describe('JSON payload matching the creation spec from get_data_service_spec')
281
+ },
282
+ annotations: {destructiveHint: false, idempotentHint: false}
283
+ },
284
+ async (params) => {
285
+ if (!registry.selectedApp) {
286
+ return {content: [{type: 'text', text: 'No app selected. Use list_apps → select_app first.'}], isError: true};
287
+ }
288
+ try {
289
+ const payload = JSON.parse(params.data);
290
+
291
+ // Pre-validate name BEFORE platform call — surface clear hint on common errors.
292
+ const nameError = _validateServiceName(payload.name);
293
+ if (nameError) {
294
+ return toolError(`Invalid 'name': ${nameError}`, new Error('client-side validation'));
295
+ }
296
+
297
+ payload.app = registry.selectedApp;
298
+
299
+ // Normalize definition: fill universal property defaults, wrap Array children
300
+ // in _self, compute dataPath/dataPathSegs. Required for the platform to accept.
301
+ _normalizeServiceDefinition(payload.definition);
302
+
303
+ dnioClient.setToken(userContext.token);
304
+
305
+ const autoFilled = await _ensureConnectors(payload, dnioClient, registry.selectedApp);
306
+ const rolesInfo = _ensureRolesAndFields(payload);
307
+ const result = await dnioClient.services.create(registry.selectedApp, payload);
308
+
309
+ const response = {result};
310
+ if (autoFilled) {
311
+ response.connectorsAutoFilled = autoFilled;
312
+ }
313
+ if (rolesInfo) {
314
+ response.rolesAutoFilled = rolesInfo;
315
+ }
316
+ return {content: [{type: 'text', text: JSON.stringify(response, null, 2)}]};
317
+ } catch (error) {
318
+ return toolError('Failed to create data service', error);
319
+ }
320
+ }
321
+ );
322
+
323
+ server.registerTool(
324
+ 'get_data_service',
325
+ {
326
+ title: 'Get Data Service',
327
+ description: `Get details of a data service by ID. Use 'select' to fetch only needed fields (comma-separated).
328
+ Common select patterns:
329
+ - Overview: "_id,name,description,status,attributeCount,version"
330
+ - Schema: "_id,name,definition,schemaFree"
331
+ - Hooks: "_id,name,preHooks,workflowHooks"
332
+ - Workflow: "_id,name,workflowConfig,stateModel"
333
+ - Full edit: "_id,name,description,definition,preHooks,workflowHooks,workflowConfig,stateModel,schemaFree,permanentDeleteData,disableInsights,enableSearchIndex,allowedFileTypes"
334
+ Use draft=true to get the unsaved draft version.`,
335
+ inputSchema: {
336
+ serviceId: z.string().min(1).describe('Service ID (e.g., SRVC13120)'),
337
+ select: z.string().optional().describe('Comma-separated fields to return. Omit to get all fields.'),
338
+ draft: z.boolean().optional().describe('If true, returns the draft version. Default: false')
339
+ },
340
+ annotations: {readOnlyHint: true, idempotentHint: true}
341
+ },
342
+ async (params) => {
343
+ try {
344
+ dnioClient.setToken(userContext.token);
345
+ const result = await dnioClient.services.get(
346
+ registry.selectedApp,
347
+ params.serviceId,
348
+ {draft: params.draft || false, select: params.select}
349
+ );
350
+ return {content: [{type: 'text', text: JSON.stringify(result, null, 2)}]};
351
+ } catch (error) {
352
+ return toolError(`Failed to get data service ${params.serviceId}`, error);
353
+ }
354
+ }
355
+ );
356
+
357
+ server.registerTool(
358
+ 'list_data_services',
359
+ {
360
+ title: 'List Data Services',
361
+ description: 'List all data services in the selected app. Use select to limit fields returned.',
362
+ inputSchema: {
363
+ select: z.string().optional().describe('Comma-separated fields. Default: "_id,name,description,status,attributeCount"'),
364
+ statusFilter: z.enum(['Active', 'Pending', 'Draft', 'Stopped']).optional().describe('Filter by status. Omit for all.')
365
+ },
366
+ annotations: {readOnlyHint: true, idempotentHint: true}
367
+ },
368
+ async (params) => {
369
+ try {
370
+ dnioClient.setToken(userContext.token);
371
+ const filter = params.statusFilter ? {status: params.statusFilter} : {};
372
+ const select = params.select || '_id,name,description,status,attributeCount';
373
+ const result = await dnioClient.services.list(registry.selectedApp, {filter, select});
374
+ return {content: [{type: 'text', text: JSON.stringify(result, null, 2)}]};
375
+ } catch (error) {
376
+ return toolError('Failed to list data services', error);
377
+ }
378
+ }
379
+ );
380
+
381
+ server.registerTool(
382
+ 'update_data_service',
383
+ {
384
+ title: 'Update Data Service',
385
+ description: 'Update an existing data service. Call get_data_service first to get the current state, modify what you need, and send the full payload back. After updating, call deploy_data_service to apply changes.',
386
+ inputSchema: {
387
+ serviceId: z.string().min(1).describe('Service ID (e.g., SRVC13120)'),
388
+ data: z.string().describe('Full JSON payload with updated fields. Must include the complete definition array, not just changed fields.')
389
+ },
390
+ annotations: {destructiveHint: true, idempotentHint: false}
391
+ },
392
+ async (params) => {
393
+ if (!registry.selectedApp) {
394
+ return {content: [{type: 'text', text: 'No app selected. Use list_apps → select_app first.'}], isError: true};
395
+ }
396
+ try {
397
+ const payload = JSON.parse(params.data);
398
+ payload.app = registry.selectedApp;
399
+
400
+ // Same normalization as create — keeps property defaults and dataPaths in sync.
401
+ _normalizeServiceDefinition(payload.definition);
402
+
403
+ dnioClient.setToken(userContext.token);
404
+ const result = await dnioClient.services.update(
405
+ registry.selectedApp,
406
+ params.serviceId,
407
+ payload
408
+ );
409
+ return {content: [{type: 'text', text: JSON.stringify(result, null, 2)}]};
410
+ } catch (error) {
411
+ return toolError(`Failed to update data service ${params.serviceId}`, error);
412
+ }
413
+ }
414
+ );
415
+
416
+ server.registerTool(
417
+ 'deploy_data_service',
418
+ {
419
+ title: 'Deploy Data Service',
420
+ description: 'Deploy a data service after creation or update. This starts the Kubernetes deployment. Must be called after create_data_service or update_data_service to make changes live.',
421
+ inputSchema: {
422
+ serviceId: z.string().min(1).describe('Service ID to deploy (e.g., SRVC13120)')
423
+ },
424
+ annotations: {destructiveHint: false, idempotentHint: true}
425
+ },
426
+ async (params) => {
427
+ try {
428
+ dnioClient.setToken(userContext.token);
429
+ const result = await dnioClient.services.deploy(registry.selectedApp, params.serviceId);
430
+ return {
431
+ content: [{
432
+ type: 'text',
433
+ text: JSON.stringify({
434
+ serviceId: params.serviceId,
435
+ status: 'Deployment initiated',
436
+ result
437
+ }, null, 2)
438
+ }]
439
+ };
440
+ } catch (error) {
441
+ return toolError(`Failed to deploy data service ${params.serviceId}`, error);
442
+ }
443
+ }
444
+ );
445
+
446
+ server.registerTool(
447
+ 'start_stop_data_service',
448
+ {
449
+ title: 'Start or Stop Data Service',
450
+ description: 'Start or stop a deployed data service.',
451
+ inputSchema: {
452
+ serviceId: z.string().min(1).describe('Service ID (e.g., SRVC13120)'),
453
+ action: z.enum(['start', 'stop']).describe('Whether to start or stop the service')
454
+ },
455
+ annotations: {destructiveHint: true, idempotentHint: true}
456
+ },
457
+ async (params) => {
458
+ try {
459
+ dnioClient.setToken(userContext.token);
460
+ const result = await dnioClient.services.startStop(
461
+ registry.selectedApp,
462
+ params.serviceId,
463
+ {start: params.action === 'start'}
464
+ );
465
+ return {
466
+ content: [{
467
+ type: 'text',
468
+ text: JSON.stringify({serviceId: params.serviceId, action: params.action, result}, null, 2)
469
+ }]
470
+ };
471
+ } catch (error) {
472
+ return toolError(`Failed to ${params.action} data service ${params.serviceId}`, error);
473
+ }
474
+ }
475
+ );
476
+ };
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ module.exports = function registerUserGroupsTools(_ctx) {
4
+ // TODO: register DNIO user groups tools
5
+ };
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ module.exports = function registerUsersTools(_ctx) {
4
+ // TODO: register DNIO users tools
5
+ };
@@ -0,0 +1,135 @@
1
+ const DATA_SERVICE_CREATION_SPEC = {
2
+ description: "Spec for creating a data service. Only include fields listed here.",
3
+
4
+ connectors: {
5
+ description: "Required. Specifies WHERE the data service stores its records and uploaded files. Call 'list_connectors' first to fetch available connectors and confirm with the user which to use. If the user does not specify, omit this field — create_data_service will auto-attach the platform default DB + Storage connectors.",
6
+ shape: {
7
+ data: {
8
+ _id: "Connector _id with category=DB (e.g. 'CON3112' MongoDB). REQUIRED.",
9
+ options: "Empty object {} unless the connector requires extra options."
10
+ },
11
+ file: {
12
+ _id: "Connector _id with category=STORAGE (e.g. 'CON3113' GridFS). REQUIRED."
13
+ }
14
+ },
15
+ example: {
16
+ data: {_id: "CON3112", options: {}},
17
+ file: {_id: "CON3113"}
18
+ }
19
+ },
20
+
21
+ fieldTypes: {
22
+ String: {
23
+ properties: ["name", "_typeChanged", "_description", "required", "default", "email", "richText", "enum", "pattern", "minLength", "maxLength", "fieldLength"],
24
+ variants: {
25
+ Text: "(no extra flag)",
26
+ LongText: "richText: true",
27
+ RichText: "richText: true",
28
+ Email: "email: true",
29
+ ListOfValues: "enum: ['A','B','C']"
30
+ }
31
+ },
32
+ Number: {
33
+ properties: ["name", "_typeChanged", "_description", "required", "default", "precision", "enum", "currency", "min", "max", "fieldLength"],
34
+ variants: {
35
+ Number: "precision: <int>",
36
+ ListOfNumber: "precision: <int>, enum: [1,2,3]",
37
+ Currency: "currency: true"
38
+ }
39
+ },
40
+ Boolean: {properties: ["name", "_typeChanged", "_description", "required", "default"]},
41
+ Date: {
42
+ properties: ["name", "_typeChanged", "_description", "required", "default", "dateType"],
43
+ variants: {
44
+ DateOnly: "dateType: 'date'",
45
+ DateTime: "dateType: 'datetime-local'"
46
+ }
47
+ },
48
+ Object: {properties: ["name", "_typeChanged", "_description"], note: "Contains nested 'definition' array with child fields"},
49
+ Array: {properties: ["name", "_typeChanged", "_description"], note: "Set type of items inside 'definition'"}
50
+ },
51
+
52
+ sharedPropertyNote: "Two property keys are shared across data-format AND data-service field definitions and MUST always be present in 'properties': '_typeChanged' (stored type, same as field.type — except _id field which uses '_typeChanged: \"id\"') and '_description' (free-form description; NOT 'description'). Variant flags (richText, email, enum, currency, dateType) work identically on both surfaces.",
53
+
54
+ idField: {
55
+ note: "First field must always be _id with prefix. Counter starts at 1001.",
56
+ example: {
57
+ key: "_id", type: "String", prefix: "USR", suffix: null, padding: null, counter: 1001,
58
+ properties: {name: "ID", fieldLength: 10, _typeChanged: "id"}
59
+ }
60
+ },
61
+
62
+ hooks: {
63
+ preHooks: {
64
+ description: "Executed before save. Array of hook objects.",
65
+ example: {name: "ValidateData", url: "https://example.com/webhook", type: "external", failMessage: ""}
66
+ },
67
+ workflowHooks: {
68
+ description: "Post-workflow hooks triggered on submit/approve/reject/rework/discard.",
69
+ triggers: ["submit", "approve", "reject", "rework", "discard"],
70
+ example: {name: "NotifyOnSubmit", url: "https://example.com/webhook", type: "external"}
71
+ }
72
+ },
73
+
74
+ stateModel: {
75
+ description: "Optional state machine on a field. Set enabled:true and define states.",
76
+ example: {
77
+ attribute: "status",
78
+ enabled: true,
79
+ initialStates: ["New"],
80
+ states: {New: ["Active", "Inactive"], Active: ["Inactive"]}
81
+ }
82
+ },
83
+
84
+ workflowConfig: {
85
+ description: "Maker-checker approval workflow.",
86
+ example: {enabled: true, makerCheckers: [{steps: [{name: "Approve", approvers: []}]}]}
87
+ },
88
+
89
+ defaults: {
90
+ schemaFree: false,
91
+ permanentDeleteData: true,
92
+ disableInsights: false,
93
+ enableSearchIndex: false,
94
+ allowedFileTypes: ["jpeg", "jpg", "png", "pdf", "csv", "xlsx", "json", "xml", "txt"]
95
+ },
96
+
97
+ examplePayload: {
98
+ name: "studentRecord",
99
+ description: "Stores student information for the school",
100
+ app: "<app-name>",
101
+ connectors: {
102
+ data: {_id: "CON3112", options: {}},
103
+ file: {_id: "CON3113"}
104
+ },
105
+ definition: [
106
+ {
107
+ key: "_id",
108
+ type: "String",
109
+ prefix: "STU",
110
+ suffix: null,
111
+ padding: null,
112
+ counter: 1001,
113
+ properties: {name: "ID", fieldLength: 10, _typeChanged: "id"}
114
+ },
115
+ {key: "fullName", type: "String", properties: {name: "fullName", required: true, _typeChanged: "String", _description: "Student full name"}},
116
+ {key: "email", type: "String", properties: {name: "email", email: true, _typeChanged: "String", _description: "Contact email"}},
117
+ {key: "grade", type: "Number", properties: {name: "grade", precision: 0, _typeChanged: "Number", _description: "Current grade"}},
118
+ {
119
+ key: "address", type: "Object", definition: [
120
+ {key: "city", type: "String", properties: {name: "city", _typeChanged: "String"}},
121
+ {key: "zip", type: "String", properties: {name: "zip", _typeChanged: "String"}}
122
+ ], properties: {name: "address", _typeChanged: "Object"}
123
+ }
124
+ ],
125
+ preHooks: [],
126
+ workflowHooks: {postHooks: {submit: [], discard: [], approve: [], rework: [], reject: []}},
127
+ stateModel: {attribute: "", initialStates: [], enabled: false},
128
+ workflowConfig: {enabled: false, makerCheckers: []}
129
+ }
130
+ };
131
+
132
+
133
+ module.exports = {
134
+ DATA_SERVICE_CREATION_SPEC
135
+ }