datagrok-tools 6.7.8 → 6.7.10
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/.devcontainer/Dockerfile.pkg_dev +1 -1
- package/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +33 -0
- package/CLAUDE.md +13 -0
- package/GROK_S.md +138 -6
- package/README.md +14 -0
- package/bin/commands/add.js +4 -4
- package/bin/commands/api.js +49 -20
- package/bin/commands/check.js +43 -1
- package/bin/commands/connect-folder.js +411 -0
- package/bin/commands/help.js +68 -2
- package/bin/commands/publish.js +6 -0
- package/bin/commands/server-domains.js +24 -7
- package/bin/commands/server-observe.js +854 -0
- package/bin/commands/server.js +72 -5
- package/bin/commands/src.js +458 -0
- package/bin/grok.js +9 -5
- package/bin/utils/connect-folder/docker.js +244 -0
- package/bin/utils/connect-folder/docker.ts +259 -0
- package/bin/utils/ent-helpers.js +2 -2
- package/bin/utils/migrate/walker.js +2 -1
- package/bin/utils/migrate/walker.ts +1 -1
- package/bin/utils/node-dapi.js +56 -17
- package/bin/utils/server-client.js +8 -1
- package/bin/utils/test-utils.js +54 -2
- package/domain-schema.schema.json +50 -1
- package/entity-template/domain-app-imports.js +2 -2
- package/package.json +5 -2
- package/plugins/func-gen-plugin.js +1 -1
- package/vitest.config.mts +4 -0
package/bin/utils/node-dapi.js
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.NodeUsersDataSource = exports.NodeTablesDataSource = exports.NodeSharesDataSource = exports.NodePackagesDataSource = exports.NodeHttpDataSource = exports.NodeGroupsDataSource = exports.NodeFuncsDataSource = exports.NodeFilesDataSource = exports.NodeDomainsDataSource = exports.NodeDapi = exports.NodeConnectionsDataSource = exports.NodeApiClient = exports.InternalDataSource = void 0;
|
|
6
|
+
exports.UUID_RE = exports.NodeUsersDataSource = exports.NodeTablesDataSource = exports.NodeSharesDataSource = exports.NodePackagesDataSource = exports.NodeHttpDataSource = exports.NodeGroupsDataSource = exports.NodeFuncsDataSource = exports.NodeFilesDataSource = exports.NodeDomainsDataSource = exports.NodeDapi = exports.NodeConnectionsDataSource = exports.NodeApiClient = exports.InternalDataSource = void 0;
|
|
7
7
|
exports.apiPath = apiPath;
|
|
8
8
|
exports.buildQuery = buildQuery;
|
|
9
9
|
exports.ensureBodyId = ensureBodyId;
|
|
10
10
|
exports.mapPositionalParams = mapPositionalParams;
|
|
11
11
|
exports.parseDomainAddress = parseDomainAddress;
|
|
12
12
|
exports.throwIfApiError = throwIfApiError;
|
|
13
|
+
exports.tryParseJson = tryParseJson;
|
|
13
14
|
var _crypto = require("crypto");
|
|
14
15
|
var _keypair = require("./keypair");
|
|
15
16
|
/// Docs: [Grok Dapi](/docs/plans/grok-dapi/)
|
|
@@ -63,6 +64,17 @@ async function fetchOrRetry(url, opts, retriable, timeoutMs = setting('TIMEOUT',
|
|
|
63
64
|
class NodeApiClient {
|
|
64
65
|
/** Set by `createClient` when the run asked for an admin session, so a re-login restores it. */
|
|
65
66
|
adminMode = false;
|
|
67
|
+
/**
|
|
68
|
+
* `--log`: one id for every request of the command, wherever the server runs it, and the logger settings
|
|
69
|
+
* its records are kept at until `GET /log/requests/<logId>` collects them.
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
get logHeaders() {
|
|
73
|
+
const headers = {};
|
|
74
|
+
if (this.logId) headers['datlas-log-id'] = this.logId;
|
|
75
|
+
if (this.logSpec) headers['datlas-log'] = this.logSpec;
|
|
76
|
+
return headers;
|
|
77
|
+
}
|
|
66
78
|
constructor(baseUrl, token, devKey, privateKey) {
|
|
67
79
|
this.baseUrl = baseUrl;
|
|
68
80
|
this.token = token;
|
|
@@ -119,6 +131,7 @@ class NodeApiClient {
|
|
|
119
131
|
headers: {
|
|
120
132
|
'Authorization': this.token,
|
|
121
133
|
'Content-Type': 'application/json',
|
|
134
|
+
...this.logHeaders,
|
|
122
135
|
...headers
|
|
123
136
|
}
|
|
124
137
|
};
|
|
@@ -142,7 +155,7 @@ class NodeApiClient {
|
|
|
142
155
|
return null;
|
|
143
156
|
}
|
|
144
157
|
const ct = res.headers.get('content-type') ?? '';
|
|
145
|
-
return throwIfApiError(ct.includes('application/json') ? await res.json() : await res.text());
|
|
158
|
+
return throwIfApiError(ct.includes('application/json') ? await res.json() : await res.text(), res.headers);
|
|
146
159
|
}
|
|
147
160
|
get(path) {
|
|
148
161
|
return this.request('GET', path);
|
|
@@ -164,13 +177,14 @@ class NodeApiClient {
|
|
|
164
177
|
method: 'POST',
|
|
165
178
|
headers: {
|
|
166
179
|
'Authorization': this.token,
|
|
167
|
-
'Content-Type': contentType
|
|
180
|
+
'Content-Type': contentType,
|
|
181
|
+
...this.logHeaders
|
|
168
182
|
},
|
|
169
183
|
body: bytes
|
|
170
184
|
}, false, BYTES_TIMEOUT_MS);
|
|
171
185
|
if (!res.ok) await throwHttpError(res);
|
|
172
186
|
const ct = res.headers.get('content-type') ?? '';
|
|
173
|
-
return throwIfApiError(ct.includes('application/json') ? await res.json() : await res.text());
|
|
187
|
+
return throwIfApiError(ct.includes('application/json') ? await res.json() : await res.text(), res.headers);
|
|
174
188
|
}
|
|
175
189
|
|
|
176
190
|
/** POST a JSON body and read the response as raw bytes (d42 query results). */
|
|
@@ -179,7 +193,8 @@ class NodeApiClient {
|
|
|
179
193
|
method: 'POST',
|
|
180
194
|
headers: {
|
|
181
195
|
'Authorization': this.token,
|
|
182
|
-
'Content-Type': 'application/json'
|
|
196
|
+
'Content-Type': 'application/json',
|
|
197
|
+
...this.logHeaders
|
|
183
198
|
},
|
|
184
199
|
body: JSON.stringify(body),
|
|
185
200
|
signal: AbortSignal.timeout(BYTES_TIMEOUT_MS)
|
|
@@ -192,7 +207,8 @@ class NodeApiClient {
|
|
|
192
207
|
async getBytes(path) {
|
|
193
208
|
const res = await fetchOrRetry(`${this.baseUrl}${path}`, {
|
|
194
209
|
headers: {
|
|
195
|
-
'Authorization': this.token
|
|
210
|
+
'Authorization': this.token,
|
|
211
|
+
...this.logHeaders
|
|
196
212
|
}
|
|
197
213
|
}, true, BYTES_TIMEOUT_MS);
|
|
198
214
|
if (!res.ok) await throwHttpError(res);
|
|
@@ -236,13 +252,15 @@ async function throwHttpError(res) {
|
|
|
236
252
|
* a JSON string when the handler returned text. Every response passes through here so a
|
|
237
253
|
* missing entity, a rejected save or an unknown function throws instead of printing as data.
|
|
238
254
|
*/
|
|
239
|
-
|
|
255
|
+
/** A user error answers HTTP 200 with the ApiError in the body and its status in the `api-error-code` header. */
|
|
256
|
+
function throwIfApiError(payload, headers) {
|
|
240
257
|
const parsed = typeof payload === 'string' ? tryParseJson(payload) : payload;
|
|
241
258
|
if (parsed?.['#type'] === 'ApiError') {
|
|
259
|
+
const code = headers?.get('api-error-code');
|
|
242
260
|
const err = {
|
|
243
261
|
error: parsed.message ?? 'Request failed',
|
|
244
262
|
source: 'Server',
|
|
245
|
-
errorCode: parsed.errorCode,
|
|
263
|
+
errorCode: parsed.errorCode ?? (code ? Number(code) : undefined),
|
|
246
264
|
stackTrace: parsed.stackTrace,
|
|
247
265
|
body: parsed
|
|
248
266
|
};
|
|
@@ -388,7 +406,7 @@ class InternalDataSource {
|
|
|
388
406
|
}
|
|
389
407
|
}
|
|
390
408
|
exports.InternalDataSource = InternalDataSource;
|
|
391
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
409
|
+
const UUID_RE = exports.UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
392
410
|
class NodeGroupsDataSource extends NodeHttpDataSource {
|
|
393
411
|
constructor(client) {
|
|
394
412
|
super(client, 'groups', '/groups');
|
|
@@ -638,9 +656,11 @@ class NodeFuncsDataSource extends NodeHttpDataSource {
|
|
|
638
656
|
constructor(client) {
|
|
639
657
|
super(client, 'functions', '/log/funcs');
|
|
640
658
|
}
|
|
641
|
-
|
|
659
|
+
|
|
660
|
+
/** [debug] runs it as a debug call, as the query debugger does; its log is kept with the command's `--log`. */
|
|
661
|
+
async run(name, params, debug = false) {
|
|
642
662
|
const normalizedName = name.replace(':', '.');
|
|
643
|
-
return this.client.post(`/public/v1/functions/${encodeURIComponent(normalizedName)}/call`, params ?? {});
|
|
663
|
+
return this.client.post(`/public/v1/functions/${encodeURIComponent(normalizedName)}/call${debug ? '?debug=true' : ''}`, params ?? {});
|
|
644
664
|
}
|
|
645
665
|
|
|
646
666
|
/** The public API has no DELETE for functions; scripts and queries go to their own routers. */
|
|
@@ -936,6 +956,10 @@ function parseDomainAddress(s, opts = {}) {
|
|
|
936
956
|
if (opts.table === false && address.table) throw new Error(`'${str}' names a table; this command needs a schema`);
|
|
937
957
|
return address;
|
|
938
958
|
}
|
|
959
|
+
/** Every literal a `/domains/{schema}/{table}/{id}` route registers at the id position, the dot
|
|
960
|
+
* segments a proxy folds away, and the u2 draft sentinel — the Dart `DomainRowKey.reserved` list. */
|
|
961
|
+
const RESERVED_ROW_KEYS = ['access', 'aggregate', 'audit', 'batch', 'delete', 'facets', 'filters', 'query', 'update', 'version', 'watch', 'new', '.', '..'];
|
|
962
|
+
|
|
939
963
|
/**
|
|
940
964
|
* Client for entity-mapped domain tables (`/domains/...`, DomainsRouter). Rows are plain
|
|
941
965
|
* JSON objects; the server validates, permission-checks and audits every write. Errors
|
|
@@ -950,6 +974,13 @@ class NodeDomainsDataSource {
|
|
|
950
974
|
return `/domains/${encodeURIComponent(schema)}/${encodeURIComponent(table)}`;
|
|
951
975
|
}
|
|
952
976
|
|
|
977
|
+
/** The row-id segment as the Dart `DomainRowKey` spells it (grok_shared domain_manifest_rules.dart):
|
|
978
|
+
* escaped, then percent-encoded once more so an external key's own `%2C` survives the proxy. */
|
|
979
|
+
row(schema, table, id) {
|
|
980
|
+
const escaped = RESERVED_ROW_KEYS.includes(id) || id.startsWith('~') ? `~${id}` : id;
|
|
981
|
+
return `${this.rows(schema, table)}/${encodeURIComponent(escaped)}`;
|
|
982
|
+
}
|
|
983
|
+
|
|
953
984
|
/** Registered schemas with their tables (`GET /domains/schemas`); [text] is a smart filter. */
|
|
954
985
|
schemas(text) {
|
|
955
986
|
return this.client.get(`/domains/schemas${buildQuery({
|
|
@@ -1001,7 +1032,7 @@ class NodeDomainsDataSource {
|
|
|
1001
1032
|
})}`);
|
|
1002
1033
|
}
|
|
1003
1034
|
rowAudit(schema, table, id) {
|
|
1004
|
-
return this.client.get(`${this.
|
|
1035
|
+
return this.client.get(`${this.row(schema, table, id)}/audit`);
|
|
1005
1036
|
}
|
|
1006
1037
|
grants(entityId) {
|
|
1007
1038
|
return this.client.get(`/domains/grants/${encodeURIComponent(entityId)}`);
|
|
@@ -1053,7 +1084,7 @@ class NodeDomainsDataSource {
|
|
|
1053
1084
|
/** One row, or null when it does not exist or is not visible (the server answers 404). */
|
|
1054
1085
|
async getRow(schema, table, id) {
|
|
1055
1086
|
try {
|
|
1056
|
-
return await this.client.get(
|
|
1087
|
+
return await this.client.get(this.row(schema, table, id));
|
|
1057
1088
|
} catch (err) {
|
|
1058
1089
|
if (err?.apiError?.errorCode === 404) return null;
|
|
1059
1090
|
throw err;
|
|
@@ -1073,10 +1104,10 @@ class NodeDomainsDataSource {
|
|
|
1073
1104
|
values
|
|
1074
1105
|
};
|
|
1075
1106
|
if (version !== undefined) body.version = version;
|
|
1076
|
-
return this.client.request('PATCH',
|
|
1107
|
+
return this.client.request('PATCH', this.row(schema, table, id), body);
|
|
1077
1108
|
}
|
|
1078
1109
|
deleteRow(schema, table, id) {
|
|
1079
|
-
return this.client.del(
|
|
1110
|
+
return this.client.del(this.row(schema, table, id));
|
|
1080
1111
|
}
|
|
1081
1112
|
|
|
1082
1113
|
/** Soft-deletes up to [limit] (≤1000) matching rows in one transaction; `{deleted, hasMore}`. */
|
|
@@ -1138,6 +1169,9 @@ class NodeDapi {
|
|
|
1138
1169
|
get reports() {
|
|
1139
1170
|
return this.internal('/reports');
|
|
1140
1171
|
}
|
|
1172
|
+
get problems() {
|
|
1173
|
+
return this.internal('/problems');
|
|
1174
|
+
}
|
|
1141
1175
|
get files() {
|
|
1142
1176
|
return new NodeFilesDataSource(this.client);
|
|
1143
1177
|
}
|
|
@@ -1153,9 +1187,14 @@ class NodeDapi {
|
|
|
1153
1187
|
internal(route) {
|
|
1154
1188
|
return new InternalDataSource(this.client, route);
|
|
1155
1189
|
}
|
|
1190
|
+
|
|
1191
|
+
/** `/info/server`; [extended] adds the in-flight requests per endpoint (admins only). */
|
|
1192
|
+
async serverStatus(extended = false) {
|
|
1193
|
+
const raw = await this.client.get(extended ? '/info/server?extended=true' : '/info/server');
|
|
1194
|
+
return typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
1195
|
+
}
|
|
1156
1196
|
async serverInfo() {
|
|
1157
|
-
const
|
|
1158
|
-
const info = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
1197
|
+
const info = await this.serverStatus();
|
|
1159
1198
|
return {
|
|
1160
1199
|
version: info?.Version ?? '',
|
|
1161
1200
|
commit: info?.Commit
|
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.createClient = createClient;
|
|
7
|
+
exports.logSpecOf = logSpecOf;
|
|
7
8
|
var _nodeDapi = require("./node-dapi");
|
|
8
9
|
var _keypair = require("./keypair");
|
|
9
10
|
/**
|
|
@@ -13,11 +14,17 @@ var _keypair = require("./keypair");
|
|
|
13
14
|
* flag into the token, and a dev key mints a fresh session per invocation, so it cannot outlive
|
|
14
15
|
* the command or reach another session.
|
|
15
16
|
*/
|
|
16
|
-
|
|
17
|
+
/** `--log` alone is `verbose`. */
|
|
18
|
+
function logSpecOf(argv) {
|
|
19
|
+
return argv.log === undefined || argv.log === false ? undefined : argv.log === true ? 'verbose' : String(argv.log);
|
|
20
|
+
}
|
|
21
|
+
async function createClient(hostArg, admin = false, log) {
|
|
17
22
|
// Resolved from the alias the caller named, not from its URL: two aliases can point at the
|
|
18
23
|
// same server, and only this side knows which one was asked for.
|
|
19
24
|
const cred = (0, _keypair.getServerCredentials)(hostArg ?? '');
|
|
20
25
|
const client = await _nodeDapi.NodeApiClient.login(cred.url, cred.key ?? '', cred.privateKey);
|
|
26
|
+
client.logId = log?.logId;
|
|
27
|
+
client.logSpec = log?.spec;
|
|
21
28
|
if (!admin) return client;
|
|
22
29
|
const token = (await client.post('/users/sessions/current/admin'))?.token;
|
|
23
30
|
if (!token) throw new Error(`${cred.url} refused an admin session — the account behind this key cannot start one`);
|
package/bin/utils/test-utils.js
CHANGED
|
@@ -16,6 +16,7 @@ exports.getToken = getToken;
|
|
|
16
16
|
exports.getWebUrl = getWebUrl;
|
|
17
17
|
exports.getWebUrlFromPage = getWebUrlFromPage;
|
|
18
18
|
exports.isPackageOnServer = isPackageOnServer;
|
|
19
|
+
exports.isPackageU2Usage = isPackageU2Usage;
|
|
19
20
|
exports.loadPackage = loadPackage;
|
|
20
21
|
exports.loadPackages = loadPackages;
|
|
21
22
|
exports.loadTestsList = loadTestsList;
|
|
@@ -159,6 +160,36 @@ function getDevKey(hostKey) {
|
|
|
159
160
|
key
|
|
160
161
|
};
|
|
161
162
|
}
|
|
163
|
+
const U2_USAGE = 'u2 usage:';
|
|
164
|
+
/** The u2 usage mode a package built on u2 or ems is tested in: a misuse throws, unless `U2_STRICT` says otherwise
|
|
165
|
+
* (`report` logs each one as a `u2 usage:` line and the run does not fail on them). */
|
|
166
|
+
const U2_MODE = process.env.U2_STRICT ?? 'throw';
|
|
167
|
+
|
|
168
|
+
/** Whether a `u2 usage:` line the page logged fails the run: the package is tested with u2 strict. */
|
|
169
|
+
function failsOnU2Usage() {
|
|
170
|
+
return U2_MODE !== 'report' && usesU2(process.cwd());
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Whether a `u2 usage:` line is the tested package's: a stack frame it carries is in the package's bundle
|
|
174
|
+
* (`…/files/<package>/…`), or it carries none to tell. A line whose frames are all in other bundles is another
|
|
175
|
+
* package's misuse, reported but not failed on. */
|
|
176
|
+
function isPackageU2Usage(line, packageName) {
|
|
177
|
+
const frames = line.match(/https?:\/\/[^\s()]+/g) ?? [];
|
|
178
|
+
const bundle = `/files/${packageName?.toLowerCase()}/`;
|
|
179
|
+
return packageName === undefined || frames.length === 0 || frames.some(f => f.toLowerCase().includes(bundle));
|
|
180
|
+
}
|
|
181
|
+
function usesU2(packageDir) {
|
|
182
|
+
try {
|
|
183
|
+
const json = JSON.parse(_fs.default.readFileSync(_path.default.join(packageDir, 'package.json'), 'utf-8'));
|
|
184
|
+
const deps = {
|
|
185
|
+
...json.dependencies,
|
|
186
|
+
...json.devDependencies
|
|
187
|
+
};
|
|
188
|
+
return '@datagrok-libraries/u2' in deps || '@datagrok-libraries/ems' in deps;
|
|
189
|
+
} catch {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
162
193
|
function appendUrlParams(url, extraParams) {
|
|
163
194
|
if (!extraParams) return url;
|
|
164
195
|
return url + (url.includes('?') ? '&' : '?') + extraParams;
|
|
@@ -194,6 +225,9 @@ async function getBrowserPage(puppeteer, params = defaultLaunchParameters, urlPa
|
|
|
194
225
|
height: 1080
|
|
195
226
|
});
|
|
196
227
|
page.setDefaultNavigationTimeout(0);
|
|
228
|
+
if (usesU2(process.cwd())) await page.evaluateOnNewDocument(mode => {
|
|
229
|
+
globalThis.__U2_STRICT__ = mode;
|
|
230
|
+
}, U2_MODE);
|
|
197
231
|
await page.goto(`${url}/oauth/`);
|
|
198
232
|
await page.browser().setCookie({
|
|
199
233
|
name: 'auth',
|
|
@@ -537,6 +571,16 @@ function printBrowsersResult(browserResult, verbose = false) {
|
|
|
537
571
|
console.log('Skipped tests: ' + (browserResult?.skippedAmount ?? 0));
|
|
538
572
|
console.log('Failed tests: ' + (browserResult?.failedAmount ?? 0));
|
|
539
573
|
}
|
|
574
|
+
const own = browserResult.u2Usage?.filter(l => isPackageU2Usage(l, process.env.TARGET_PACKAGE)) ?? [];
|
|
575
|
+
const others = browserResult.u2Usage?.filter(l => !own.includes(l)) ?? [];
|
|
576
|
+
if (own.length) {
|
|
577
|
+
color.warn(failsOnU2Usage() ? `${own.length} u2 usage error(s) caught on the page (the run fails):` : `${own.length} u2 usage report(s), in report mode (the run does not fail on them):`);
|
|
578
|
+
for (const line of own) color.warn(` ${line}`);
|
|
579
|
+
}
|
|
580
|
+
if (others.length) {
|
|
581
|
+
color.warn(`${others.length} u2 usage line(s) from other packages' code (the run does not fail on them):`);
|
|
582
|
+
for (const line of others) color.warn(` ${line}`);
|
|
583
|
+
}
|
|
540
584
|
if (browserResult.failed) {
|
|
541
585
|
if (browserResult.verboseFailed === 'Package not found') color.fail('Tests not found');else color.fail('Tests failed.');
|
|
542
586
|
} else color.success('Tests passed.');
|
|
@@ -860,6 +904,11 @@ async function runBrowser(testExecutionData, browserOptions, browsersId, testInv
|
|
|
860
904
|
// Subscribe to page console events for modern output formatting
|
|
861
905
|
// On retry, old listeners were removed so we need to re-attach
|
|
862
906
|
page.on('console', msg => reporter.onLine(msg.text()));
|
|
907
|
+
const u2Usage = [];
|
|
908
|
+
page.on('console', msg => {
|
|
909
|
+
const text = msg.text();
|
|
910
|
+
if (text.includes(U2_USAGE) && !u2Usage.includes(text)) u2Usage.push(text);
|
|
911
|
+
});
|
|
863
912
|
const testingResults = await page.evaluate((testData, options) => {
|
|
864
913
|
if (options.benchmark) window.DG.Test.isInBenchmark = true;
|
|
865
914
|
if (options.reproduce) window.DG.Test.isReproducing = true;
|
|
@@ -918,12 +967,14 @@ async function runBrowser(testExecutionData, browserOptions, browsersId, testInv
|
|
|
918
967
|
// Return browser session for potential reuse
|
|
919
968
|
return {
|
|
920
969
|
...testingResults,
|
|
970
|
+
failed: testingResults.failed || failsOnU2Usage() && u2Usage.some(l => isPackageU2Usage(l, process.env.TARGET_PACKAGE)),
|
|
921
971
|
browserSession: browserOptions.keepBrowserOpen ? {
|
|
922
972
|
browser,
|
|
923
973
|
page,
|
|
924
974
|
webUrl
|
|
925
975
|
} : undefined,
|
|
926
|
-
modernOutput: reporter.modernOutput
|
|
976
|
+
modernOutput: reporter.modernOutput,
|
|
977
|
+
u2Usage
|
|
927
978
|
};
|
|
928
979
|
}, testInvocationTimeout);
|
|
929
980
|
}
|
|
@@ -938,7 +989,8 @@ async function mergeBrowsersResults(browsersResults) {
|
|
|
938
989
|
failedAmount: browsersResults[0].failedAmount,
|
|
939
990
|
csv: browsersResults[0].csv,
|
|
940
991
|
error: '',
|
|
941
|
-
modernOutput: browsersResults.some(r => r.modernOutput)
|
|
992
|
+
modernOutput: browsersResults.some(r => r.modernOutput),
|
|
993
|
+
u2Usage: [...new Set(browsersResults.flatMap(r => r.u2Usage ?? []))]
|
|
942
994
|
};
|
|
943
995
|
for (const browsersResult of browsersResults) {
|
|
944
996
|
if (mergedResult.csv === browsersResult.csv) continue;
|
|
@@ -16,6 +16,36 @@
|
|
|
16
16
|
"type": "string",
|
|
17
17
|
"description": "Manifest version (semver recommended). Recorded in the registry on deploy."
|
|
18
18
|
},
|
|
19
|
+
"storage": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"description": "Where the rows live. Absent = domain: the platform creates and owns the tables. external: the tables already exist in the database of the named platform connection and are read through it (no row security, soft delete, audit, relations, constraints, property schemas or extensions; read-only unless 'writable'); every table then declares its remote primary key as businessKey, and 'table'/'column' carry the remote names when they differ from the declared ones.",
|
|
22
|
+
"required": ["kind"],
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"properties": {
|
|
25
|
+
"kind": {"enum": ["domain", "external"]},
|
|
26
|
+
"connection": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"minLength": 1,
|
|
29
|
+
"description": "external: the qualified name of the platform connection to read through ('Package:Connection'); required."
|
|
30
|
+
},
|
|
31
|
+
"schema": {
|
|
32
|
+
"$ref": "#/definitions/remoteIdentifier",
|
|
33
|
+
"description": "external: the schema inside the remote database (e.g. 'public', 'dbo'); required."
|
|
34
|
+
},
|
|
35
|
+
"catalog": {
|
|
36
|
+
"$ref": "#/definitions/remoteIdentifier",
|
|
37
|
+
"description": "external: the catalog/database, for targets that have one."
|
|
38
|
+
},
|
|
39
|
+
"writable": {
|
|
40
|
+
"type": "boolean",
|
|
41
|
+
"default": false,
|
|
42
|
+
"description": "external: rows may be inserted, patched and deleted through the connection (one remote transaction per request, keyed by businessKey; on ClickHouse, which commits every statement, a request is one multi-row INSERT and patch/delete/upsert are refused). The publisher must hold Query, AddRows, ChangeValues and RemoveRows on the connection; a table opts out with its own 'writable': false."
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"if": {"properties": {"kind": {"const": "external"}}},
|
|
46
|
+
"then": {"required": ["connection", "schema"]},
|
|
47
|
+
"else": {"properties": {"connection": false, "schema": false, "catalog": false, "writable": false}}
|
|
48
|
+
},
|
|
19
49
|
"extensible": {
|
|
20
50
|
"type": "object",
|
|
21
51
|
"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.",
|
|
@@ -64,11 +94,26 @@
|
|
|
64
94
|
{"not": {"type": "string", "pattern": "^x_"}}
|
|
65
95
|
]
|
|
66
96
|
},
|
|
97
|
+
"remoteIdentifier": {
|
|
98
|
+
"description": "A name inside the remote database, verbatim (spaces allowed, the connector quotes it); it may not contain brackets, quotes, backslashes or dots.",
|
|
99
|
+
"type": "string",
|
|
100
|
+
"minLength": 1,
|
|
101
|
+
"maxLength": 256,
|
|
102
|
+
"pattern": "^[^\\[\\]\"'`\\\\.]+$"
|
|
103
|
+
},
|
|
67
104
|
"table": {
|
|
68
105
|
"type": "object",
|
|
69
106
|
"required": ["columns"],
|
|
70
107
|
"additionalProperties": false,
|
|
71
108
|
"properties": {
|
|
109
|
+
"table": {
|
|
110
|
+
"$ref": "#/definitions/remoteIdentifier",
|
|
111
|
+
"description": "External storage only: the remote table's name when it differs from the declared one (e.g. 'Order Details')."
|
|
112
|
+
},
|
|
113
|
+
"writable": {
|
|
114
|
+
"type": "boolean",
|
|
115
|
+
"description": "Writable external storage only: false keeps this table read-only while the rest of the binding accepts writes."
|
|
116
|
+
},
|
|
72
117
|
"securityMode": {
|
|
73
118
|
"enum": ["table", "master", "row"],
|
|
74
119
|
"default": "table",
|
|
@@ -263,7 +308,11 @@
|
|
|
263
308
|
"properties": {
|
|
264
309
|
"type": {
|
|
265
310
|
"enum": ["string", "int", "float", "bool", "datetime", "string_list", "ref", "file", "json", "user", "group"],
|
|
266
|
-
"description": "Property type; ref = FK to another table in this manifest; user/group = aliases of ref Core.users / ref Core.groups (semantic type User / Group); file = a file://<connection>/<path> string into platform file storage (semantic type File); json = a jsonb-backed opaque object (filters, sorts, facets and aggregates refuse it)."
|
|
311
|
+
"description": "Property type; ref = FK to another table in this manifest; user/group = aliases of ref Core.users / ref Core.groups (semantic type User / Group); file = a file://<connection>/<path> string into platform file storage (semantic type File); json = a jsonb-backed opaque object (filters, sorts, facets and aggregates refuse it). An external storage admits string, int, float, bool, datetime and same-manifest ref only."
|
|
312
|
+
},
|
|
313
|
+
"column": {
|
|
314
|
+
"$ref": "#/definitions/remoteIdentifier",
|
|
315
|
+
"description": "External storage only: the remote column's name when it differs from the declared one (e.g. 'OrderID')."
|
|
267
316
|
},
|
|
268
317
|
"required": {"type": "boolean", "default": false},
|
|
269
318
|
"unique": {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {domains} from '@datagrok-libraries/
|
|
1
|
+
import {domains} from '@datagrok-libraries/ems/src/dg';
|
|
2
2
|
|
|
3
3
|
// the skins of everything the u2 domain stack renders
|
|
4
|
-
import '@datagrok-libraries/
|
|
4
|
+
import '@datagrok-libraries/ems/src/dg/styles.js';
|
package/package.json
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
"type": "git",
|
|
5
5
|
"url": "https://github.com/datagrok-ai/public.git"
|
|
6
6
|
},
|
|
7
|
-
"version": "6.7.
|
|
7
|
+
"version": "6.7.10",
|
|
8
8
|
"description": "Utility to upload and publish packages to Datagrok",
|
|
9
9
|
"homepage": "https://github.com/datagrok-ai/public/tree/master/tools#readme",
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=22.12.0"
|
|
12
|
+
},
|
|
10
13
|
"dependencies": {
|
|
11
14
|
"@babel/parser": "^7.29.7",
|
|
12
15
|
"@babel/runtime": "^7.29.7",
|
|
@@ -24,7 +27,7 @@
|
|
|
24
27
|
"minimist": "^1.2.8",
|
|
25
28
|
"node-fetch": "^2.7.0",
|
|
26
29
|
"papaparse": "^5.5.3",
|
|
27
|
-
"puppeteer": "^
|
|
30
|
+
"puppeteer": "^25.14.0",
|
|
28
31
|
"ts-morph": "^28.0.0",
|
|
29
32
|
"@babel/generator": "^7.29.0",
|
|
30
33
|
"datagrok-api": "^1.27.12"
|
|
@@ -202,8 +202,8 @@ class FuncGeneratorPlugin {
|
|
|
202
202
|
this._checkPackageFileForDecoratorsExport(packageFilePath);
|
|
203
203
|
// Uncommment to add obvious import/export
|
|
204
204
|
// this._writeToPackageFile(packageFilePath, genImports, genExports);
|
|
205
|
+
api({_: ['api']});
|
|
205
206
|
});
|
|
206
|
-
api({_: ['api']});
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
_addNodeData(
|
package/vitest.config.mts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import {defineConfig} from 'vitest/config';
|
|
2
2
|
|
|
3
3
|
export default defineConfig({
|
|
4
|
+
// Babel writes each .js beside its .ts in bin/: tests must load the sources, and one copy of each module.
|
|
5
|
+
resolve: {extensions: ['.ts', '.mts', '.mjs', '.js', '.jsx', '.tsx', '.json']},
|
|
4
6
|
test: {
|
|
5
7
|
projects: [
|
|
6
8
|
{
|
|
9
|
+
extends: true,
|
|
7
10
|
test: {
|
|
8
11
|
name: 'unit',
|
|
9
12
|
environment: 'node',
|
|
@@ -12,6 +15,7 @@ export default defineConfig({
|
|
|
12
15
|
},
|
|
13
16
|
},
|
|
14
17
|
{
|
|
18
|
+
extends: true,
|
|
15
19
|
test: {
|
|
16
20
|
name: 'integration',
|
|
17
21
|
environment: 'node',
|