datagrok-tools 6.5.3 → 6.5.5

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)`);
@@ -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.3",
3
+ "version": "6.5.5",
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",
@@ -84,17 +84,23 @@ function preparseIvpModel(parser, text) {
84
84
  const viewer = `Grid(block: 100) | Line chart(block: 100, multiAxis: "${multiAxis}",${segments} ` +
85
85
  `multiAxisLegendPosition: "RightCenter", autoLayout: "false", showAggrSelectors: "false")` +
86
86
  ` | DiffStudio Facet(block: 100${facetSegment})`;
87
- const outputAnnotation = `//output: dataframe df {caption: ${ivp.name}; viewer: ${viewer}}`;
88
-
89
87
  const metas = {runOnOpen: 'true', runOnInput: 'true', features: '{"sens-analysis": true, "fitting": true}'};
90
88
  let isModel = false;
89
+ let comparison = null;
91
90
  for (const meta of ivp.metas || []) {
92
91
  const m = /^(?:\/\/)?meta\.([\w-]+):\s*(.*)$/.exec(String(meta).trim());
93
92
  if (!m) continue;
94
93
  if (m[1] === 'role') {
95
94
  if (m[2].split(',').map((s) => s.trim()).includes('model')) isModel = true;
95
+ } else if (m[1] === 'comparison') {
96
+ // run comparison reads the {comparison: ...} JSON off the dataframe output's
97
+ // options, not off function-level meta
98
+ comparison = m[2];
96
99
  } else if (m[1] !== 'solver') metas[m[1]] = m[2];
97
100
  }
101
+
102
+ const outputAnnotation = `//output: dataframe df {caption: ${ivp.name}; ` +
103
+ `${comparison ? `comparison: ${comparison}; ` : ''}viewer: ${viewer}}`;
98
104
  // Convert a `#meta.inputs` lookup into a real `propagateChoice: all` string input. RFV renders
99
105
  // this natively (it ignores `meta.inputs`); selecting a row fills the matching inputs by name.
100
106
  // Name / brace parsing mirrors DiffStudio's getLookupsInfo (utils.ts): the name is the text
