@jskit-ai/jskit-catalog 0.2.45 → 0.2.46
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/catalog/packages.json +148 -148
- package/package.json +1 -1
- package/scripts/data-access-files.mjs +61 -16
- package/scripts/data-access-source.mjs +169 -0
- package/scripts/data-access.mjs +132 -142
package/scripts/data-access.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { ROOT, lintIgnores, sourceFiles } from "./data-access-files.mjs";
|
|
3
|
+
import { ROOT, lintIgnores, sourceFiles, vueFiles } from "./data-access-files.mjs";
|
|
4
4
|
import { toPosixPath } from "./files.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
assignedVariable, blank, closingOffset, enclosingObject, identifierAt, keyValue, nextCodeOffset, objectArgument, objectAt,
|
|
7
|
+
objectKeys, readSourceFile, readValue, topLevelKeys
|
|
8
|
+
} from "./data-access-source.mjs";
|
|
6
9
|
|
|
7
10
|
// The data part of plain `jskit check`, read from source, never run. It backs up what the lint rules cannot see across
|
|
8
11
|
// files: every table has exactly one table resource (`defineTableResource({ name, tableName })`) across the app and its
|
|
@@ -15,8 +18,10 @@ import { blankJsComments, lineIndex } from "./material3-source.mjs";
|
|
|
15
18
|
// too; an eslint-disable or inline eslint config that turns off a data-access rule, or every rule, fails in any file
|
|
16
19
|
// but the client's; and so does an app file the app's own ESLint config lints without every data-access rule as an
|
|
17
20
|
// error, or ignores though it is server code. Whatever else that config ignores, no lint reads, so neither does this
|
|
18
|
-
// check: it lists what it skipped.
|
|
19
|
-
//
|
|
21
|
+
// check: it lists what it skipped. Every app table resource a lookup field picks from or a CRUD list screen shows defines
|
|
22
|
+
// searchSchema.q, on the resource or its owns entry, since the picker and the list's Search box send what the person
|
|
23
|
+
// types as q. As information, it lists the fields of each app table resource that its CRUD contracts never name, which
|
|
24
|
+
// no API shows or accepts.
|
|
20
25
|
|
|
21
26
|
const GUIDE = "https://mobily-enterprises.github.io/jskit-ai/guide/framework/data-access";
|
|
22
27
|
const SECTIONS = Object.freeze({
|
|
@@ -27,7 +32,8 @@ const SECTIONS = Object.freeze({
|
|
|
27
32
|
"data/runtime-capability": "the-resource-is-the-only-door",
|
|
28
33
|
"data/lint-disabled": "hard-rules",
|
|
29
34
|
"data/lint-config": "hard-rules",
|
|
30
|
-
"data/unnamed-fields": "layer-2-the-crud-surface"
|
|
35
|
+
"data/unnamed-fields": "layer-2-the-crud-surface",
|
|
36
|
+
"data/search-q": "narrow-in-the-query"
|
|
31
37
|
});
|
|
32
38
|
// What each rule asks, said once above its findings in the report.
|
|
33
39
|
const RULE_EXPLANATIONS = Object.freeze({
|
|
@@ -41,6 +47,9 @@ const RULE_EXPLANATIONS = Object.freeze({
|
|
|
41
47
|
"takes any other one once the baseline's systemScopes lists it with its reason.",
|
|
42
48
|
"data/lint-disabled": "neither eslint-disable nor an inline eslint config comment can turn off a data-access rule, and a " +
|
|
43
49
|
"disable that names no rule turns them all off. Change the code, or name the rules the directive turns off.",
|
|
50
|
+
"data/search-q": "a lookup field's picker and a CRUD list screen's Search box send what the person types as q, so the " +
|
|
51
|
+
"table resource they read defines searchSchema.q (on the resource or its owns entry), searching the fields a person " +
|
|
52
|
+
"would type; without it, every search fails. Searching the rows a page loaded instead misses every row past it.",
|
|
44
53
|
"data/lint-config": "the app's ESLint config makes every data-access rule an error in each server file, and ignores no " +
|
|
45
54
|
"server code; a config that turns one off, lowers it or ignores the file hides that code from it. Put the rule back, or, " +
|
|
46
55
|
"once the module's whole job is data access, list { \"file\", \"rule\" } in the baseline's \"infrastructure\"."
|
|
@@ -78,7 +87,7 @@ const CLIENT_FILE = new RegExp(`^${ROOT}src/client/`, "u");
|
|
|
78
87
|
// tests, migrations, seeds and build output at a root, seed scripts, and test files.
|
|
79
88
|
const LINT_EXEMPT = new RegExp(`^${ROOT}(?:(?:src/client|src/test|test|tests|__tests__|e2e|test-support|migrations|seeds?|` +
|
|
80
89
|
"dist|build|coverage)/|(?:(?:scripts|bin)/)?seed[^/]*\\.[cm]?js$)|\\.(?:test|spec|vitest)\\.[^/]*$", "u");
|
|
81
|
-
const MARKERS = new RegExp(`defineTableResource|defineCrudContract|systemScope|systemContext|readable-check: allow|eslint|${CAPABILITY_NAMES}`, "iu");
|
|
90
|
+
const MARKERS = new RegExp(`defineTableResource|defineCrudContract|systemScope|systemContext|readable-check: allow|eslint|lookup|searchSchema|useCrudListScreen|${CAPABILITY_NAMES}`, "iu");
|
|
82
91
|
const TABLE_RESOURCE_CALL = /\bdefineTableResource\s*\(/gu;
|
|
83
92
|
const TABLE_RESOURCE_MARKER = /\bdefineTableResource\s*\(/u;
|
|
84
93
|
// A CRUD contract on a table resource named by its variable: `defineCrudContract(receivalResource, { ... })`.
|
|
@@ -96,134 +105,6 @@ const FEATURE_CALL = /\bdefine(?:Crud)?Feature\s*\(/gu;
|
|
|
96
105
|
const SYSTEM_CONTEXT_CALL = /\bsystemContext\s*\(/gu;
|
|
97
106
|
const INFRASTRUCTURE_EXCUSE = /readable-check: allow (?:jskit\/)?(no-direct-knex|no-transaction-reader|no-hand-scope|sql-literal) - (infrastructure:.*?)\s*(?:\*\/)?\s*$/u;
|
|
98
107
|
|
|
99
|
-
const blank = (text) => text.replace(/[^\n]/gu, " ");
|
|
100
|
-
|
|
101
|
-
// Code with its comments and the contents of its strings and templates blanked to spaces, every offset kept: calls
|
|
102
|
-
// and keys are found in it, values are read from the code at the same offsets.
|
|
103
|
-
function maskStrings(code) {
|
|
104
|
-
let output = "";
|
|
105
|
-
let index = 0;
|
|
106
|
-
while (index < code.length) {
|
|
107
|
-
const quote = code[index];
|
|
108
|
-
if (quote !== "\"" && quote !== "'" && quote !== "`") {
|
|
109
|
-
output += quote;
|
|
110
|
-
index += 1;
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
let end = index + 1;
|
|
114
|
-
while (end < code.length && code[end] !== quote && (quote === "`" || code[end] !== "\n")) {
|
|
115
|
-
end += code[end] === "\\" ? 2 : 1;
|
|
116
|
-
}
|
|
117
|
-
output += quote + blank(code.slice(index + 1, end)) + code.slice(end, end + 1);
|
|
118
|
-
index = end + 1;
|
|
119
|
-
}
|
|
120
|
-
return output;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// The offset just past the bracket that closes the one at `start`, in masked code; -1 when it never closes.
|
|
124
|
-
function closingOffset(masked, start) {
|
|
125
|
-
let depth = 0;
|
|
126
|
-
for (let index = start; index < masked.length; index += 1) {
|
|
127
|
-
if ("([{".includes(masked[index])) depth += 1;
|
|
128
|
-
else if (")]}".includes(masked[index]) && --depth === 0) return index + 1;
|
|
129
|
-
}
|
|
130
|
-
return -1;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const nextCodeOffset = (text, offset) => offset + (/^\s*/u.exec(text.slice(offset))?.[0].length || 0);
|
|
134
|
-
|
|
135
|
-
// The string a top-level `const NAME = "..."` gives, or null.
|
|
136
|
-
function constantValue(code, name) {
|
|
137
|
-
const match = new RegExp(`(?:^|[\\s;])const\\s+${name}\\s*=\\s*(["'\`])((?:\\\\.|(?!\\1)[^\\\\\\n])*)\\1\\s*[;,\\n]`, "u").exec(code);
|
|
138
|
-
return match && !(match[1] === "`" && match[2].includes("${")) ? match[2] : null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* The value written at an offset, read up to the comma or bracket that ends it: { text } for a string literal, a
|
|
143
|
-
* template without expressions, or a const holding one; { text: null } for anything else.
|
|
144
|
-
*/
|
|
145
|
-
function readValue(code, masked, offset) {
|
|
146
|
-
const start = nextCodeOffset(code, offset);
|
|
147
|
-
const quote = code[start];
|
|
148
|
-
if (quote === "\"" || quote === "'" || quote === "`") {
|
|
149
|
-
let end = start + 1;
|
|
150
|
-
while (end < code.length && code[end] !== quote) end += code[end] === "\\" ? 2 : 1;
|
|
151
|
-
const after = masked[nextCodeOffset(masked, end + 1)];
|
|
152
|
-
const text = code.slice(start + 1, end);
|
|
153
|
-
const whole = after === "," || after === "}" || after === ")";
|
|
154
|
-
return { text: whole && !(quote === "`" && text.includes("${")) ? text.replace(/\\(.)/gu, "$1") : null };
|
|
155
|
-
}
|
|
156
|
-
const identifier = /^[A-Za-z_$][\w$]*(?=\s*[,})])/u.exec(code.slice(start));
|
|
157
|
-
return { text: identifier ? constantValue(code, identifier[0]) : null };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// The top-level keys of the object literal between `start` and `end`, in masked code: for each wanted key, the offset
|
|
161
|
-
// just past its colon, or "shorthand" for `{ name }`.
|
|
162
|
-
function topLevelKeys(masked, start, end, names) {
|
|
163
|
-
let depth = 0;
|
|
164
|
-
let top = "";
|
|
165
|
-
for (let index = start; index < end; index += 1) {
|
|
166
|
-
const char = masked[index];
|
|
167
|
-
if ("([{".includes(char)) depth += 1;
|
|
168
|
-
top += depth === 1 || char === "\n" ? char : " ";
|
|
169
|
-
if (")]}".includes(char)) depth -= 1;
|
|
170
|
-
}
|
|
171
|
-
const keys = new Map();
|
|
172
|
-
for (const match of top.matchAll(/(?:^|[{,])\s*([A-Za-z_$][\w$]*)\s*(:|(?=[,}]))/gu)) {
|
|
173
|
-
const [whole, key, colon] = match;
|
|
174
|
-
if (names.includes(key) && !keys.has(key)) keys.set(key, colon ? start + match.index + whole.length : "shorthand");
|
|
175
|
-
}
|
|
176
|
-
return keys;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// The object literal a call at `callOffset` takes as its first argument: { start, end } in masked code, or null when the
|
|
180
|
-
// call is a declaration (`function defineFeature(`) or its first argument is anything else.
|
|
181
|
-
function objectArgument(masked, callOffset, matchLength) {
|
|
182
|
-
if (/function\s*\*?\s*$/u.test(masked.slice(Math.max(0, callOffset - 20), callOffset))) return null;
|
|
183
|
-
const start = nextCodeOffset(masked, callOffset + matchLength);
|
|
184
|
-
if (masked[start] !== "{") return null;
|
|
185
|
-
const end = closingOffset(masked, start);
|
|
186
|
-
return end < 0 ? null : { start, end };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// The value of one key of an object literal, as readValue reads it; undefined when the object has no such key.
|
|
190
|
-
function keyValue(code, masked, keys, key) {
|
|
191
|
-
const at = keys.get(key);
|
|
192
|
-
if (at === undefined) return undefined;
|
|
193
|
-
return at === "shorthand" ? { text: constantValue(code, key) } : readValue(code, masked, at);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function readSourceFile(source) {
|
|
197
|
-
const code = blankJsComments(source);
|
|
198
|
-
return { source, code, masked: maskStrings(code), position: lineIndex(source) };
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Every key of the object literal between `start` and `end` (masked code), in order, and whether the literal also
|
|
202
|
-
// spreads or computes keys, so that its keys are not all known.
|
|
203
|
-
function objectKeys(masked, start, end) {
|
|
204
|
-
let depth = 0;
|
|
205
|
-
let top = "";
|
|
206
|
-
for (let index = start; index < end; index += 1) {
|
|
207
|
-
const char = masked[index];
|
|
208
|
-
if ("([{".includes(char)) depth += 1;
|
|
209
|
-
top += depth === 1 || char === "\n" ? char : " ";
|
|
210
|
-
if (")]}".includes(char)) depth -= 1;
|
|
211
|
-
}
|
|
212
|
-
const keys = [...top.matchAll(/(?:^|[{,])\s*([A-Za-z_$][\w$]*)\s*(?::|(?=[,}]))/gu)].map((match) => ({
|
|
213
|
-
key: match[1],
|
|
214
|
-
at: start + match.index + match[0].length
|
|
215
|
-
}));
|
|
216
|
-
return { keys, partial: /(?:^|[{,])\s*(?:\.\.\.|["'`[])/u.test(top) };
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// The object literal written at an offset: { start, end } in masked code, or null for anything else.
|
|
220
|
-
function objectAt(masked, offset) {
|
|
221
|
-
const start = nextCodeOffset(masked, offset);
|
|
222
|
-
if (masked[start] !== "{") return null;
|
|
223
|
-
const end = closingOffset(masked, start);
|
|
224
|
-
return end < 0 ? null : { start, end };
|
|
225
|
-
}
|
|
226
|
-
|
|
227
108
|
// A table resource's schema fields that a CRUD contract could show: { names, partial }, or null when the schema is
|
|
228
109
|
// not an object literal. The ownership fields, the stamped timestamps, the primary key and hidden fields are left out.
|
|
229
110
|
function readSchemaFields(code, masked, keys, ownership) {
|
|
@@ -249,40 +130,142 @@ function readTableResources(file, { code, masked, position }) {
|
|
|
249
130
|
for (const match of masked.matchAll(TABLE_RESOURCE_CALL)) {
|
|
250
131
|
const object = objectArgument(masked, match.index, match[0].length);
|
|
251
132
|
if (!object) continue;
|
|
252
|
-
const keys = topLevelKeys(masked, object.start, object.end, ["name", "tableName", "ownership", "schema", "foreignKeys"]);
|
|
133
|
+
const keys = topLevelKeys(masked, object.start, object.end, ["name", "tableName", "ownership", "schema", "foreignKeys", "searchSchema"]);
|
|
253
134
|
const ownership = keyValue(code, masked, keys, "ownership")?.text ?? null;
|
|
254
135
|
resources.push({
|
|
255
136
|
file,
|
|
256
137
|
line: position(match.index).line,
|
|
257
138
|
name: keyValue(code, masked, keys, "name")?.text ?? null,
|
|
258
139
|
tableName: keyValue(code, masked, keys, "tableName")?.text ?? null,
|
|
259
|
-
variable:
|
|
140
|
+
variable: assignedVariable(masked, match.index),
|
|
260
141
|
fields: readSchemaFields(code, masked, keys, ownership),
|
|
261
|
-
foreignKeys: keys.has("foreignKeys")
|
|
142
|
+
foreignKeys: keys.has("foreignKeys"),
|
|
143
|
+
searchQ: readSearchQ(masked, keys)
|
|
262
144
|
});
|
|
263
145
|
}
|
|
264
146
|
return resources;
|
|
265
147
|
}
|
|
266
148
|
|
|
267
|
-
//
|
|
268
|
-
//
|
|
149
|
+
// Whether an object literal's `searchSchema` key defines q: true or false, or null when it is not written as an object
|
|
150
|
+
// literal whose keys are all known. `keys` are the literal's top-level keys (topLevelKeys).
|
|
151
|
+
function readSearchQ(masked, keys) {
|
|
152
|
+
const at = keys.get("searchSchema");
|
|
153
|
+
if (at === undefined) return false;
|
|
154
|
+
const schema = typeof at === "number" ? objectAt(masked, at) : null;
|
|
155
|
+
const named = schema ? objectKeys(masked, schema.start, schema.end) : null;
|
|
156
|
+
if (named?.keys.some(({ key }) => key === "q")) return true;
|
|
157
|
+
return named && !named.partial ? false : null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Every CRUD contract a file defines: its variable and namespace, the variable of its table resource and the fields its
|
|
161
|
+
// `fields` names, null when they are not all written as literal keys.
|
|
269
162
|
function readCrudContracts({ code, masked }) {
|
|
270
163
|
const contracts = [];
|
|
271
164
|
for (const match of masked.matchAll(CRUD_CONTRACT_CALL)) {
|
|
165
|
+
const variable = assignedVariable(masked, match.index);
|
|
272
166
|
const config = objectAt(masked, match.index + match[0].length);
|
|
273
167
|
if (!config) {
|
|
274
|
-
contracts.push({ resource: match[1], fields: null });
|
|
168
|
+
contracts.push({ variable, namespace: null, resource: match[1], fields: null });
|
|
275
169
|
continue;
|
|
276
170
|
}
|
|
277
|
-
const
|
|
171
|
+
const keys = topLevelKeys(masked, config.start, config.end, ["fields", "namespace"]);
|
|
172
|
+
const at = keys.get("fields");
|
|
278
173
|
const fields = typeof at === "number" ? objectAt(masked, at) : null;
|
|
279
174
|
const named = fields ? objectKeys(masked, fields.start, fields.end) : null;
|
|
280
175
|
const known = at === undefined || (named && !named.partial);
|
|
281
|
-
contracts.push({
|
|
176
|
+
contracts.push({
|
|
177
|
+
variable,
|
|
178
|
+
namespace: keyValue(code, masked, keys, "namespace")?.text ?? null,
|
|
179
|
+
resource: match[1],
|
|
180
|
+
fields: at === undefined ? [] : known ? named.keys.map(({ key }) => key) : null
|
|
181
|
+
});
|
|
282
182
|
}
|
|
283
183
|
return contracts;
|
|
284
184
|
}
|
|
285
185
|
|
|
186
|
+
/**
|
|
187
|
+
* What a file says about searches: the namespaces its lookup fields pick from (`lookup: "<namespace>"`, or a relation
|
|
188
|
+
* `{ kind: "lookup", namespace }`), the table resource variables its owns entries give a searchSchema
|
|
189
|
+
* (`{ resource, searchSchema }`, with searchQ as readSearchQ reads it), and the CRUD contract variables its list screens
|
|
190
|
+
* show (`useCrudListScreen({ resource })`).
|
|
191
|
+
*/
|
|
192
|
+
function readSearches(file, { code, masked, position }) {
|
|
193
|
+
const lookups = [];
|
|
194
|
+
for (const match of masked.matchAll(/\b(lookup|kind)\s*:/gu)) {
|
|
195
|
+
const value = readValue(code, masked, match.index + match[0].length).text;
|
|
196
|
+
if (match[1] === "lookup" && value) {
|
|
197
|
+
lookups.push({ file, line: position(match.index).line, namespace: value });
|
|
198
|
+
} else if (match[1] === "kind" && value === "lookup") {
|
|
199
|
+
const object = enclosingObject(masked, match.index);
|
|
200
|
+
const namespace = object && keyValue(code, masked, topLevelKeys(masked, object.start, object.end, ["namespace"]), "namespace")?.text;
|
|
201
|
+
if (namespace) lookups.push({ file, line: position(match.index).line, namespace });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const owned = [];
|
|
205
|
+
for (const match of masked.matchAll(/\bsearchSchema\s*:/gu)) {
|
|
206
|
+
const object = enclosingObject(masked, match.index);
|
|
207
|
+
const keys = object ? topLevelKeys(masked, object.start, object.end, ["resource", "searchSchema"]) : new Map();
|
|
208
|
+
const at = keys.get("resource");
|
|
209
|
+
const variable = typeof at === "number" ? identifierAt(code, at) : null;
|
|
210
|
+
if (variable) owned.push({ variable, searchQ: readSearchQ(masked, keys) });
|
|
211
|
+
}
|
|
212
|
+
const screens = [];
|
|
213
|
+
for (const match of masked.matchAll(/\buseCrudListScreen\s*\(/gu)) {
|
|
214
|
+
const object = objectArgument(masked, match.index, match[0].length);
|
|
215
|
+
const at = object && topLevelKeys(masked, object.start, object.end, ["resource"]).get("resource");
|
|
216
|
+
const contract = typeof at === "number" ? identifierAt(code, at) : null;
|
|
217
|
+
if (contract) screens.push({ file, line: position(match.index).line, contract });
|
|
218
|
+
}
|
|
219
|
+
return { lookups, owned, screens };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// A Vue component's <script> blocks, everything else blanked with its line breaks kept, so offsets and lines hold.
|
|
223
|
+
const vueScripts = (source) => {
|
|
224
|
+
let scripts = blank(source);
|
|
225
|
+
for (const match of source.matchAll(/(<script\b[^>]*>)([\s\S]*?)<\/script>/gu)) {
|
|
226
|
+
const start = match.index + match[1].length;
|
|
227
|
+
scripts = scripts.slice(0, start) + match[2] + scripts.slice(start + match[2].length);
|
|
228
|
+
}
|
|
229
|
+
return scripts;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// The resource name a namespace stands for, as @jskit-ai/resource-core's resourceNameFromNamespace makes it:
|
|
233
|
+
// `processing-lots` and `/processing-lots` are `processingLots`.
|
|
234
|
+
const resourceNameFromNamespace = (namespace) => namespace.replace(/^\/+|\/+$/gu, "").replace(/\//gu, "-")
|
|
235
|
+
.replace(/[-_\s]+([A-Za-z0-9])/gu, (_, letter) => letter.toUpperCase());
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Fails each lookup and CRUD list screen whose app table resource defines no searchSchema.q, on the resource or an
|
|
239
|
+
* owns entry. A lookup names its table resource by namespace; a list screen by its CRUD contract's variable. One the
|
|
240
|
+
* check cannot follow (a framework resource, a contract or resource it cannot find, a searchSchema it cannot read) is
|
|
241
|
+
* left unjudged.
|
|
242
|
+
*/
|
|
243
|
+
function checkSearchQ(resources, contracts, searches, report) {
|
|
244
|
+
const appResources = resources.filter((resource) => !resource.framework);
|
|
245
|
+
const ownedQ = Map.groupBy(searches.flatMap(({ owned }) => owned), ({ variable }) => variable);
|
|
246
|
+
// An unreadable searchSchema (null) counts as one that defines q.
|
|
247
|
+
const searchQ = (resource) => resource.searchQ !== false || (ownedQ.get(resource.variable) || []).some(({ searchQ: q }) => q !== false);
|
|
248
|
+
const unique = (list) => (list.length === 1 ? list[0] : null);
|
|
249
|
+
const where = (resource) => `${resource.name} (${resource.file}:${resource.line})`;
|
|
250
|
+
const fix = "give it searchSchema: { q: { type: \"string\", oneOf: [<the fields a person types>], filterOperator: \"like\" } }.";
|
|
251
|
+
for (const lookup of searches.flatMap(({ lookups }) => lookups)) {
|
|
252
|
+
const name = resourceNameFromNamespace(lookup.namespace);
|
|
253
|
+
const resource = unique(appResources.filter((entry) => entry.name === name));
|
|
254
|
+
if (resource && !searchQ(resource)) {
|
|
255
|
+
report(lookup, "data/search-q", `lookup "${lookup.namespace}" picks from ${where(resource)}, which defines no ` +
|
|
256
|
+
`searchSchema.q: the picker sends what the person types as q, so ${fix}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
for (const screen of searches.flatMap(({ screens }) => screens)) {
|
|
260
|
+
const contract = unique(contracts.filter((entry) => entry.variable === screen.contract));
|
|
261
|
+
const resource = contract && unique(appResources.filter((entry) => entry.variable === contract.resource));
|
|
262
|
+
if (resource && !searchQ(resource)) {
|
|
263
|
+
report(screen, "data/search-q", `this list screen shows ${screen.contract}, whose table resource ${where(resource)} ` +
|
|
264
|
+
`defines no searchSchema.q: the list's Search box sends what the person types as q, so ${fix}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
286
269
|
// The fields of each app table resource that its CRUD contracts never name, for a resource with at least one contract
|
|
287
270
|
// whose fields are all known.
|
|
288
271
|
function unnamedFields(resources, contracts) {
|
|
@@ -568,6 +551,7 @@ async function checkDataAccess({ projectRoot = process.cwd() } = {}) {
|
|
|
568
551
|
const scopes = [];
|
|
569
552
|
const excuses = [];
|
|
570
553
|
const capabilities = [];
|
|
554
|
+
const searches = [];
|
|
571
555
|
const findings = [];
|
|
572
556
|
const report = ({ file, line }, rule, message) => findings.push({ file, line, rule, message });
|
|
573
557
|
for (const { file, absolute, framework } of sources) {
|
|
@@ -579,6 +563,7 @@ async function checkDataAccess({ projectRoot = process.cwd() } = {}) {
|
|
|
579
563
|
scopes.push(...readSystemScopes(name, read).map((scope) => ({ ...scope, framework })));
|
|
580
564
|
if (framework) continue;
|
|
581
565
|
contracts.push(...readCrudContracts(read));
|
|
566
|
+
searches.push(readSearches(name, read));
|
|
582
567
|
const fileExcuses = readInfrastructureExcuses(name, source);
|
|
583
568
|
excuses.push(...fileExcuses);
|
|
584
569
|
capabilities.push(...readRuntimeCapabilities(name, read).map((use) => ({ ...use, infrastructure: fileExcuses.length > 0 })));
|
|
@@ -589,7 +574,12 @@ async function checkDataAccess({ projectRoot = process.cwd() } = {}) {
|
|
|
589
574
|
: disable.rules.length > 0 ? `eslint-disable turns off ${disable.rules.join(", ")}` : "eslint-disable names no rule");
|
|
590
575
|
}
|
|
591
576
|
}
|
|
577
|
+
for (const { file, absolute } of await vueFiles(root)) {
|
|
578
|
+
const source = await readFile(absolute, "utf8");
|
|
579
|
+
if (source.includes("useCrudListScreen")) searches.push(readSearches(file, readSourceFile(vueScripts(source))));
|
|
580
|
+
}
|
|
592
581
|
checkTables(resources, report);
|
|
582
|
+
checkSearchQ(resources, contracts, searches, report);
|
|
593
583
|
checkSystemScopes(scopes, report);
|
|
594
584
|
const baseline = await readBaseline(root, report);
|
|
595
585
|
checkBaseline(baseline, excuses, scopes, report);
|