datagrok-tools 6.5.4 → 6.5.6

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.
@@ -78,6 +78,8 @@ Usage: grok add <entity> <name>
78
78
  Add an object template to your package:
79
79
 
80
80
  grok add app <name>
81
+ grok add app [name] --domain <schema>[.<table>]
82
+ grok add app [name] --domain <path to schema.json>
81
83
  grok add connection <name>
82
84
  grok add detector <semantic-type-name>
83
85
  grok add function [tag] <name>
@@ -89,6 +91,15 @@ grok add tests
89
91
 
90
92
  Please note that entity names may only include letters and numbers
91
93
 
94
+ --domain scaffolds a working browse/CRUD app over an entity-mapped domain table
95
+ (\`grok.dapi.domains\`) from the \`@datagrok-libraries/domain-ui\` defaults. Give it
96
+ one table (\`--domain grit.issue\`), a whole schema the package declares in
97
+ \`databases/<schema>/schema.json\` (one app per table), or the path to a schema
98
+ manifest to copy into the package. A fresh app package is two commands:
99
+
100
+ grok create MyTracker
101
+ cd MyTracker && grok add app --domain grit.issue
102
+
92
103
  Supported languages for scripts:
93
104
  javascript, julia, node, octave, python, r
94
105
 
@@ -113,12 +124,18 @@ Options:
113
124
  const HELP_API = `
114
125
  Usage: grok api
115
126
 
116
- Create wrapper functions for package scripts and queries
127
+ Create wrapper functions for package scripts and queries.
128
+ Packages with \`databases/<schema>/schema.json\` manifests also get typed domain
129
+ clients in \`src/generated/db.ts\`.
117
130
 
118
131
  Options:
119
- [-v | --verbose]
132
+ [-v | --verbose] [--ui]
120
133
 
121
134
  --verbose Print detailed output
135
+ --ui Also generate \`src/generated/db-ui.ts\` — typed UI wrappers over
136
+ \`@datagrok-libraries/domain-ui\` for every domain table. Once the
137
+ file exists, plain \`grok api\` keeps it up to date; delete it to
138
+ opt out again
122
139
  `;
123
140
  const HELP_CONFIG = `
124
141
  Usage: grok config
@@ -73,6 +73,27 @@ function discoverDockerfiles(packageName, version, debug) {
73
73
  }
74
74
  return results;
75
75
  }
76
+
77
+ // Folders that name a published image in container.json instead of shipping a Dockerfile.
78
+ // Nothing is built or pushed for them; the server resolves the reference on deploy.
79
+ function referencedImageDirs() {
80
+ const dockerfilesDir = _path.default.join(curDir, 'dockerfiles');
81
+ if (!_fs.default.existsSync(dockerfilesDir)) return [];
82
+ const results = [];
83
+ for (const entry of _fs.default.readdirSync(dockerfilesDir, {
84
+ withFileTypes: true
85
+ })) {
86
+ if (!entry.isDirectory() || _fs.default.existsSync(_path.default.join(dockerfilesDir, entry.name, 'Dockerfile'))) continue;
87
+ const configPath = _path.default.join(dockerfilesDir, entry.name, 'container.json');
88
+ if (!_fs.default.existsSync(configPath)) continue;
89
+ const image = JSON.parse(_fs.default.readFileSync(configPath, 'utf-8')).image;
90
+ if (image) results.push({
91
+ name: entry.name,
92
+ image
93
+ });
94
+ }
95
+ return results;
96
+ }
76
97
  function dockerCommand(args) {
77
98
  return execSync(`docker ${args}`, {
78
99
  encoding: 'utf-8',
@@ -285,6 +306,7 @@ async function resolveLatestCompatible(host, devKey, dockerName, version, conten
285
306
  }
286
307
  }
287
308
  async function processDockerImages(packageName, version, registry, devKey, host, rebuildDocker, zip, localTimestamps, debug, skipDockerRebuild = false, generatedDirs = []) {
309
+ for (const dir of referencedImageDirs()) color.log(`dockerfiles/${dir.name} runs ${dir.image} — nothing to build`);
288
310
  const dockerImages = discoverDockerfiles(packageName, version, debug);
289
311
  if (dockerImages.length === 0) return;
290
312
  color.log(`Found ${dockerImages.length} Dockerfile(s)`);
@@ -367,10 +367,10 @@ async function handleTicket(args) {
367
367
  const issueType = args['type'] || 'Bug';
368
368
  const auth = jiraAuthHeader();
369
369
  if (auth == null) {
370
- color.error('JIRA_USER and JIRA_TOKEN env vars are required for `grok report ticket`.');
370
+ color.error('Set JIRA_TOKEN (plus JIRA_USER for a user API token) for `grok report ticket`.');
371
371
  return false;
372
372
  }
373
- const jiraBase = resolveJiraBase(args);
373
+ const jiraBase = await resolveJiraBase(args);
374
374
  try {
375
375
  const {
376
376
  url,
@@ -444,10 +444,23 @@ async function handleTicket(args) {
444
444
 
445
445
  // ─── JIRA REST helpers (used by `grok report comment` / `grok report label`) ─
446
446
  //
447
- // These talk DIRECTLY to Atlassian Cloud REST v2 (not Datagrok). Auth is HTTP
448
- // Basic with `JIRA_USER` (Atlassian email) + `JIRA_TOKEN` (API token from
449
- // id.atlassian.com/manage-profile/security/api-tokens). Base URL defaults to
450
- // the Datagrok org instance; override via --jira-url or $JIRA_URL.
447
+ // These talk DIRECTLY to Atlassian Cloud REST v2 (not Datagrok). Two auth schemes, picked from
448
+ // the token itself:
449
+ //
450
+ // * user API token (`ATATT...`, from id.atlassian.com/manage-profile/security/api-tokens)
451
+ // HTTP Basic with `JIRA_USER` + `JIRA_TOKEN`, against the site. The original path.
452
+ // * service-account token (`ATSTT...`, from admin.atlassian.com) — a SCOPED token. The site
453
+ // host will not take it: Basic gives 401, and Bearer gives 403 "Failed to parse Connect
454
+ // Session Auth Token" because Jira tries to read it as a Connect session. Only the API
455
+ // gateway accepts it, addressed by cloud id, with Bearer. No JIRA_USER is involved.
456
+ //
457
+ // Both are supported on purpose. Automation moved to a service account when its bot mailbox
458
+ // became a Google group (a group holds no Atlassian identity and cannot mint tokens), while
459
+ // release-notes CI and every developer's local setup still use user tokens.
460
+ //
461
+ // Base URL defaults to the Datagrok org instance; override via --jira-url or $JIRA_URL. That
462
+ // value stays the SITE — the gateway address is derived from it, never substituted for it, so
463
+ // human-facing links elsewhere keep working.
451
464
  //
452
465
  // Why v2 and not v3: v3 requires comment bodies in ADF (Atlassian Document
453
466
  // Format) JSON, which is much heavier to construct. v2 accepts a plain string
@@ -455,15 +468,49 @@ async function handleTicket(args) {
455
468
  // becomes a top-level ordered-list item, `&nbsp;` shows up literally, etc.
456
469
  // `markdownToJiraWiki` below bridges the gap for Markdown-emitting callers.
457
470
 
458
- function resolveJiraBase(args) {
471
+ const JIRA_SERVICE_TOKEN_RE = /^ATSTT/;
472
+ function isJiraServiceToken() {
473
+ return JIRA_SERVICE_TOKEN_RE.test(process.env.JIRA_TOKEN || '');
474
+ }
475
+
476
+ // The site, as configured. Human-facing links are built from this.
477
+ function resolveJiraSite(args) {
459
478
  const cli = args['jira-url'] || '';
460
479
  const env = process.env.JIRA_URL || '';
461
480
  return (cli || env || 'https://reddata.atlassian.net').replace(/\/+$/, '');
462
481
  }
482
+ let cloudIdCache = null;
483
+
484
+ // The gateway addresses a site by cloud id rather than hostname. Public endpoint, no credentials
485
+ // needed, cached for the process.
486
+ async function resolveJiraCloudId(site) {
487
+ if (process.env.JIRA_CLOUD_ID) return process.env.JIRA_CLOUD_ID.trim();
488
+ if (cloudIdCache !== null) return cloudIdCache;
489
+ try {
490
+ const r = await fetch(`${site}/_edge/tenant_info`);
491
+ const j = r.ok ? await r.json() : {};
492
+ cloudIdCache = (j.cloudId || '').trim();
493
+ } catch {
494
+ cloudIdCache = '';
495
+ }
496
+ return cloudIdCache;
497
+ }
498
+
499
+ // Where REST calls go. A service token needs the gateway; anything else keeps the site. If the
500
+ // cloud id cannot be resolved, fall back to the site rather than failing outright — the request
501
+ // then reports a normal auth error instead of a confusing lookup one.
502
+ async function resolveJiraBase(args) {
503
+ const site = resolveJiraSite(args);
504
+ if (!isJiraServiceToken()) return site;
505
+ const cloudId = await resolveJiraCloudId(site);
506
+ return cloudId ? `https://api.atlassian.com/ex/jira/${cloudId}` : site;
507
+ }
463
508
  function jiraAuthHeader() {
464
- const user = process.env.JIRA_USER;
465
509
  const token = process.env.JIRA_TOKEN;
466
- if (!user || !token) return null;
510
+ if (!token) return null;
511
+ if (isJiraServiceToken()) return `Bearer ${token}`;
512
+ const user = process.env.JIRA_USER;
513
+ if (!user) return null;
467
514
  return 'Basic ' + Buffer.from(`${user}:${token}`).toString('base64');
468
515
  }