@@ -1,116 +0,0 @@
1
- "use strict";
2
-
3
- var _vitest = require("vitest");
4
- var _build = require("../commands/build");
5
- (0, _vitest.describe)('getNestedValue', () => {
6
- (0, _vitest.it)('returns value for a simple key', () => {
7
- (0, _vitest.expect)((0, _build.getNestedValue)({
8
- name: 'Chem'
9
- }, 'name')).toBe('Chem');
10
- });
11
- (0, _vitest.it)('returns value for a nested path', () => {
12
- (0, _vitest.expect)((0, _build.getNestedValue)({
13
- a: {
14
- b: {
15
- c: 42
16
- }
17
- }
18
- }, 'a.b.c')).toBe(42);
19
- });
20
- (0, _vitest.it)('returns undefined for a missing key', () => {
21
- (0, _vitest.expect)((0, _build.getNestedValue)({
22
- name: 'Chem'
23
- }, 'version')).toBeUndefined();
24
- });
25
- (0, _vitest.it)('returns undefined when a mid-path segment is null', () => {
26
- (0, _vitest.expect)((0, _build.getNestedValue)({
27
- a: null
28
- }, 'a.b')).toBeUndefined();
29
- });
30
- (0, _vitest.it)('returns undefined when a mid-path segment is missing', () => {
31
- (0, _vitest.expect)((0, _build.getNestedValue)({
32
- a: {}
33
- }, 'a.b.c')).toBeUndefined();
34
- });
35
- (0, _vitest.it)('returns undefined for an empty path (splits to empty string key)', () => {
36
- (0, _vitest.expect)((0, _build.getNestedValue)({
37
- x: 1
38
- }, '')).toBeUndefined();
39
- });
40
- });
41
- const pkg = overrides => ({
42
- dir: '/tmp/pkg',
43
- name: overrides.name ?? 'test-pkg',
44
- friendlyName: overrides.friendlyName ?? overrides.name ?? 'Test Pkg',
45
- version: overrides.version ?? '1.0.0',
46
- packageJson: overrides
47
- });
48
- (0, _vitest.describe)('applyFilter', () => {
49
- const packages = [pkg({
50
- name: 'Chem',
51
- version: '1.5.0',
52
- category: 'Cheminformatics'
53
- }), pkg({
54
- name: 'Bio',
55
- version: '2.0.0',
56
- category: 'Bioinformatics'
57
- }), pkg({
58
- name: 'PowerGrid',
59
- version: '1.5.0',
60
- category: 'Viewers'
61
- })];
62
- (0, _vitest.it)('returns all packages when filter matches all', () => {
63
- (0, _vitest.expect)((0, _build.applyFilter)(packages, 'name:.')).toHaveLength(3);
64
- });
65
- (0, _vitest.it)('filters by exact name match', () => {
66
- const result = (0, _build.applyFilter)(packages, 'name:^Chem$');
67
- (0, _vitest.expect)(result).toHaveLength(1);
68
- (0, _vitest.expect)(result[0].name).toBe('Chem');
69
- });
70
- (0, _vitest.it)('filters by partial name (regex substring)', () => {
71
- const result = (0, _build.applyFilter)(packages, 'name:Bio');
72
- (0, _vitest.expect)(result).toHaveLength(1);
73
- (0, _vitest.expect)(result[0].name).toBe('Bio');
74
- });
75
- (0, _vitest.it)('returns empty array when nothing matches', () => {
76
- (0, _vitest.expect)((0, _build.applyFilter)(packages, 'name:NOMATCH')).toHaveLength(0);
77
- });
78
- (0, _vitest.it)('filters by version', () => {
79
- const result = (0, _build.applyFilter)(packages, 'version:^1\\.5');
80
- (0, _vitest.expect)(result).toHaveLength(2);
81
- (0, _vitest.expect)(result.map(p => p.name)).toEqual(_vitest.expect.arrayContaining(['Chem', 'PowerGrid']));
82
- });
83
- (0, _vitest.it)('applies && conjunction (both conditions must match)', () => {
84
- const result = (0, _build.applyFilter)(packages, 'name:Chem && version:1\\.5');
85
- (0, _vitest.expect)(result).toHaveLength(1);
86
- (0, _vitest.expect)(result[0].name).toBe('Chem');
87
- });
88
- (0, _vitest.it)('returns empty when one part of && conjunction fails', () => {
89
- (0, _vitest.expect)((0, _build.applyFilter)(packages, 'name:Chem && version:^2')).toHaveLength(0);
90
- });
91
- (0, _vitest.it)('filters by nested field', () => {
92
- const withNested = [pkg({
93
- name: 'A',
94
- datagrok: {
95
- apiVersion: '1.0'
96
- }
97
- }), pkg({
98
- name: 'B',
99
- datagrok: {
100
- apiVersion: '2.0'
101
- }
102
- })];
103
- const result = (0, _build.applyFilter)(withNested, 'datagrok.apiVersion:^1');
104
- (0, _vitest.expect)(result).toHaveLength(1);
105
- (0, _vitest.expect)(result[0].name).toBe('A');
106
- });
107
- (0, _vitest.it)('returns empty when field does not exist', () => {
108
- (0, _vitest.expect)((0, _build.applyFilter)(packages, 'nonexistent:anything')).toHaveLength(0);
109
- });
110
- (0, _vitest.it)('treats filter with no colon as field name with match-all pattern', () => {
111
- // No colon → field = whole string, pattern = /./ (matches any value)
112
- // The function returns packages where the field exists and is non-empty
113
- const result = (0, _build.applyFilter)(packages, 'name');
114
- (0, _vitest.expect)(result).toHaveLength(3);
115
- });
116
- });