datagrok-tools 6.5.6 → 6.5.8

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.
@@ -42,6 +42,11 @@ function tableProp(tableName) {
42
42
  return utils.snakeToCamelCase(pluralizeTableName(tableName), false);
43
43
  }
44
44
  const domainSchemaPath = _path.default.join(_path.default.dirname(_path.default.dirname(__dirname)), 'domain-schema.schema.json');
45
+ /** The platform's own schema, whose tables a qualified ref may target (`Core.users`, ...). */
46
+ const CORE_SCHEMA = 'Core';
47
+ /** The sealed Core declaration (a copy of core/server/db/snapshots/Core.json, pinned equal by the
48
+ * server's seal test) — what a `ref: <CORE_SCHEMA>.<table>` resolves against at build time. */
49
+ const coreDeclarationPath = _path.default.join(_path.default.dirname(_path.default.dirname(__dirname)), `${CORE_SCHEMA}.json`);
45
50
  const domainSystemColumns = [['id', 'string'], ['version', 'number'], ['created_on', 'Dayjs'], ['updated_on', 'Dayjs'], ['author_id', 'string']];
46
51
  const domainTypeMap = {
47
52
  string: 'string',
@@ -51,10 +56,22 @@ const domainTypeMap = {
51
56
  datetime: 'Dayjs',
52
57
  string_list: 'string[]',
53
58
  ref: 'string',
54
- user: 'string',
55
- group: 'string',
56
- file: 'string'
59
+ file: 'string',
60
+ json: '{[key: string]: any}'
57
61
  };
62
+
63
+ /** `type: user` / `type: group` are aliases of a Core ref (the server's
64
+ * `DomainTableColumn.canonicalize`); codegen spells them the same way so both generate alike. */
65
+ const coreRefAliases = {
66
+ user: `${CORE_SCHEMA}.users`,
67
+ group: `${CORE_SCHEMA}.groups`
68
+ };
69
+
70
+ /** The sealed declaration lists a table's columns and a property schema's columns as
71
+ * `[{name, ...}]`; manifests key them by name. */
72
+ function columnsByName(columns) {
73
+ return Array.isArray(columns) ? Object.fromEntries(columns.map(c => [c.name, c])) : columns;
74
+ }
58
75
  function normEol(s) {
59
76
  return s.replace(/\r\n/g, '\n');
60
77
  }
@@ -202,7 +219,7 @@ function generateDomainClients(packageDir = curDir, options) {
202
219
  for (const err of validateManifest.errors ?? []) color.error(`${relPath}: ${err.instancePath || '/'} ${err.message}`);
203
220
  return false;
204
221
  }
205
- const code = generateDomainSchemaCode(manifest, relPath, emittedTypes);
222
+ const code = generateDomainSchemaCode(manifest, relPath, emittedTypes, packageDir);
206
223
  if (code == null) return false;
207
224
  parts.push(code);
208
225
  if (ui) uiParts.push(generateDomainUiCode(manifest, dbImports));
@@ -220,6 +237,26 @@ function generateDomainClients(packageDir = curDir, options) {
220
237
  }
221
238
  return true;
222
239
  }
240
+ /** Subdirectory names of [dir], or none when it does not exist. */
241
+ function subdirs(dir) {
242
+ return _fs.default.existsSync(dir) ? _fs.default.readdirSync(dir, {
243
+ withFileTypes: true
244
+ }).filter(e => e.isDirectory()).map(e => e.name) : [];
245
+ }
246
+
247
+ /** The `databases/<schema>/schema.json` manifest named [schemaName] among the package's
248
+ * installed dependencies (scoped packages included), or null. */
249
+ function findDependencyManifest(packageDir, schemaName) {
250
+ const nm = _path.default.join(packageDir, 'node_modules');
251
+ const pkgDirs = subdirs(nm).flatMap(d => d.startsWith('@') ? subdirs(_path.default.join(nm, d)).map(s => _path.default.join(nm, d, s)) : [_path.default.join(nm, d)]);
252
+ for (const pkgDir of pkgDirs) for (const schemaDir of subdirs(_path.default.join(pkgDir, 'databases'))) {
253
+ const manifestPath = _path.default.join(pkgDir, 'databases', schemaDir, 'schema.json');
254
+ if (!_fs.default.existsSync(manifestPath)) continue;
255
+ const manifest = JSON.parse(_fs.default.readFileSync(manifestPath, 'utf8'));
256
+ if (manifest.name === schemaName) return manifest;
257
+ }
258
+ return null;
259
+ }
223
260
 
