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.
- package/CHANGELOG.md +17 -1
- package/bin/commands/add.js +116 -1
- package/bin/commands/api.js +381 -2
- package/bin/commands/help.js +19 -2
- package/bin/commands/publish.js +22 -0
- package/bin/commands/report.js +68 -19
- package/bin/utils/ent-helpers.js +13 -0
- package/bin/utils/utils.js +6 -0
- package/domain-schema.schema.json +253 -0
- package/entity-template/domain-app.js +11 -0
- package/entity-template/domain-app.ts +11 -0
- package/package.json +2 -1
- package/bin/__tests__/build.test.js +0 -116
- package/bin/__tests__/build.test.ts +0 -101
- package/bin/__tests__/jira-wiki-converter.test.js +0 -101
- package/bin/__tests__/jira-wiki-converter.test.ts +0 -150
- package/bin/__tests__/node-dapi.connections.test.js +0 -120
- package/bin/__tests__/node-dapi.connections.test.ts +0 -84
- package/bin/__tests__/node-dapi.groups.test.js +0 -467
- package/bin/__tests__/node-dapi.groups.test.ts +0 -298
- package/bin/__tests__/node-dapi.integration.test.ts +0 -447
- package/bin/__tests__/node-dapi.shares.test.ts +0 -70
- package/bin/__tests__/node-dapi.users.test.ts +0 -58
- package/bin/__tests__/server-output.test.js +0 -171
- package/bin/__tests__/server-output.test.ts +0 -133
- package/bin/__tests__/server.test.js +0 -277
- package/bin/__tests__/server.test.ts +0 -197
package/bin/commands/help.js
CHANGED
|
@@ -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
|
package/bin/commands/publish.js
CHANGED
|
@@ -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)`);
|
package/bin/commands/report.js
CHANGED
|
@@ -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
|
|
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).
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
//
|
|
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, ` ` shows up literally, etc.
|
|
456
469
|
// `markdownToJiraWiki` below bridges the gap for Markdown-emitting callers.
|
|
457
470
|
|
|
458
|
-
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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', '-
|
|
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
|
|
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 => ({
|
package/bin/utils/ent-helpers.js
CHANGED
|
@@ -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,
|
package/bin/utils/utils.js
CHANGED
|
@@ -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.
|
|
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",
|