469
516
 
@@ -544,7 +591,7 @@ async function handleComment(args) {
544
591
  }
545
592
  const auth = jiraAuthHeader();
546
593
  if (auth == null) {
547
- color.error('JIRA_USER and JIRA_TOKEN env vars are required for `grok report comment`.');
594
+ color.error('Set JIRA_TOKEN (plus JIRA_USER for a user API token) for `grok report comment`.');
548
595
  return false;
549
596
  }
550
597
  let body;
@@ -566,7 +613,7 @@ async function handleComment(args) {
566
613
  color.error('Comment body is empty (use --body, --body-file, or pipe to stdin).');
567
614
  return false;
568
615
  }
569
- const base = resolveJiraBase(args);
616
+ const base = await resolveJiraBase(args);
570
617
  const url = `${base}/rest/api/2/issue/${encodeURIComponent(ticket)}/comment`;
571
618
  // Callers (especially the dg-fix-reports M2 handoff) emit Markdown, but JIRA
572
619
  // REST v2 renders the body as wiki markup. Convert before posting so headings,
@@ -614,13 +661,15 @@ async function handleAttach(args) {
614
661
  color.error(`File not found: ${filePath}`);
615
662
  return false;
616
663
  }
617
- const user = process.env.JIRA_USER;
618
- const token = process.env.JIRA_TOKEN;
619
- if (!user || !token) {
620
- color.error('JIRA_USER and JIRA_TOKEN env vars are required for `grok report attach`.');
664
+
665
+ // Through the same helper as every other call: a service token authenticates with Bearer and
666
+ // has no JIRA_USER at all, so checking for one here would refuse a working setup.
667
+ const auth = jiraAuthHeader();
668
+ if (auth == null) {
669
+ color.error('Set JIRA_TOKEN (plus JIRA_USER for a user API token) for `grok report attach`.');
621
670
  return false;
622
671
  }
623
- const base = resolveJiraBase(args);
672
+ const base = await resolveJiraBase(args);
624
673
  const url = `${base}/rest/api/2/issue/${encodeURIComponent(ticket)}/attachments`;
625
674
 
626
675
  // node-fetch v2 has no built-in FormData and the codebase doesn't depend on
@@ -630,7 +679,7 @@ async function handleAttach(args) {
630
679
  const {
631
680
  spawnSync
632
681
  } = require('child_process');
633
- const r = spawnSync('curl', ['-sS', '-X', 'POST', '-u', `${user}:${token}`, '-H', 'X-Atlassian-Token: no-check', '-F', `file=@${filePath}`, '-w', '\n%{http_code}\n', url], {
682
+ const r = spawnSync('curl', ['-sS', '-X', 'POST', '-H', `Authorization: ${auth}`, '-H', 'X-Atlassian-Token: no-check', '-F', `file=@${filePath}`, '-w', '\n%{http_code}\n', url], {
634
683
  encoding: 'utf8',
635
684
  timeout: 120_000,
636
685
  maxBuffer: 4 * 1024 * 1024
@@ -671,10 +720,10 @@ async function handleLabel(args) {
671
720
  }
672
721
  const auth = jiraAuthHeader();
673
722
  if (auth == null) {
674
- color.error('JIRA_USER and JIRA_TOKEN env vars are required for `grok report label`.');
723
+ color.error('Set JIRA_TOKEN (plus JIRA_USER for a user API token) for `grok report label`.');
675
724
  return false;
676
725
  }
677
- const base = resolveJiraBase(args);
726
+ const base = await resolveJiraBase(args);
678
727
  const url = `${base}/rest/api/2/issue/${encodeURIComponent(ticket)}`;
679
728
  const update = {
680
729
  labels: labels.map(l => ({
@@ -8,6 +8,18 @@ const app = appName => `
8
8
  The application ${appName} has been added successfully
9
9
  Read more at https://datagrok.ai/help/develop/how-to/build-an-app
10
10
  See application examples at https://public.datagrok.ai/apps`;
11
+ const domainApp = (appNames, tables) => `
12
+ The application${appNames.length > 1 ? 's' : ''} ${appNames.join(', ')} ` + `(over ${tables.join(', ')}) ${appNames.length > 1 ? 'have' : 'has'} been added successfully
13
+
14
+ Next steps:
15
+ npm install install @datagrok-libraries/domain-ui
16
+ npm run build && grok publish publish the package and open the app from the browse tree
17
+
18
+ The app is the domain-ui defaults alone — list, search, entity page, editing,
19
+ permissions and deep links. Customize through the options of the handle's factories
20
+ (domains.table(...).app({actions: ...})), or register a DG.DomainObjectHandler for
21
+ the table to change how its rows render.
22
+ Read more at https://datagrok.ai/help/develop/how-to/build-an-app`;
11
23
  const connection = connectionName => `
12
24
  The connection ${connectionName} has been added successfully
13
25
  Read more at https://datagrok.ai/help/access/access#data-connection,
@@ -47,6 +59,7 @@ Run 'npm install' to get newly added packages
47
59
  Read more about package testing at https://datagrok.ai/help/develop/how-to/test-packages`;
48
60
  const help = exports.help = {
49
61
  app,
62
+ domainApp,
50
63
  connection,
51
64
  detector,
52
65
  func,
@@ -34,6 +34,7 @@ exports.runAll = runAll;
34
34
  exports.runScript = runScript;
35
35
  exports.scriptWrapperTemplate = exports.scriptLangExtMap = exports.scriptExtensions = void 0;
36
36
  exports.setHost = setHost;
37
+ exports.snakeToCamelCase = snakeToCamelCase;
37
38
  exports.spaceToCamelCase = spaceToCamelCase;
38
39
  exports.wordsToCamelCase = wordsToCamelCase;
39
40
  var _fs = _interopRequireDefault(require("fs"));
@@ -56,6 +57,10 @@ function kebabToCamelCase(s, firstUpper = true) {
56
57
  s = s.replace(/-./g, x => x.toUpperCase()[1]);
57
58
  return (firstUpper ? s[0].toUpperCase() : s[0].toLowerCase()) + s.slice(1);
58
59
  }
60
+ function snakeToCamelCase(s, firstUpper = true) {
61
+ s = s.replace(/_./g, x => x[1].toUpperCase());
62
+ return (firstUpper ? s[0].toUpperCase() : s[0].toLowerCase()) + s.slice(1);
63
+ }
59
64
  function descriptionToComment(s) {
60
65
  if (s.length === 0) return '';
61
66
  return '/**\n' + s + '\n*/\n';
@@ -126,6 +131,7 @@ const replacers = exports.replacers = {
126
131
  NAME_TITLECASE: (s, name) => s.replace(/#{NAME_TITLECASE}/g, name[0].toUpperCase() + name.slice(1).toLowerCase()),
127
132
  NAME_LOWERCASE: (s, name) => s.replace(/#{NAME_LOWERCASE}/g, name.toLowerCase()),
128
133
  NAME_PREFIX: (s, name) => s.replace(/#{NAME_PREFIX}/g, name.slice(0, 3)),
134
+ DOMAIN_TABLE: (s, table) => s.replace(/#{DOMAIN_TABLE}/g, table),
129
135
  PACKAGE_DETECTORS_NAME: (s, name) => s.replace(/#{PACKAGE_DETECTORS_NAME}/g, kebabToCamelCase(name)),
130
136
  PACKAGE_NAMESPACE: (s, name) => s.replace(/#{PACKAGE_NAMESPACE}/g, name),
131
137
  FUNC_DESCRIPTION: (s, desc) => s.replace(/#{FUNC_DESCRIPTION}/g, descriptionToComment(desc)),
@@ -0,0 +1,253 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://datagrok.ai/schemas/domain-schema.schema.json",
4
+ "title": "Datagrok domain schema manifest",
5
+ "description": "Declares entity-mapped tables of a plugin database schema (databases/<schema>/schema.json). Tables get platform row/column security, Datlas-managed CRUD, and an in-transaction audit trail.",
6
+ "type": "object",
7
+ "required": ["name", "version", "tables"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "$schema": {"type": "string"},
11
+ "name": {
12
+ "description": "Logical schema name; must match the databases/<name>/ directory.",
13
+ "$ref": "#/definitions/identifier"
14
+ },
15
+ "version": {
16
+ "type": "string",
17
+ "description": "Manifest version (semver recommended). Recorded in the registry on deploy."
18
+ },
19
+ "extensible": {
20
+ "type": "object",
21
+ "description": "Opt-in for user extensions of this schema. Users holding the Extend permission on the schema entity may then add their own tables; per-table column extension is opted in with the table's own 'extensible' flag. User objects are retained across plugin upgrades and never appear in generated typed clients.",
22
+ "required": ["tables"],
23
+ "additionalProperties": false,
24
+ "properties": {
25
+ "tables": {
26
+ "type": "boolean",
27
+ "description": "Whether users may add their own tables to this schema."
28
+ }
29
+ }
30
+ },
31
+ "tables": {
32
+ "type": "object",
33
+ "description": "Entity-mapped tables keyed by table name.",
34
+ "minProperties": 1,
35
+ "propertyNames": {"$ref": "#/definitions/identifier"},
36
+ "additionalProperties": {"$ref": "#/definitions/table"}
37
+ },
38
+ "propertySchemas": {
39
+ "type": "object",
40
+ "description": "Named groups of dynamic (jsonb) properties; grant a schema to a group to control column visibility.",
41
+ "propertyNames": {"$ref": "#/definitions/identifier"},
42
+ "additionalProperties": {
43
+ "type": "object",
44
+ "minProperties": 1,
45
+ "propertyNames": {"$ref": "#/definitions/columnName"},
46
+ "additionalProperties": {"$ref": "#/definitions/column"}
47
+ }
48
+ },
49
+ "migrations": {
50
+ "type": "object",
51
+ "description": "Explicit statements for destructive changes the diff engine refuses to apply automatically."
52
+ }
53
+ },
54
+ "definitions": {
55
+ "identifier": {
56
+ "type": "string",
57
+ "pattern": "^[a-z][a-z0-9_]*$",
58
+ "maxLength": 63
59
+ },
60
+ "columnName": {
61
+ "description": "An identifier that additionally may not start with 'x_' — that prefix is reserved for the physical names of user-added extension columns.",
62
+ "allOf": [
63
+ {"$ref": "#/definitions/identifier"},
64
+ {"not": {"type": "string", "pattern": "^x_"}}
65
+ ]
66
+ },
67
+ "table": {
68
+ "type": "object",
69
+ "required": ["columns"],
70
+ "additionalProperties": false,
71
+ "properties": {
72
+ "securityMode": {
73
+ "enum": ["table", "master", "row"],
74
+ "default": "table",
75
+ "description": "table: one check against the table entity; master: delegate to the master row via the delegate column; row: per-row grants with lazy/eager promotion."
76
+ },
77
+ "promotion": {
78
+ "enum": ["lazy", "eager"],
79
+ "default": "lazy",
80
+ "description": "Row mode only. lazy: entities row created on first share; eager: at insert."
81
+ },
82
+ "defaultRowVisibility": {
83
+ "enum": ["table", "none"],
84
+ "default": "table",
85
+ "description": "Row/master modes: whether table-level View shows unshared rows."
86
+ },
87
+ "delegate": {
88
+ "$ref": "#/definitions/identifier",
89
+ "description": "Master mode: the ref column whose target row's security applies."
90
+ },
91
+ "softDelete": {"type": "boolean", "default": true},
92
+ "audit": {
93
+ "type": "boolean",
94
+ "default": true,
95
+ "description": "Per-table in-transaction audit trail (before/after diffs, history, restore)."
96
+ },
97
+ "extensible": {
98
+ "type": "boolean",
99
+ "default": false,
100
+ "description": "Whether users holding Extend on this schema may add their own columns to this table. Such columns stay optional, non-unique, outside the business key, and may not cascade deletes; they are stored under an 'x_' physical prefix but addressed by their declared name everywhere."
101
+ },
102
+ "idempotency": {
103
+ "type": "boolean",
104
+ "default": false,
105
+ "description": "Adds the idempotency_key system column for replay-safe creates."
106
+ },
107
+ "ginIndex": {
108
+ "type": "boolean",
109
+ "default": false,
110
+ "description": "Adds a GIN (jsonb_path_ops) index on the data column."
111
+ },
112
+ "businessKey": {
113
+ "type": "array",
114
+ "items": {"$ref": "#/definitions/identifier"},
115
+ "minItems": 1,
116
+ "description": "Natural key: powers dedup-on-insert, upsert matching, and entity handles."
117
+ },
118
+ "friendlyName": {"type": "string"},
119
+ "description": {"type": "string"},
120
+ "singularName": {
121
+ "type": "string",
122
+ "maxLength": 256,
123
+ "description": "Explicit singular display name ('New <singular>...' buttons); defaults to a guess from the table name."
124
+ },
125
+ "pluralName": {
126
+ "type": "string",
127
+ "maxLength": 256,
128
+ "description": "Explicit plural display name (view titles); defaults to a guess from the table name."
129
+ },
130
+ "columns": {
131
+ "type": "object",
132
+ "description": "Relational columns keyed by column name. System columns (id, is_deleted, version, created_on, updated_on, author_id, idempotency_key, data) are generated and must not be declared.",
133
+ "propertyNames": {"$ref": "#/definitions/columnName"},
134
+ "additionalProperties": {"$ref": "#/definitions/column"}
135
+ },
136
+ "schemas": {
137
+ "type": "array",
138
+ "items": {"$ref": "#/definitions/identifier"},
139
+ "description": "Property schemas contributing jsonb keys to this table."
140
+ },
141
+ "relations": {
142
+ "type": "object",
143
+ "description": "Many-to-many relations of this table keyed by relation name: each links the table to a target table through a junction table. The name shares one namespace with the columns (it is what expand and filter paths address), so it may not collide with a column of this table. Declaration order is preserved and is the order every UI surface lays the relations out in.",
144
+ "propertyNames": {"$ref": "#/definitions/columnName"},
145
+ "additionalProperties": {"$ref": "#/definitions/relation"}
146
+ },
147
+ "filters": {
148
+ "type": "array",
149
+ "description": "Default filter panel for this table (entity filters): declared columns pre-open in order. Absent = heuristics by column type, INCLUDING one facet per travelable many-to-many relation. Declaring this section replaces the whole heuristic set, relation facets included — to keep one, list it explicitly as {\"column\": \"<relation>.id\"}.",
150
+ "items": {
151
+ "type": "object",
152
+ "required": ["column"],
153
+ "additionalProperties": false,
154
+ "properties": {
155
+ "column": {
156
+ "type": "string",
157
+ "description": "Column name, a filterable system column (id, author_id, created_on, updated_on, version), or a dotted forward FK path (e.g. 'category_id.name', max 3 hops)."
158
+ },
159
+ "type": {
160
+ "enum": ["categories", "histogram", "range", "text", "bool"],
161
+ "description": "Filter kind; defaults by column type. histogram/range need a numeric or datetime column, text a string column, bool a bool column."
162
+ },
163
+ "bins": {
164
+ "type": "integer",
165
+ "minimum": 1,
166
+ "maximum": 200,
167
+ "description": "Histogram bin count (type histogram/range only)."
168
+ },
169
+ "label": {
170
+ "type": "string",
171
+ "minLength": 1,
172
+ "maxLength": 256,
173
+ "description": "Display label for the filter; defaults to the column's friendly name."
174
+ }
175
+ }
176
+ }
177
+ }
178
+ }
179
+ },
180
+ "relation": {
181
+ "type": "object",
182
+ "required": ["via", "target"],
183
+ "additionalProperties": false,
184
+ "properties": {
185
+ "via": {
186
+ "$ref": "#/definitions/identifier",
187
+ "description": "Junction table declared in this manifest; differs from the owner and the target table. Its businessKey must contain both FK columns — that is what keeps re-linking the same pair idempotent."
188
+ },
189
+ "target": {
190
+ "$ref": "#/definitions/identifier",
191
+ "description": "Related table declared in this manifest."
192
+ },
193
+ "viaSelf": {
194
+ "$ref": "#/definitions/identifier",
195
+ "description": "Junction ref column pointing back at the owner table; may be omitted when the junction has exactly one such column. Self-referential relations must declare it."
196
+ },
197
+ "viaTarget": {
198
+ "$ref": "#/definitions/identifier",
199
+ "description": "Junction ref column pointing at the target table; may be omitted when the junction has exactly one such column. Self-referential relations must declare it."
200
+ },
201
+ "allowCreate": {
202
+ "type": "boolean",
203
+ "default": true,
204
+ "description": "Whether the UI may create a target row on the fly while linking (still subject to create permission on the target table)."
205
+ }
206
+ }
207
+ },
208
+ "column": {
209
+ "type": "object",
210
+ "required": ["type"],
211
+ "additionalProperties": false,
212
+ "properties": {
213
+ "type": {
214
+ "enum": ["string", "int", "float", "bool", "datetime", "string_list", "ref", "user", "group", "file"],
215
+ "description": "Property type; ref = FK to another table in this manifest; user/group = FK to the core users/groups tables with the matching semantic type; file = a file://<connection>/<path> string into platform file storage (semantic type File)."
216
+ },
217
+ "required": {"type": "boolean", "default": false},
218
+ "unique": {
219
+ "type": "boolean",
220
+ "default": false,
221
+ "description": "Unique among live (not soft-deleted) rows."
222
+ },
223
+ "isName": {
224
+ "type": "boolean",
225
+ "default": false,
226
+ "description": "Marks the primary display-name column (string columns only; at most one per table). Its value titles cards, tooltips, and entity views, and becomes the friendly name of promoted rows. Convention fallback: a string column literally named 'name'."
227
+ },
228
+ "ref": {
229
+ "$ref": "#/definitions/identifier",
230
+ "description": "Target table name (type = ref only). Cross-plugin refs are not allowed."
231
+ },
232
+ "onDelete": {
233
+ "enum": ["cascade", "restrict", "setnull"],
234
+ "description": "Engine-enforced referential action on soft delete (ref/user/group columns)."
235
+ },
236
+ "min": {"type": "number"},
237
+ "max": {"type": "number"},
238
+ "choices": {
239
+ "type": "array",
240
+ "items": {"type": "string"},
241
+ "description": "Controlled dictionary of allowed values."
242
+ },
243
+ "semType": {"type": "string"},
244
+ "friendlyName": {"type": "string"},
245
+ "description": {"type": "string"},
246
+ "default": {
247
+ "description": "Default value for new rows / UI inputs."
248
+ },
249
+ "format": {"type": "string"}
250
+ }
251
+ }
252
+ }
253
+ }
@@ -0,0 +1,11 @@
1
+
2
+ //name: #{NAME}
3
+ //description: Browse and edit #{DOMAIN_TABLE} rows
4
+ //meta.role: app
5
+ //input: string path {meta.url: true; optional: true}
6
+ //output: view result
7
+ export async function #{NAME}() {
8
+ // The declared `path` input is what makes the app URL-addressable; the page
9
+ // reads the deep link itself (restoreFromUrl), so the function takes no parameter.
10
+ return (await domains.table('#{DOMAIN_TABLE}')).app();
11
+ }
@@ -0,0 +1,11 @@
1
+
2
+ //name: #{NAME}
3
+ //description: Browse and edit #{DOMAIN_TABLE} rows
4
+ //meta.role: app
5
+ //input: string path {meta.url: true; optional: true}
6
+ //output: view result
7
+ export async function #{NAME}(): Promise<DG.ViewBase> {
8
+ // The declared `path` input is what makes the app URL-addressable; the page
9
+ // reads the deep link itself (restoreFromUrl), so the function takes no parameter.
10
+ return (await domains.table('#{DOMAIN_TABLE}')).app();
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datagrok-tools",
3
- "version": "6.5.4",
3
+ "version": "6.5.6",
4
4
  "description": "Utility to upload and publish packages to Datagrok",
5
5
  "homepage": "https://github.com/datagrok-ai/public/tree/master/tools#readme",
6
6
  "dependencies": {
@@ -10,6 +10,7 @@
10
10
  "@typescript-eslint/typescript-estree": "^8.61.1",
11
11
  "@typescript-eslint/visitor-keys": "^8.61.1",
12
12
  "adm-zip": "^0.6.0",
13
+ "ajv": "^8.20.0",
13
14
  "archiver": "^7.0.1",
14
15
  "datagrok-api": "^1.27.6",
15
16
  "estraverse": "^5.3.0",