224
261
  /** A resolved many-to-many relation of one table: the expand key and the target
225
262
  * table whose row ids its write-side link set carries. */
@@ -227,10 +264,12 @@ function generateDomainClients(packageDir = curDir, options) {
227
264
  /** Emits choices aliases, row/insert interfaces, column-name unions, expand maps, the
228
265
  * `<Schema>TransactionOp` union, and the lazy `<schema>Db` clients for one manifest.
229
266
  * Returns null on a semantic error (reported to the console). */
230
- function generateDomainSchemaCode(manifest, manifestPath, emittedTypes) {
267
+ function generateDomainSchemaCode(manifest, manifestPath, emittedTypes, packageDir) {
231
268
  const decls = [];
232
269
  const systemColumnNames = new Set(domainSystemColumns.map(([name]) => name));
233
270
  const tableNames = Object.keys(manifest.tables);
271
+ // Keyed by table name; a qualified ref target ('Core.queries', 'grit.issue') lands here
272
+ // under its qualified key so the expand map can list its columns — it gets no accessor.
234
273
  const tableColumns = {};
235
274
  // Choices aliases are deduplicated by name: identical value sets share the first alias,
236
275
  // different sets fall back to a `<alias><PascalTable>` name (deterministic).
@@ -250,57 +289,112 @@ function generateDomainSchemaCode(manifest, manifestPath, emittedTypes) {
250
289
  return alias;
251
290
  };
252
291
 
253
- // Pass 1: resolve every table's full column list (relational + property-schema columns).
254
- for (const tableName of tableNames) {
255
- const table = manifest.tables[tableName];
256
- const typeName = utils.snakeToCamelCase(tableName);
257
- if (emittedTypes.has(typeName)) {
258
- color.error(`${manifestPath}: table '${tableName}' emits interface '${typeName}Row' ` + `already generated for another table (table names must be unique across the package's manifests)`);
259
- return null;
292
+ // A qualified ref target, resolved from files: `Core.<table>` from the CLI's sealed Core
293
+ // declaration, any other schema from a dependency's databases/*/schema.json.
294
+ const externalManifests = new Map();
295
+ function loadExternalTable(ref, where) {
296
+ if (tableColumns[ref] != null) return true;
297
+ const [schemaName, tableName] = ref.split('.');
298
+ const core = schemaName === CORE_SCHEMA;
299
+ if (!externalManifests.has(schemaName)) externalManifests.set(schemaName, core ? _fs.default.existsSync(coreDeclarationPath) ? JSON.parse(_fs.default.readFileSync(coreDeclarationPath, 'utf8')) : null : findDependencyManifest(packageDir, schemaName));
300
+ const owner = externalManifests.get(schemaName);
301
+ if (owner == null) {
302
+ color.error(`${where} references '${ref}', but ` + (core ? `the sealed ${CORE_SCHEMA} declaration is missing: ${coreDeclarationPath}` : `no installed dependency declares schema '${schemaName}' — install the package that declares ` + `schema '${schemaName}' as a dependency`));
303
+ return false;
260
304
  }
261
- emittedTypes.add(typeName);
305
+ if (owner.tables[tableName] == null) {
306
+ color.error(`${where} references '${ref}', but schema '${schemaName}' declares no table '${tableName}'`);
307
+ return false;
308
+ }
309
+ const columns = collectColumns(tableName, {
310
+ ...owner,
311
+ name: schemaName
312
+ }, true);
313
+ if (columns == null) return false;
314
+ tableColumns[ref] = columns;
315
+ return true;
316
+ }
317
+
318
+ // Pass 1: resolve every table's full column list (relational + property-schema columns).
319
+ // [owner] is the manifest the table comes from; an external (qualified-ref target) table
320
+ // skips the system-column check — its declaration may legitimately name one (Core.packages
321
+ // has `version`) — and its own refs are never followed (no nested expand).
322
+ function collectColumns(tableName, owner, external) {
323
+ const table = owner.tables[tableName];
324
+ const key = external ? `${owner.name}.${tableName}` : tableName;
262
325
  const columns = [];
263
- const columnNames = new Set(systemColumnNames);
326
+ const columnNames = new Set(external ? [] : systemColumnNames);
264
327
  const addColumn = (name, column) => {
328
+ if (coreRefAliases[column.type] != null) {
329
+ column = {
330
+ ...column,
331
+ type: 'ref',
332
+ ref: coreRefAliases[column.type]
333
+ };
334
+ delete column.onDelete;
335
+ }
265
336
  if (columnNames.has(name)) {
266
- color.error(systemColumnNames.has(name) ? `${manifestPath}: table '${tableName}' column '${name}' collides with a generated system column` : `${manifestPath}: table '${tableName}' declares duplicate column '${name}'`);
337
+ color.error(systemColumnNames.has(name) && !external ? `${manifestPath}: table '${key}' column '${name}' collides with a generated system column` : `${manifestPath}: table '${key}' declares duplicate column '${name}'`);
267
338
  return false;
268
339
  }
269
340
  columnNames.add(name);
270
341
  let tsType = domainTypeMap[column.type];
271
342
  if (Array.isArray(column.choices) && column.choices.length > 0) {
272
- const alias = choicesAlias(tableName, name, column.choices);
343
+ const alias = choicesAlias(key.replace('.', '_'), name, column.choices);
273
344
  if (alias == null) return false;
274
345
  tsType = alias;
275
346
  }
347
+ let ref;
348
+ if (column.type === 'ref' && !external) {
349
+ if (column.ref == null) {
350
+ color.error(`${manifestPath}: table '${tableName}' column '${name}' is a ref column without a 'ref' target`);
351
+ return false;
352
+ }
353
+ if (column.ref.includes('.')) {
354
+ if (!loadExternalTable(column.ref, `${manifestPath}: table '${tableName}' column '${name}'`)) return false;
355
+ ref = column.ref;
356
+ } else if (manifest.tables[column.ref] != null) ref = column.ref;
357
+ }
276
358
  columns.push({
277
359
  name: name,
278
360
  rawType: column.type,
279
361
  tsType: tsType,
280
362
  insertType: column.type === 'datetime' ? 'Dayjs | string' : tsType,
281
363
  required: column.required === true,
282
- ref: column.type === 'ref' && manifest.tables[column.ref] != null ? column.ref : undefined
364
+ autoNumber: column.autoNumber != null,
365
+ ref: ref
283
366
  });
284
367
  return true;
285
368
  };
286
- for (const columnName of Object.keys(table.columns)) if (!addColumn(columnName, table.columns[columnName])) return null;
369
+ const declared = columnsByName(table.columns);
370
+ for (const columnName of Object.keys(declared)) if (!addColumn(columnName, declared[columnName])) return null;
287
371
  for (const schemaName of table.schemas ?? []) {
288
- const props = manifest.propertySchemas?.[schemaName];
372
+ const props = columnsByName(owner.propertySchemas?.[schemaName]);
289
373
  if (props == null) {
290
- color.error(`${manifestPath}: table '${tableName}' references unknown property schema '${schemaName}'`);
374
+ color.error(`${manifestPath}: table '${key}' references unknown property schema '${schemaName}'`);
291
375
  return null;
292
376
  }
293
377
  for (const propName of Object.keys(props)) if (!addColumn(propName, props[propName])) return null;
294
378
  }
379
+ return columns;
380
+ }
381
+ for (const tableName of tableNames) {
382
+ const typeName = utils.snakeToCamelCase(tableName);
383
+ if (emittedTypes.has(typeName)) {
384
+ color.error(`${manifestPath}: table '${tableName}' emits interface '${typeName}Row' ` + `already generated for another table (table names must be unique across the package's manifests)`);
385
+ return null;
386
+ }
387
+ emittedTypes.add(typeName);
388
+ const columns = collectColumns(tableName, manifest, false);
389
+ if (columns == null) return null;
295
390
  tableColumns[tableName] = columns;
296
391
  }
297
392
 
298
- // Pass 1.5: resolve declared many-to-many relations. Same rules the server's
299
- // manifest parser applies (manifest.dart 'relations'), so a manifest that
300
- // generates here is one that deploys: via/target declared in this manifest,
301
- // via distinct from owner and target, self-referential relations explicit,
302
- // FK sides unique-or-explicit, and a junction business key covering both
303
- // that key is what keeps re-linking the same pair idempotent.
393
+ // Pass 1.5: declared many-to-many relations. Only what the generated code needs
394
+ // is checked here: the target table must be declared (its name lands in the
395
+ // generated doc comments) and the name must not collide with a column (relations
396
+ // and columns share one expand/filter namespace). Everything else about a
397
+ // relation (via, viaSelf/viaTarget, the junction business key) is the deploy's gate.
304
398
  const tableRelations = {};
305
399
  for (const tableName of tableNames) {
306
400
  const relations = [];
@@ -315,53 +409,10 @@ function generateDomainSchemaCode(manifest, manifestPath, emittedTypes) {
315
409
  color.error(`${where} collides with a column of '${tableName}' — relations and columns ` + `share one expand/filter namespace`);
316
410
  return null;
317
411
  }
318
- if (manifest.tables[r.via] == null) {
319
- color.error(`${where}: junction table '${r.via}' is not declared in this manifest`);
320
- return null;
321
- }
322
412
  if (manifest.tables[r.target] == null) {
323
413
  color.error(`${where}: target table '${r.target}' is not declared in this manifest`);
324
414
  return null;
325
415
  }
326
- if (r.via === tableName || r.via === r.target) {
327
- color.error(`${where}: junction table '${r.via}' must differ from the owner and the target table`);
328
- return null;
329
- }
330
- if (r.target === tableName && (r.viaSelf == null || r.viaTarget == null)) {
331
- color.error(`${where}: a self-referential relation must name both 'viaSelf' and 'viaTarget'`);
332
- return null;
333
- }
334
- // One side of the junction: the declared column when explicit, otherwise
335
- // the single ref column of `via` pointing at `to`.
336
- const resolveSide = (key, explicit, to) => {
337
- const candidates = tableColumns[r.via].filter(c => c.ref === to).map(c => c.name);
338
- if (explicit != null) {
339
- if (candidates.includes(explicit)) return explicit;
340
- color.error(`${where}: '${explicit}' is not a ref column of junction table '${r.via}' ` + `targeting '${to}'`);
341
- return null;
342
- }
343
- if (candidates.length === 0) {
344
- color.error(`${where}: junction table '${r.via}' has no ref column targeting '${to}'`);
345
- return null;
346
- }
347
- if (candidates.length > 1) {
348
- color.error(`${where}: junction table '${r.via}' has more than one ref column targeting ` + `'${to}' (${candidates.join(', ')}) — declare '${key}' explicitly`);
349
- return null;
350
- }
351
- return candidates[0];
352
- };
353
- const viaSelf = resolveSide('viaSelf', r.viaSelf, tableName);
354
- const viaTarget = resolveSide('viaTarget', r.viaTarget, r.target);
355
- if (viaSelf == null || viaTarget == null) return null;
356
- if (viaSelf === viaTarget) {
357
- color.error(`${where}: 'viaSelf' and 'viaTarget' must be different columns of '${r.via}'`);
358
- return null;
359
- }
360
- const businessKey = manifest.tables[r.via].businessKey ?? [];
361
- if (!businessKey.includes(viaSelf) || !businessKey.includes(viaTarget)) {
362
- color.error(`${where}: junction table '${r.via}' must declare a 'businessKey' containing ` + `both '${viaSelf}' and '${viaTarget}', so linking the same pair twice stays idempotent`);
363
- return null;
364
- }
365
416
  relations.push({
366
417
  name: name,
367
418
  target: r.target
@@ -379,7 +430,7 @@ function generateDomainSchemaCode(manifest, manifestPath, emittedTypes) {
379
430
  const relations = tableRelations[tableName];
380
431
  const rowLines = [`/** Row of \`${manifest.name}.${tableName}\`. */`, `export interface ${typeName}Row {`];
381
432
  for (const [name, tsType] of domainSystemColumns) rowLines.push(` ${name}: ${tsType};`);
382
- for (const c of columns) rowLines.push(` ${c.name}${c.required ? '' : '?'}: ${c.tsType};`);
433
+ for (const c of columns) rowLines.push(` ${c.name}${c.required || c.autoNumber ? '' : '?'}: ${c.tsType};`);
383
434
  rowLines.push('}');
384
435
  decls.push(rowLines.join(sep));
385
436
  const insertLines = [`/** Insert payload for \`${manifest.name}.${tableName}\`. */`, `export interface ${typeName}Insert {`];
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.help = void 0;
7
+ var _server = require("./server");
7
8
  const HELP = `
8
9
  Usage: grok <command>
9
10
 
@@ -391,69 +392,6 @@ Examples:
391
392
 
392
393
  The instance name must match a server alias in ~/.grok/config.yaml.
393
394
  `;
394
- const HELP_SERVER = `
395
- Usage: grok server <entity> <verb> [args] [options]
396
- grok s <entity> <verb> [args] [options]
397
-
398
- Manage a Datagrok server from the command line.
399
-
400
- Entities:
401
- users, groups, functions, connections, queries, scripts, packages, reports, files, tables
402
-
403
- Verbs:
404
- list List entities
405
- get Get a single entity by ID or name
406
- delete Delete an entity by ID
407
-
408
- Special commands:
409
- grok s functions run <Name:func(args)> Call a function
410
- grok s functions list [--type <t>] [--language <l>] [--package <p>] [--filter <expr>]
411
- Type: script|query|function|package; language applies to scripts
412
- grok s files list [path] [-r] List files (recursive with -r)
413
- grok s shares add <entity> <group>[,<group>...] [--access View|Edit]
414
- Share an entity with groups
415
- grok s shares list <entity-id> List who an entity (UUID) is shared with
416
- grok s users save --json user.json Create or update a user from JSON
417
- grok s users block <id-or-login> Block a user from the platform
418
- grok s users unblock <id-or-login> Unblock a previously blocked user
419
- grok s groups save --json group.json [--save-relations]
420
- Create or update a group from JSON
421
- grok s connections save --json conn.json [--save-credentials]
422
- Create or update a connection from JSON
423
- grok s connections test <id-or-name> Test connectivity of an existing connection
424
- grok s connections test --json conn.json Test connectivity of a connection defined in JSON
425
- grok s tables upload <name> <file.csv> Upload a CSV as a Datagrok table
426
- grok s tables download <name-or-id> [-O <file>] Download a table as CSV (stdout by default)
427
- grok s raw <METHOD> <path> Hit any API endpoint
428
- grok s describe <entity-type> Show entity JSON schema
429
-
430
- Options:
431
- --host <alias|url> Server alias from config or full URL
432
- --output <format> Output format: table (default), json, csv, quiet
433
- --filter <text> Smart filter expression
434
- --limit <n> Page size (default: 50)
435
- --offset <n> Start offset (default: 0)
436
- -r, --recursive Recursive (for files list)
437
- --json <file> Read function parameters from JSON file
438
-
439
- Examples:
440
- grok s users list
441
- grok s connections list --filter "PostgreSQL" --output json
442
- grok s connections get <id>
443
- grok s connections delete <id>
444
- grok s connections save --json conn.json --save-credentials
445
- grok s connections test "JohnDoe:MyConnection"
446
- grok s connections test --json conn.json
447
- grok s users save --json user.json
448
- grok s groups save --json group.json --save-relations
449
- grok s shares add "JohnDoe:MyConnection" Chemists,Admins --access Edit
450
- grok s shares list <entity-uuid>
451
- grok s functions run 'Chem:smilesToMw("ccc")'
452
- grok s files list "System:AppData" -r
453
- grok s raw GET /api/users/current
454
- grok s describe connections
455
- grok s users list --host dev
456
- `;
457
395
  const help = exports.help = {
458
396
  add: HELP_ADD,
459
397
  api: HELP_API,
@@ -471,7 +409,7 @@ const help = exports.help = {
471
409
  test: HELP_TEST,
472
410
  testall: HELP_TESTALL,
473
411
  migrate: HELP_MIGRATE,
474
- server: HELP_SERVER,
475
- s: HELP_SERVER,
412
+ server: _server.HELP_SERVER,
413
+ s: _server.HELP_SERVER,
476
414
  help: HELP
477
415
  };