@anzusystems/common-admin 1.47.0-beta.36 → 1.47.0-beta.361

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.
@@ -0,0 +1,574 @@
1
+ const DEFAULT_DEPRECATED_IMPORTS = [
2
+ 'AFilterWrapper',
3
+ 'AFilterBooleanSelect',
4
+ 'AFilterBooleanGroup',
5
+ 'AFilterDatetimePicker',
6
+ 'AFilterInteger',
7
+ 'AFilterRemoteAutocomplete',
8
+ 'AFilterRemoteAutocompleteWithMinimal',
9
+ 'AFilterString',
10
+ 'AFilterValueObjectOptionsSelect',
11
+ 'ADatatableOrdering',
12
+ 'ADatatablePagination',
13
+ 'AFormRemoteAutocomplete',
14
+ 'ASubjectSelect',
15
+ 'usePagination',
16
+ 'useFilterHelpers',
17
+ 'createDatatableColumnsConfig',
18
+ 'useSubjectSelect',
19
+ 'useApiQueryBuilder',
20
+ 'useJobApi',
21
+ 'Pagination',
22
+ 'makeFilterHelper',
23
+ 'apiFetchList',
24
+ 'FilterBag',
25
+ 'Filter',
26
+ 'apiFetchByIds',
27
+ 'apiAnyRequest',
28
+ 'apiCreateOne',
29
+ 'apiDeleteOne',
30
+ 'apiFetchOne',
31
+ 'apiUpdateOne',
32
+ ]
33
+
34
+ const DEFAULT_INTERNAL_DEPRECATED_IMPORTS = [
35
+ {
36
+ path: '@/services/api/apiFetchList',
37
+ imports: ['apiFetchList'],
38
+ },
39
+ {
40
+ path: '@/services/api/apiFetchListBatch',
41
+ imports: ['apiFetchListBatch'],
42
+ },
43
+ {
44
+ path: '@/composables/system/pagination',
45
+ imports: ['usePagination', 'Pagination'],
46
+ },
47
+ {
48
+ path: '@/composables/filter/filterHelpers',
49
+ imports: ['useFilterHelpers', 'makeFilterHelper'],
50
+ },
51
+ {
52
+ path: '@/composables/system/datatableColumns',
53
+ imports: ['createDatatableColumnsConfig'],
54
+ },
55
+ {
56
+ path: '@/components/subjectSelect/useSubjectSelect',
57
+ imports: ['useSubjectSelect'],
58
+ },
59
+ {
60
+ path: '@/services/api/queryBuilder',
61
+ imports: ['useApiQueryBuilder'],
62
+ },
63
+ {
64
+ path: '@/services/api/job/jobApi',
65
+ imports: ['useJobApi'],
66
+ },
67
+ {
68
+ path: '@/types/Filter',
69
+ imports: ['FilterBag', 'Filter'],
70
+ },
71
+ {
72
+ path: '@/components/filter/AFilterWrapper',
73
+ imports: ['AFilterWrapper'],
74
+ },
75
+ {
76
+ path: '@/components/filter/AFilterBooleanSelect',
77
+ imports: ['AFilterBooleanSelect'],
78
+ },
79
+ {
80
+ path: '@/components/filter/AFilterBooleanGroup',
81
+ imports: ['AFilterBooleanGroup'],
82
+ },
83
+ {
84
+ path: '@/components/filter/AFilterDatetimePicker',
85
+ imports: ['AFilterDatetimePicker'],
86
+ },
87
+ {
88
+ path: '@/components/filter/AFilterInteger',
89
+ imports: ['AFilterInteger'],
90
+ },
91
+ {
92
+ path: '@/components/filter/AFilterRemoteAutocomplete',
93
+ imports: ['AFilterRemoteAutocomplete'],
94
+ },
95
+ {
96
+ path: '@/components/filter/AFilterRemoteAutocompleteWithMinimal',
97
+ imports: ['AFilterRemoteAutocompleteWithMinimal'],
98
+ },
99
+ {
100
+ path: '@/components/filter/AFilterString',
101
+ imports: ['AFilterString'],
102
+ },
103
+ {
104
+ path: '@/components/filter/AFilterValueObjectOptionsSelect',
105
+ imports: ['AFilterValueObjectOptionsSelect'],
106
+ },
107
+ {
108
+ path: '@/components/ADatatableOrdering',
109
+ imports: ['ADatatableOrdering'],
110
+ },
111
+ {
112
+ path: '@/components/ADatatablePagination',
113
+ imports: ['ADatatablePagination'],
114
+ },
115
+ {
116
+ path: '@/components/form/AFormRemoteAutocomplete',
117
+ imports: ['AFormRemoteAutocomplete'],
118
+ },
119
+ {
120
+ path: '@/components/subjectSelect/ASubjectSelect',
121
+ imports: ['ASubjectSelect'],
122
+ },
123
+ ]
124
+
125
+ const anzuPlugin = {
126
+ rules: {
127
+ 'no-ts-extension': {
128
+ meta: {
129
+ type: 'problem',
130
+ docs: {
131
+ description: 'Disallow .ts extension in import statements',
132
+ },
133
+ fixable: 'code',
134
+ schema: [],
135
+ },
136
+ create(context) {
137
+ return {
138
+ ImportDeclaration(node) {
139
+ const source = node.source.value
140
+ if (typeof source === 'string' && source.endsWith('.ts')) {
141
+ context.report({
142
+ node,
143
+ message: 'Do not include .ts extension in import paths',
144
+ fix(fixer) {
145
+ const sourceText = node.source.raw
146
+ const newSource = sourceText.replace(/\.ts(['"])$/, '$1')
147
+ return fixer.replaceText(node.source, newSource)
148
+ },
149
+ })
150
+ }
151
+ },
152
+ }
153
+ },
154
+ },
155
+
156
+ 'no-deprecated-imports': {
157
+ meta: {
158
+ type: 'problem',
159
+ docs: {
160
+ description: 'Disallow usage of deprecated imports',
161
+ },
162
+ schema: [
163
+ {
164
+ type: 'object',
165
+ properties: {
166
+ rules: {
167
+ type: 'array',
168
+ items: {
169
+ type: 'object',
170
+ properties: {
171
+ path: { type: 'string' },
172
+ module: { type: 'string' },
173
+ imports: {
174
+ type: 'array',
175
+ items: { type: 'string' },
176
+ },
177
+ },
178
+ required: ['imports'],
179
+ additionalProperties: false,
180
+ },
181
+ },
182
+ skipFiles: {
183
+ type: 'array',
184
+ items: { type: 'string' },
185
+ },
186
+ },
187
+ additionalProperties: false,
188
+ },
189
+ ],
190
+ },
191
+ create(context) {
192
+ const options = context.options[0] || {}
193
+ const deprecationRules = options.rules || []
194
+ const skipFiles = options.skipFiles || []
195
+
196
+ // Collect source file paths from path-based rules for auto-skip
197
+ const ruleFilePaths = deprecationRules
198
+ .filter((rule) => rule.path)
199
+ .map((rule) => {
200
+ if (rule.path.startsWith('@/')) {
201
+ return rule.path.replace('@/', 'src/')
202
+ }
203
+ return rule.path
204
+ })
205
+
206
+ return {
207
+ ImportDeclaration(node) {
208
+ const filename = context.filename
209
+ const normalizedFilename = filename.replace(/\\/g, '/')
210
+
211
+ // Check manual skip list
212
+ if (skipFiles.some((skip) => normalizedFilename.endsWith(skip))) return
213
+
214
+ // Auto-skip source files of path-based rules
215
+ if (
216
+ ruleFilePaths.some(
217
+ (rulePath) =>
218
+ normalizedFilename.endsWith(rulePath + '.ts') ||
219
+ normalizedFilename.endsWith(rulePath + '.js') ||
220
+ normalizedFilename.endsWith(rulePath + '.vue') ||
221
+ normalizedFilename.endsWith(rulePath),
222
+ )
223
+ )
224
+ return
225
+
226
+ const source = node.source.value
227
+ if (typeof source !== 'string') return
228
+
229
+ for (const rule of deprecationRules) {
230
+ const matchPath = rule.path || rule.module
231
+ if (!matchPath || source !== matchPath) continue
232
+
233
+ const deprecatedImports = node.specifiers
234
+ .filter((spec) => spec.type === 'ImportSpecifier')
235
+ .filter((spec) => rule.imports.includes(spec.imported.name))
236
+
237
+ for (const importSpec of deprecatedImports) {
238
+ context.report({
239
+ node: importSpec,
240
+ message: `'${importSpec.imported.name}' from '${matchPath}' is deprecated`,
241
+ })
242
+ }
243
+ }
244
+ },
245
+ }
246
+ },
247
+ },
248
+
249
+ 'url-params-match-template': {
250
+ meta: {
251
+ type: 'problem',
252
+ docs: {
253
+ description:
254
+ 'Ensure urlParams keys match the :placeholders declared in urlTemplate ' +
255
+ 'for useApiRequest / useApiFetchList / useApiFetchByIds / useApiFetchListBatch calls.',
256
+ },
257
+ schema: [],
258
+ },
259
+ create(context) {
260
+ const TARGET_CALLEES = new Set([
261
+ 'useApiRequest',
262
+ 'useApiFetchList',
263
+ 'useApiFetchByIds',
264
+ 'useApiFetchListBatch',
265
+ ])
266
+
267
+ const PLACEHOLDER_RE = /:([a-zA-Z_][\w]*)/g
268
+
269
+ const getCalleeName = (callee) => {
270
+ if (callee.type === 'Identifier') return callee.name
271
+ if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {
272
+ return callee.property.name
273
+ }
274
+ return null
275
+ }
276
+
277
+ const findProperty = (objectExpr, name) => {
278
+ for (const prop of objectExpr.properties) {
279
+ if (prop.type !== 'Property' || prop.computed) continue
280
+ const key = prop.key
281
+ const keyName =
282
+ key.type === 'Identifier' ? key.name : key.type === 'Literal' ? key.value : null
283
+ if (keyName === name) return prop
284
+ }
285
+ return null
286
+ }
287
+
288
+ const resolveIdentifierToString = (identNode) => {
289
+ const scope = context.sourceCode.getScope(identNode)
290
+ let cur = scope
291
+ while (cur) {
292
+ const variable = cur.variables.find((v) => v.name === identNode.name)
293
+ if (variable && variable.defs.length === 1) {
294
+ const def = variable.defs[0]
295
+ if (
296
+ def.type === 'Variable' &&
297
+ def.node.type === 'VariableDeclarator' &&
298
+ def.parent &&
299
+ def.parent.kind === 'const' &&
300
+ def.node.init
301
+ ) {
302
+ return resolveToString(def.node.init)
303
+ }
304
+ }
305
+ cur = cur.upper
306
+ }
307
+ return null
308
+ }
309
+
310
+ const resolveToString = (node) => {
311
+ if (!node) return null
312
+ if (node.type === 'Literal' && typeof node.value === 'string') return node.value
313
+ if (node.type === 'TemplateLiteral') {
314
+ let out = ''
315
+ for (let i = 0; i < node.quasis.length; i++) {
316
+ out += node.quasis[i].value.cooked
317
+ if (i < node.expressions.length) {
318
+ const part = resolveToString(node.expressions[i])
319
+ if (part === null) return null
320
+ out += part
321
+ }
322
+ }
323
+ return out
324
+ }
325
+ if (node.type === 'BinaryExpression' && node.operator === '+') {
326
+ const left = resolveToString(node.left)
327
+ const right = resolveToString(node.right)
328
+ if (left === null || right === null) return null
329
+ return left + right
330
+ }
331
+ if (node.type === 'Identifier') return resolveIdentifierToString(node)
332
+ return null
333
+ }
334
+
335
+ const collectStaticKeys = (objectExpr) => {
336
+ const keys = []
337
+ for (const prop of objectExpr.properties) {
338
+ if (prop.type !== 'Property') return null
339
+ if (prop.computed) return null
340
+ const key = prop.key
341
+ if (key.type === 'Identifier') keys.push(key.name)
342
+ else if (key.type === 'Literal' && typeof key.value === 'string') keys.push(key.value)
343
+ else return null
344
+ }
345
+ return keys
346
+ }
347
+
348
+ return {
349
+ CallExpression(node) {
350
+ const name = getCalleeName(node.callee)
351
+ if (!name || !TARGET_CALLEES.has(name)) return
352
+
353
+ const arg = node.arguments[0]
354
+ if (!arg || arg.type !== 'ObjectExpression') return
355
+
356
+ const templateProp = findProperty(arg, 'urlTemplate')
357
+ const paramsProp = findProperty(arg, 'urlParams')
358
+ if (!templateProp || !paramsProp) return
359
+ if (paramsProp.value.type !== 'ObjectExpression') return
360
+
361
+ const resolved = resolveToString(templateProp.value)
362
+ if (resolved === null) return
363
+
364
+ const placeholders = new Set()
365
+ let match
366
+ PLACEHOLDER_RE.lastIndex = 0
367
+ while ((match = PLACEHOLDER_RE.exec(resolved)) !== null) {
368
+ placeholders.add(match[1])
369
+ }
370
+
371
+ const paramKeys = collectStaticKeys(paramsProp.value)
372
+ if (paramKeys === null) return
373
+
374
+ const paramKeySet = new Set(paramKeys)
375
+
376
+ for (const placeholder of placeholders) {
377
+ if (!paramKeySet.has(placeholder)) {
378
+ context.report({
379
+ node: paramsProp,
380
+ message:
381
+ `urlParams is missing key '${placeholder}' required by urlTemplate ` +
382
+ `'${resolved}'.`,
383
+ })
384
+ }
385
+ }
386
+
387
+ for (const key of paramKeys) {
388
+ if (!placeholders.has(key)) {
389
+ context.report({
390
+ node: paramsProp,
391
+ message:
392
+ `urlParams key '${key}' has no matching ':${key}' placeholder in urlTemplate ` +
393
+ `'${resolved}'.`,
394
+ })
395
+ }
396
+ }
397
+ },
398
+ }
399
+ },
400
+ },
401
+
402
+ 'no-fatal-error-axios-check': {
403
+ meta: {
404
+ type: 'problem',
405
+ docs: {
406
+ description:
407
+ 'Disallow isAnzuFatalError + axios.isAxiosError(error.cause) pattern.' +
408
+ ' Labs API throws AnzuApiAxiosError instead.',
409
+ },
410
+ schema: [],
411
+ },
412
+ create(context) {
413
+ return {
414
+ LogicalExpression(node) {
415
+ if (node.operator !== '&&') return
416
+ if (node.parent.type === 'LogicalExpression' && node.parent.operator === '&&') return
417
+
418
+ const parts = []
419
+ let current = node
420
+ while (current.type === 'LogicalExpression' && current.operator === '&&') {
421
+ parts.unshift(current.right)
422
+ current = current.left
423
+ }
424
+ parts.unshift(current)
425
+
426
+ const hasFatalCheck = parts.some(
427
+ (part) => part.type === 'CallExpression' && part.callee.name === 'isAnzuFatalError',
428
+ )
429
+ const hasInstanceofErrorCheck = parts.some(
430
+ (part) =>
431
+ part.type === 'BinaryExpression' &&
432
+ part.operator === 'instanceof' &&
433
+ part.right.type === 'Identifier' &&
434
+ part.right.name === 'Error',
435
+ )
436
+ const hasAxiosCheck = parts.some(
437
+ (part) =>
438
+ part.type === 'CallExpression' &&
439
+ part.callee.type === 'MemberExpression' &&
440
+ part.callee.object.name === 'axios' &&
441
+ part.callee.property.name === 'isAxiosError',
442
+ )
443
+
444
+ if (hasAxiosCheck && (hasFatalCheck || hasInstanceofErrorCheck)) {
445
+ context.report({
446
+ node,
447
+ message:
448
+ 'Replace error type check && axios.isAxiosError(error.cause)' +
449
+ ' with isAnzuApiAxiosError(error).' +
450
+ ' Labs API throws AnzuApiAxiosError with typed AxiosError cause.',
451
+ })
452
+ }
453
+ },
454
+ }
455
+ },
456
+ },
457
+ },
458
+ }
459
+
460
+ /**
461
+ * Creates an ESLint flat config entry for Anzu rules.
462
+ *
463
+ * @param {Object} [options]
464
+ * @param {boolean|'error'|'warn'|'off'} [options.noTsExtension='error'] - Severity for no-ts-extension rule.
465
+ * @param {boolean|'error'|'warn'|'off'} [options.noFatalErrorAxiosCheck='error']
466
+ * - Severity for no-fatal-error-axios-check rule.
467
+ * @param {boolean|'error'|'warn'|'off'|Object} [options.deprecatedImports='error'] - Severity or config object.
468
+ * @param {string[]} [options.deprecatedImports.exclude] - Import names to remove from the default list.
469
+ * @param {string[]} [options.deprecatedImports.include] - Additional import names to add to the default list.
470
+ * @param {Array} [options.deprecatedImports.extraRules]
471
+ * - Additional rule entries ({ path, imports } or { module, imports }).
472
+ * @param {string[]} [options.deprecatedImports.skipFiles] - Files to skip (matched by suffix).
473
+ * @param {'error'|'warn'} [options.deprecatedImports.severity='error'] - Severity level.
474
+ * @param {'consumer'|'internal'} [options.deprecatedImports.mode='consumer'] - 'consumer' uses module-based defaults,
475
+ * 'internal' uses path-based defaults for common-admin development.
476
+ * @returns {Object} ESLint flat config entry
477
+ */
478
+ export function recommended(options = {}) {
479
+ const {
480
+ noTsExtension = 'error',
481
+ noFatalErrorAxiosCheck = 'error',
482
+ urlParamsMatchTemplate = 'error',
483
+ deprecatedImports = 'error',
484
+ } = options
485
+
486
+ const rules = {}
487
+
488
+ // no-ts-extension
489
+ const tsExtSeverity = normalizeSeverity(noTsExtension)
490
+ if (tsExtSeverity) {
491
+ rules['anzu/no-ts-extension'] = tsExtSeverity
492
+ }
493
+
494
+ // no-fatal-error-axios-check
495
+ const fatalSeverity = normalizeSeverity(noFatalErrorAxiosCheck)
496
+ if (fatalSeverity) {
497
+ rules['anzu/no-fatal-error-axios-check'] = fatalSeverity
498
+ }
499
+
500
+ // url-params-match-template
501
+ const urlParamsSeverity = normalizeSeverity(urlParamsMatchTemplate)
502
+ if (urlParamsSeverity) {
503
+ rules['anzu/url-params-match-template'] = urlParamsSeverity
504
+ }
505
+
506
+ // no-deprecated-imports
507
+ if (deprecatedImports !== false && deprecatedImports !== 'off') {
508
+ let severity = 'error'
509
+ const ruleEntries = []
510
+ let skipFiles = []
511
+
512
+ if (typeof deprecatedImports === 'object') {
513
+ severity = deprecatedImports.severity || 'error'
514
+ const mode = deprecatedImports.mode || 'consumer'
515
+
516
+ if (mode === 'internal') {
517
+ // Internal mode: path-based rules for common-admin development
518
+ ruleEntries.push(...DEFAULT_INTERNAL_DEPRECATED_IMPORTS)
519
+ } else {
520
+ // Consumer mode: module-based rules for projects using common-admin
521
+ let importsList = [...DEFAULT_DEPRECATED_IMPORTS]
522
+ if (deprecatedImports.exclude) {
523
+ importsList = importsList.filter((name) => !deprecatedImports.exclude.includes(name))
524
+ }
525
+ if (deprecatedImports.include) {
526
+ importsList.push(...deprecatedImports.include)
527
+ }
528
+ ruleEntries.push({
529
+ module: '@anzusystems/common-admin',
530
+ imports: importsList,
531
+ })
532
+ }
533
+
534
+ if (deprecatedImports.extraRules) {
535
+ ruleEntries.push(...deprecatedImports.extraRules)
536
+ }
537
+ if (deprecatedImports.skipFiles) {
538
+ skipFiles = deprecatedImports.skipFiles
539
+ }
540
+ } else {
541
+ if (deprecatedImports === 'warn') {
542
+ severity = 'warn'
543
+ }
544
+ // Default consumer mode
545
+ ruleEntries.push({
546
+ module: '@anzusystems/common-admin',
547
+ imports: [...DEFAULT_DEPRECATED_IMPORTS],
548
+ })
549
+ }
550
+
551
+ const ruleConfig = { rules: ruleEntries }
552
+ if (skipFiles.length > 0) {
553
+ ruleConfig.skipFiles = skipFiles
554
+ }
555
+
556
+ rules['anzu/no-deprecated-imports'] = [severity, ruleConfig]
557
+ }
558
+
559
+ return {
560
+ plugins: {
561
+ anzu: anzuPlugin,
562
+ },
563
+ rules,
564
+ }
565
+ }
566
+
567
+ function normalizeSeverity(value) {
568
+ if (value === false || value === 'off') return null
569
+ if (value === true || value === 'error') return 'error'
570
+ if (value === 'warn') return 'warn'
571
+ return 'error'
572
+ }
573
+
574
+ export { anzuPlugin, DEFAULT_DEPRECATED_IMPORTS, DEFAULT_INTERNAL_DEPRECATED_IMPORTS }