@dinoxx/dinox-cli 1.1.1 → 2.1.0

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.
Files changed (141) hide show
  1. package/README.md +44 -3
  2. package/dist/cli.js +4 -0
  3. package/dist/commands/auth/index.js +1 -2
  4. package/dist/commands/boxes/index.js +37 -13
  5. package/dist/commands/boxes/operations.d.ts +3 -3
  6. package/dist/commands/boxes/operations.js +51 -28
  7. package/dist/commands/info/index.js +30 -5
  8. package/dist/commands/meta/repo.js +2 -1
  9. package/dist/commands/notes/actions.js +20 -37
  10. package/dist/commands/notes/bulk.d.ts +2 -2
  11. package/dist/commands/notes/bulk.js +3 -18
  12. package/dist/commands/notes/contentPatch.d.ts +5 -1
  13. package/dist/commands/notes/contentPatch.js +12 -1
  14. package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
  15. package/dist/commands/notes/contentPatchCommand.js +29 -8
  16. package/dist/commands/notes/descriptors.js +19 -16
  17. package/dist/commands/notes/export.js +3 -0
  18. package/dist/commands/notes/repo/read.d.ts +8 -0
  19. package/dist/commands/notes/repo/read.js +321 -0
  20. package/dist/commands/notes/repo/resolve.d.ts +8 -0
  21. package/dist/commands/notes/repo/resolve.js +187 -0
  22. package/dist/commands/notes/repo/search.d.ts +42 -0
  23. package/dist/commands/notes/repo/search.js +513 -0
  24. package/dist/commands/notes/repo/shared.d.ts +190 -0
  25. package/dist/commands/notes/repo/shared.js +102 -0
  26. package/dist/commands/notes/repo/write.d.ts +24 -0
  27. package/dist/commands/notes/repo/write.js +251 -0
  28. package/dist/commands/notes/repo.d.ts +7 -177
  29. package/dist/commands/notes/repo.js +7 -1310
  30. package/dist/commands/notes/searchOutput.d.ts +2 -1
  31. package/dist/commands/notes/searchOutput.js +2 -1
  32. package/dist/commands/notes/searchSql.js +9 -3
  33. package/dist/commands/prompt/index.d.ts +1 -0
  34. package/dist/commands/prompt/index.js +26 -11
  35. package/dist/commands/savedViews/index.d.ts +7 -0
  36. package/dist/commands/savedViews/index.js +458 -0
  37. package/dist/commands/savedViews/model.d.ts +87 -0
  38. package/dist/commands/savedViews/model.js +306 -0
  39. package/dist/commands/savedViews/repo.d.ts +123 -0
  40. package/dist/commands/savedViews/repo.js +512 -0
  41. package/dist/commands/savedViews/semantics.d.ts +46 -0
  42. package/dist/commands/savedViews/semantics.js +738 -0
  43. package/dist/commands/schema/commandMetadata.js +4 -4
  44. package/dist/commands/schema/index.d.ts +1 -0
  45. package/dist/commands/schema/index.js +23 -2
  46. package/dist/commands/schema/renderSkills.d.ts +6 -0
  47. package/dist/commands/schema/renderSkills.js +109 -11
  48. package/dist/commands/schema/spec.js +4 -0
  49. package/dist/commands/skills/index.d.ts +80 -0
  50. package/dist/commands/skills/index.js +572 -0
  51. package/dist/commands/storage/index.js +2 -2
  52. package/dist/commands/storage/repo/configs.d.ts +30 -0
  53. package/dist/commands/storage/repo/configs.js +259 -0
  54. package/dist/commands/storage/repo/operations.d.ts +25 -0
  55. package/dist/commands/storage/repo/operations.js +337 -0
  56. package/dist/commands/storage/repo/remote.d.ts +32 -0
  57. package/dist/commands/storage/repo/remote.js +132 -0
  58. package/dist/commands/storage/repo/resources.d.ts +12 -0
  59. package/dist/commands/storage/repo/resources.js +134 -0
  60. package/dist/commands/storage/repo/shared.d.ts +181 -0
  61. package/dist/commands/storage/repo/shared.js +36 -0
  62. package/dist/commands/storage/repo/targets.d.ts +12 -0
  63. package/dist/commands/storage/repo/targets.js +178 -0
  64. package/dist/commands/storage/repo.d.ts +3 -166
  65. package/dist/commands/storage/repo.js +4 -1034
  66. package/dist/commands/sync.js +2 -1
  67. package/dist/commands/tags/index.js +30 -11
  68. package/dist/commands/tags/operations.d.ts +3 -3
  69. package/dist/commands/tags/operations.js +45 -20
  70. package/dist/commands/templates/model.d.ts +50 -0
  71. package/dist/commands/templates/model.js +239 -0
  72. package/dist/commands/todo/index.js +3 -3
  73. package/dist/commands/update.d.ts +1 -0
  74. package/dist/commands/update.js +46 -9
  75. package/dist/commands/writeConfirmation.d.ts +13 -0
  76. package/dist/commands/writeConfirmation.js +54 -0
  77. package/dist/config/resolve.js +5 -10
  78. package/dist/config/types.d.ts +3 -6
  79. package/dist/constants/links.d.ts +18 -1
  80. package/dist/constants/links.js +26 -1
  81. package/dist/contract/snapshot.d.ts +30 -0
  82. package/dist/contract/snapshot.js +114 -0
  83. package/dist/daemon/client.d.ts +2 -2
  84. package/dist/daemon/client.js +108 -54
  85. package/dist/daemon/index.d.ts +5 -3
  86. package/dist/daemon/index.js +28 -5
  87. package/dist/daemon/lifecycle.d.ts +7 -2
  88. package/dist/daemon/lifecycle.js +53 -13
  89. package/dist/daemon/pid.d.ts +1 -1
  90. package/dist/daemon/pid.js +6 -3
  91. package/dist/dinox.js +9 -1
  92. package/dist/powersync/exclusiveRuntime.d.ts +1 -0
  93. package/dist/powersync/exclusiveRuntime.js +26 -8
  94. package/dist/powersync/runtime.d.ts +3 -2
  95. package/dist/powersync/runtime.js +19 -3
  96. package/dist/powersync/schema/content.d.ts +1 -78
  97. package/dist/powersync/schema/content.js +1 -78
  98. package/dist/powersync/schema/electronCanonical.d.ts +1561 -0
  99. package/dist/powersync/schema/electronCanonical.js +1154 -0
  100. package/dist/powersync/schema/index.d.ts +573 -129
  101. package/dist/powersync/schema/index.js +3 -32
  102. package/dist/powersync/schema/local.d.ts +1 -56
  103. package/dist/powersync/schema/local.js +1 -60
  104. package/dist/powersync/schema/note.d.ts +1 -35
  105. package/dist/powersync/schema/note.js +1 -35
  106. package/dist/powersync/schema/notesExtras.d.ts +1 -62
  107. package/dist/powersync/schema/notesExtras.js +1 -71
  108. package/dist/powersync/schema/projects.d.ts +1 -104
  109. package/dist/powersync/schema/projects.js +1 -104
  110. package/dist/powersync/schema/savedViews.d.ts +1 -0
  111. package/dist/powersync/schema/savedViews.js +1 -0
  112. package/dist/powersync/schema/storage.d.ts +1 -24
  113. package/dist/powersync/schema/storage.js +1 -24
  114. package/dist/powersync/schema/tags.d.ts +1 -37
  115. package/dist/powersync/schema/tags.js +1 -37
  116. package/dist/powersync/syncGate.d.ts +20 -2
  117. package/dist/powersync/uploader.d.ts +16 -2
  118. package/dist/powersync/uploader.js +152 -158
  119. package/dist/skills/check.d.ts +1 -0
  120. package/dist/skills/check.js +538 -70
  121. package/dist/skills/commandRefs.d.ts +21 -0
  122. package/dist/skills/commandRefs.js +132 -0
  123. package/dist/skills/generate.d.ts +13 -0
  124. package/dist/skills/generate.js +80 -0
  125. package/dist/skills/registry.d.ts +13 -0
  126. package/dist/skills/registry.js +58 -0
  127. package/dist/skills/retired.d.ts +60 -0
  128. package/dist/skills/retired.js +58 -0
  129. package/dist/utils/deadline.d.ts +8 -0
  130. package/dist/utils/deadline.js +31 -0
  131. package/dist/utils/dryRun.d.ts +20 -1
  132. package/dist/utils/dryRun.js +22 -3
  133. package/dist/utils/errorCodes.d.ts +311 -0
  134. package/dist/utils/errorCodes.js +92 -0
  135. package/dist/utils/errors.d.ts +15 -2
  136. package/dist/utils/errors.js +13 -0
  137. package/dist/utils/processExit.d.ts +1 -0
  138. package/dist/utils/processExit.js +33 -0
  139. package/dist/utils/updateNotice.d.ts +1 -0
  140. package/dist/utils/updateNotice.js +1 -1
  141. package/package.json +18 -8
@@ -0,0 +1,738 @@
1
+ import { DinoxError } from '../../utils/errors.js';
2
+ import { SAVED_VIEW_SYSTEM_FIELDS, } from './model.js';
3
+ const PROPERTY_DEF_CONFIG_KEY_PREFIX = 'property_def.config.v1.';
4
+ const PROPERTY_OPTION_COLORS = new Set([
5
+ 'gray',
6
+ 'red',
7
+ 'orange',
8
+ 'yellow',
9
+ 'green',
10
+ 'blue',
11
+ 'purple',
12
+ 'pink',
13
+ ]);
14
+ const PROPERTY_TYPES = new Set([
15
+ 'text',
16
+ 'number',
17
+ 'date',
18
+ 'select',
19
+ 'multi_select',
20
+ 'url',
21
+ 'checkbox',
22
+ 'relation',
23
+ ]);
24
+ const FALLBACK_ISO_4217_CODES = new Set('AED AFN ALL AMD ANG AOA ARS AUD AWG AZN BAM BBD BDT BGN BHD BIF BMD BND BOB BRL BSD BTN BWP BYN BZD CAD CDF CHF CLP CNY COP CRC CUC CUP CVE CZK DJF DKK DOP DZD EGP ERN ETB EUR FJD FKP GBP GEL GHS GIP GMD GNF GTQ GYD HKD HNL HRK HTG HUF IDR ILS INR IQD IRR ISK JMD JOD JPY KES KGS KHR KMF KPW KRW KWD KYD KZT LAK LBP LKR LRD LSL LYD MAD MDL MGA MKD MMK MNT MOP MRU MUR MVR MWK MXN MYR MZN NAD NGN NIO NOK NPR NZD OMR PAB PEN PGK PHP PKR PLN PYG QAR RON RSD RUB RWF SAR SBD SCR SDG SEK SGD SHP SLE SLL SOS SRD SSP STN SVC SYP SZL THB TJS TMT TND TOP TRY TTD TWD TZS UAH UGX USD UYU UZS VES VND VUV WST XAF XCD XCG XDR XOF XPF XSU YER ZAR ZMW ZWG ZWL'.split(' '));
25
+ const ISO_4217_CODES = (() => {
26
+ const supportedValuesOf = Intl.supportedValuesOf;
27
+ if (typeof supportedValuesOf === 'function') {
28
+ try {
29
+ return new Set([...FALLBACK_ISO_4217_CODES, ...supportedValuesOf.call(Intl, 'currency')]);
30
+ }
31
+ catch {
32
+ // Use the embedded list on runtimes without currency enumeration.
33
+ }
34
+ }
35
+ return FALLBACK_ISO_4217_CODES;
36
+ })();
37
+ const SYSTEM_FIELD_SQL = {
38
+ 'sys.title': 'n.title',
39
+ 'sys.created_at': 'n.created_at',
40
+ 'sys.updated_at': 'n.updated_at',
41
+ 'sys.tags': 'n.tags',
42
+ 'sys.zettel_boxes': 'n.zettel_boxes',
43
+ 'sys.source': 'n.source',
44
+ 'sys.type': 'n.type',
45
+ 'sys.starred_at': 'n.starred_at',
46
+ };
47
+ const TEXT_FILTER_OPERATORS = new Set([
48
+ 'equals',
49
+ 'notEquals',
50
+ 'contains',
51
+ 'notContains',
52
+ 'isEmpty',
53
+ 'isNotEmpty',
54
+ ]);
55
+ const LIST_FILTER_OPERATORS = new Set([
56
+ 'contains',
57
+ 'notContains',
58
+ 'isEmpty',
59
+ 'isNotEmpty',
60
+ ]);
61
+ const SELECT_FILTER_OPERATORS = new Set([
62
+ 'equals',
63
+ 'notEquals',
64
+ 'isEmpty',
65
+ 'isNotEmpty',
66
+ ]);
67
+ const ORDERABLE_FILTER_OPERATORS = new Set([
68
+ 'equals',
69
+ 'notEquals',
70
+ 'greaterThan',
71
+ 'greaterThanOrEqual',
72
+ 'lessThan',
73
+ 'lessThanOrEqual',
74
+ 'isEmpty',
75
+ 'isNotEmpty',
76
+ ]);
77
+ const BOOLEAN_FILTER_OPERATORS = new Set([
78
+ 'equals',
79
+ 'notEquals',
80
+ 'isEmpty',
81
+ 'isNotEmpty',
82
+ ]);
83
+ export const SYSTEM_FIELD_SEMANTICS = {
84
+ 'sys.title': { type: 'text' },
85
+ 'sys.created_at': { type: 'date', dateMode: 'datetime' },
86
+ 'sys.updated_at': { type: 'date', dateMode: 'datetime' },
87
+ 'sys.tags': { type: 'list', propertyType: 'multi_select' },
88
+ 'sys.zettel_boxes': { type: 'list', propertyType: 'relation' },
89
+ 'sys.source': { type: 'text' },
90
+ 'sys.type': { type: 'text' },
91
+ 'sys.starred_at': { type: 'date', dateMode: 'datetime' },
92
+ };
93
+ const SYSTEM_FIELD_PROPERTY_TYPES = {
94
+ 'sys.title': 'text',
95
+ 'sys.created_at': 'date',
96
+ 'sys.updated_at': 'date',
97
+ 'sys.tags': 'multi_select',
98
+ 'sys.zettel_boxes': 'relation',
99
+ 'sys.source': 'text',
100
+ 'sys.type': 'text',
101
+ 'sys.starred_at': 'date',
102
+ };
103
+ const SYSTEM_FIELD_NAMES = {
104
+ 'sys.title': 'Title',
105
+ 'sys.created_at': 'Created at',
106
+ 'sys.updated_at': 'Updated at',
107
+ 'sys.tags': 'Tags',
108
+ 'sys.zettel_boxes': 'Zettel boxes',
109
+ 'sys.source': 'Source',
110
+ 'sys.type': 'Type',
111
+ 'sys.starred_at': 'Starred at',
112
+ };
113
+ function savedViewError(code, message, details) {
114
+ return new DinoxError(message, {
115
+ code,
116
+ details,
117
+ recoverable: false,
118
+ });
119
+ }
120
+ function isRecord(value) {
121
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
122
+ }
123
+ function isPropertyType(value) {
124
+ return typeof value === 'string' && PROPERTY_TYPES.has(value);
125
+ }
126
+ function normalizePropertyOptionLabel(value) {
127
+ if (typeof value !== 'string')
128
+ return null;
129
+ const normalized = value.normalize('NFKC').trim();
130
+ return normalized && normalized.length <= 120 ? normalized : null;
131
+ }
132
+ function parsePropertyOptions(raw) {
133
+ if (!raw?.trim()) {
134
+ throw savedViewError('SAVED_VIEW_PROPERTY_OPTIONS_MIGRATION_REQUIRED', 'Saved view select fields require migrated version 1 property options');
135
+ }
136
+ let parsed;
137
+ try {
138
+ parsed = JSON.parse(raw);
139
+ }
140
+ catch {
141
+ throw savedViewError('SAVED_VIEW_PROPERTY_OPTIONS_INVALID', 'Saved view property options are invalid');
142
+ }
143
+ if (Array.isArray(parsed)) {
144
+ throw savedViewError('SAVED_VIEW_PROPERTY_OPTIONS_MIGRATION_REQUIRED', 'Saved view select fields require migrated version 1 property options');
145
+ }
146
+ if (!isRecord(parsed) || parsed.version !== 1 || !Array.isArray(parsed.items) || parsed.items.length > 200) {
147
+ throw savedViewError('SAVED_VIEW_PROPERTY_OPTIONS_INVALID', 'Saved view property options are invalid');
148
+ }
149
+ const ids = new Set();
150
+ const labels = new Set();
151
+ const options = [];
152
+ for (const item of parsed.items) {
153
+ if (!isRecord(item)) {
154
+ throw savedViewError('SAVED_VIEW_PROPERTY_OPTIONS_INVALID', 'Saved view property options are invalid');
155
+ }
156
+ const id = typeof item.id === 'string' ? item.id : '';
157
+ const label = normalizePropertyOptionLabel(item.label);
158
+ const labelKey = label?.toLowerCase() ?? '';
159
+ if (!id.trim()
160
+ || id.trim() !== id
161
+ || !label
162
+ || !PROPERTY_OPTION_COLORS.has(String(item.color))
163
+ || !Number.isSafeInteger(item.sortRank)
164
+ || typeof item.archived !== 'boolean'
165
+ || ids.has(id)
166
+ || labels.has(labelKey)) {
167
+ throw savedViewError('SAVED_VIEW_PROPERTY_OPTIONS_INVALID', 'Saved view property options are invalid');
168
+ }
169
+ ids.add(id);
170
+ labels.add(labelKey);
171
+ options.push({
172
+ id,
173
+ label,
174
+ color: String(item.color),
175
+ sortRank: item.sortRank,
176
+ archived: item.archived,
177
+ });
178
+ }
179
+ return options.sort((left, right) => left.sortRank - right.sortRank || left.id.localeCompare(right.id));
180
+ }
181
+ function optionalFiniteNumber(value, key) {
182
+ if (!Object.hasOwn(value, key))
183
+ return undefined;
184
+ const candidate = value[key];
185
+ return typeof candidate === 'number' && Number.isFinite(candidate) ? candidate : null;
186
+ }
187
+ function optionalConfigText(value, key) {
188
+ if (!Object.hasOwn(value, key))
189
+ return undefined;
190
+ const candidate = value[key];
191
+ if (typeof candidate !== 'string')
192
+ return null;
193
+ const normalized = candidate.normalize('NFKC').trim();
194
+ return normalized && normalized.length <= 120 ? normalized : null;
195
+ }
196
+ function parsePropertyConfig(raw, type) {
197
+ if (!raw?.trim()) {
198
+ return type === 'date' ? { dateMode: 'date' } : {};
199
+ }
200
+ let value;
201
+ try {
202
+ value = JSON.parse(raw);
203
+ }
204
+ catch {
205
+ throw savedViewError('SAVED_VIEW_PROPERTY_CONFIG_INVALID', 'Saved view property config is invalid');
206
+ }
207
+ if (!isRecord(value) || value.version !== 1 || value.type !== type) {
208
+ throw savedViewError('SAVED_VIEW_PROPERTY_CONFIG_INVALID', 'Saved view property config is invalid');
209
+ }
210
+ switch (type) {
211
+ case 'date':
212
+ if (value.mode !== 'date' && value.mode !== 'datetime') {
213
+ throw savedViewError('SAVED_VIEW_PROPERTY_CONFIG_INVALID', 'Saved view property config is invalid');
214
+ }
215
+ return { dateMode: value.mode };
216
+ case 'number': {
217
+ if (value.format !== 'number' && value.format !== 'percent' && value.format !== 'currency') {
218
+ throw savedViewError('SAVED_VIEW_PROPERTY_CONFIG_INVALID', 'Saved view property config is invalid');
219
+ }
220
+ const precision = optionalFiniteNumber(value, 'precision');
221
+ const min = optionalFiniteNumber(value, 'min');
222
+ const max = optionalFiniteNumber(value, 'max');
223
+ const step = optionalFiniteNumber(value, 'step');
224
+ const currency = optionalConfigText(value, 'currency');
225
+ const unit = optionalConfigText(value, 'unit');
226
+ const normalizedCurrency = currency?.toUpperCase();
227
+ if (precision === null
228
+ || min === null
229
+ || max === null
230
+ || step === null
231
+ || currency === null
232
+ || unit === null
233
+ || (precision !== undefined && (!Number.isSafeInteger(precision) || precision < 0 || precision > 12))
234
+ || (step !== undefined && step <= 0)
235
+ || (min !== undefined && max !== undefined && min > max)
236
+ || (normalizedCurrency !== undefined && !ISO_4217_CODES.has(normalizedCurrency))
237
+ || (value.format === 'currency' && currency === undefined)) {
238
+ throw savedViewError('SAVED_VIEW_PROPERTY_CONFIG_INVALID', 'Saved view property config is invalid');
239
+ }
240
+ return {};
241
+ }
242
+ case 'select':
243
+ if (value.mode !== 'open' && value.mode !== 'closed') {
244
+ throw savedViewError('SAVED_VIEW_PROPERTY_CONFIG_INVALID', 'Saved view property config is invalid');
245
+ }
246
+ return {};
247
+ case 'multi_select': {
248
+ const maxSelections = optionalFiniteNumber(value, 'maxSelections');
249
+ if ((value.mode !== 'open' && value.mode !== 'closed')
250
+ || maxSelections === null
251
+ || (maxSelections !== undefined && (!Number.isSafeInteger(maxSelections) || maxSelections < 1 || maxSelections > 50))) {
252
+ throw savedViewError('SAVED_VIEW_PROPERTY_CONFIG_INVALID', 'Saved view property config is invalid');
253
+ }
254
+ return {};
255
+ }
256
+ case 'url':
257
+ if (Object.hasOwn(value, 'schemes')
258
+ && (!Array.isArray(value.schemes)
259
+ || value.schemes.length === 0
260
+ || value.schemes.some((scheme) => scheme !== 'http' && scheme !== 'https'))) {
261
+ throw savedViewError('SAVED_VIEW_PROPERTY_CONFIG_INVALID', 'Saved view property config is invalid');
262
+ }
263
+ return {};
264
+ default:
265
+ throw savedViewError('SAVED_VIEW_PROPERTY_CONFIG_INVALID', 'Saved view property config is invalid');
266
+ }
267
+ }
268
+ function propertyDefToSemantic(row, type) {
269
+ const config = parsePropertyConfig(row.config_json, type);
270
+ switch (type) {
271
+ case 'number':
272
+ return { type: 'number', propertyType: type };
273
+ case 'date':
274
+ return { type: 'date', propertyType: type, dateMode: config.dateMode ?? 'date' };
275
+ case 'select':
276
+ return { type: 'select', propertyType: type, options: parsePropertyOptions(row.options) };
277
+ case 'checkbox':
278
+ return { type: 'boolean', propertyType: type };
279
+ case 'multi_select':
280
+ return { type: 'list', propertyType: type, options: parsePropertyOptions(row.options) };
281
+ case 'relation':
282
+ return { type: 'list', propertyType: type };
283
+ default:
284
+ return { type: 'text', propertyType: type };
285
+ }
286
+ }
287
+ function fallbackPropertySemantic(type) {
288
+ switch (type) {
289
+ case 'number': return { type: 'number', propertyType: type };
290
+ case 'date': return { type: 'date', propertyType: type, dateMode: 'date' };
291
+ case 'select': return { type: 'select', propertyType: type };
292
+ case 'checkbox': return { type: 'boolean', propertyType: type };
293
+ case 'multi_select':
294
+ case 'relation': return { type: 'list', propertyType: type };
295
+ default: return { type: 'text', propertyType: type };
296
+ }
297
+ }
298
+ function propertySemanticQuery(keys) {
299
+ const keyClause = keys && keys.length > 0
300
+ ? `AND key IN (${keys.map(() => '?').join(', ')})`
301
+ : '';
302
+ return {
303
+ sql: `
304
+ SELECT
305
+ ranked.id,
306
+ ranked.key,
307
+ ranked.name,
308
+ ranked.type,
309
+ ranked.options,
310
+ ranked.description,
311
+ ranked.icon,
312
+ ranked.sort_rank,
313
+ (
314
+ SELECT cp.c_value
315
+ FROM c_config_property AS cp
316
+ WHERE cp.user_id = ranked.user_id
317
+ AND cp.c_key = '${PROPERTY_DEF_CONFIG_KEY_PREFIX}' || ranked.id
318
+ AND (cp.is_del IS NULL OR cp.is_del = 0)
319
+ ORDER BY COALESCE(cp.updated_at, cp.created_at, cp.id) DESC, cp.id DESC
320
+ LIMIT 1
321
+ ) AS config_json
322
+ FROM (
323
+ SELECT
324
+ id,
325
+ user_id,
326
+ key,
327
+ name,
328
+ type,
329
+ options,
330
+ description,
331
+ icon,
332
+ sort_rank,
333
+ ROW_NUMBER() OVER (
334
+ PARTITION BY key
335
+ ORDER BY COALESCE(NULLIF(updated_at, ''), NULLIF(created_at, ''), id) DESC, id DESC
336
+ ) AS key_rank
337
+ FROM c_property_def
338
+ WHERE user_id = ?
339
+ ${keyClause}
340
+ AND (is_del IS NULL OR is_del = 0)
341
+ ) AS ranked
342
+ WHERE ranked.key_rank = 1
343
+ ORDER BY COALESCE(ranked.sort_rank, 0) ASC, ranked.key ASC, ranked.id ASC
344
+ `,
345
+ params: [...(keys ?? [])],
346
+ };
347
+ }
348
+ async function loadPropertySemanticRows(db, userId, keys) {
349
+ const query = propertySemanticQuery(keys);
350
+ return db.getAll(query.sql, [userId, ...query.params]);
351
+ }
352
+ function collectFilterConditions(node, conditions) {
353
+ if (!node)
354
+ return;
355
+ if (node.kind === 'condition') {
356
+ conditions.push(node);
357
+ return;
358
+ }
359
+ if (node.kind === 'not') {
360
+ collectFilterConditions(node.child, conditions);
361
+ return;
362
+ }
363
+ for (const child of node.children) {
364
+ collectFilterConditions(child, conditions);
365
+ }
366
+ }
367
+ function filterOperatorsForSemantic(semantic) {
368
+ if (semantic.type === 'text')
369
+ return TEXT_FILTER_OPERATORS;
370
+ if (semantic.type === 'select')
371
+ return SELECT_FILTER_OPERATORS;
372
+ if (semantic.type === 'list')
373
+ return LIST_FILTER_OPERATORS;
374
+ if (semantic.type === 'boolean')
375
+ return BOOLEAN_FILTER_OPERATORS;
376
+ return ORDERABLE_FILTER_OPERATORS;
377
+ }
378
+ function assertConditionSemantics(condition, semantic) {
379
+ if (!filterOperatorsForSemantic(semantic).has(condition.operator)) {
380
+ throw savedViewError('SAVED_VIEW_OPERATOR_TYPE_MISMATCH', `Operator ${condition.operator} is not supported for ${condition.field}`, { field: condition.field, operator: condition.operator, semanticType: semantic.type });
381
+ }
382
+ const value = condition.value;
383
+ if (value === undefined || value === null)
384
+ return;
385
+ if (semantic.type === 'number' && typeof value !== 'number') {
386
+ throw savedViewError('SAVED_VIEW_VALUE_TYPE_MISMATCH', `Filter value for ${condition.field} must be a number`);
387
+ }
388
+ if (semantic.type === 'boolean' && typeof value !== 'boolean') {
389
+ throw savedViewError('SAVED_VIEW_VALUE_TYPE_MISMATCH', `Filter value for ${condition.field} must be a boolean`);
390
+ }
391
+ if ((semantic.type === 'text' || semantic.type === 'select' || semantic.type === 'list' || semantic.type === 'date')
392
+ && typeof value !== 'string') {
393
+ throw savedViewError('SAVED_VIEW_VALUE_TYPE_MISMATCH', `Filter value for ${condition.field} must be a string`);
394
+ }
395
+ if (semantic.type === 'date'
396
+ && typeof value === 'string'
397
+ && normalizeDatePropertyValue(value, semantic.dateMode ?? 'date') === null) {
398
+ throw savedViewError('SAVED_VIEW_INVALID_DATE_VALUE', `Filter value for ${condition.field} is not a valid date`);
399
+ }
400
+ if ((semantic.propertyType === 'select' || semantic.propertyType === 'multi_select')
401
+ && typeof value === 'string'
402
+ && !(semantic.options ?? []).some((option) => option.id === value)) {
403
+ throw savedViewError('SAVED_VIEW_OPTION_NOT_FOUND', `Filter option ${value} does not exist for ${condition.field}`, { field: condition.field, optionId: value });
404
+ }
405
+ }
406
+ export async function validateSavedViewSemantics(db, userId, filter, config) {
407
+ const conditions = [];
408
+ collectFilterConditions(filter?.root ?? null, conditions);
409
+ const fields = new Set();
410
+ for (const condition of conditions)
411
+ fields.add(condition.field);
412
+ for (const column of config?.columns ?? [])
413
+ fields.add(column.field);
414
+ for (const sort of config?.sort ?? [])
415
+ fields.add(sort.field);
416
+ const propertyKeys = [...new Set([...fields]
417
+ .filter((field) => field.startsWith('prop.'))
418
+ .map((field) => field.slice('prop.'.length)))];
419
+ const propertySemantics = new Map();
420
+ if (propertyKeys.length > 0) {
421
+ const rows = await loadPropertySemanticRows(db, userId, propertyKeys);
422
+ for (const row of rows) {
423
+ if (row.key && isPropertyType(row.type)) {
424
+ propertySemantics.set(row.key, propertyDefToSemantic(row, row.type));
425
+ }
426
+ }
427
+ for (const key of propertyKeys) {
428
+ if (!propertySemantics.has(key)) {
429
+ throw savedViewError('SAVED_VIEW_PROPERTY_NOT_FOUND', `Saved view property definition not found: ${key}`, { propertyKey: key });
430
+ }
431
+ }
432
+ }
433
+ const semantics = new Map();
434
+ for (const field of fields) {
435
+ const semantic = field.startsWith('prop.')
436
+ ? propertySemantics.get(field.slice('prop.'.length))
437
+ : SYSTEM_FIELD_SEMANTICS[field];
438
+ if (!semantic) {
439
+ throw savedViewError('SAVED_VIEW_INVALID_FIELD', `Saved view field is invalid: ${field}`, { field });
440
+ }
441
+ semantics.set(field, semantic);
442
+ }
443
+ for (const condition of conditions) {
444
+ assertConditionSemantics(condition, semantics.get(condition.field));
445
+ }
446
+ for (const sort of config?.sort ?? []) {
447
+ if (semantics.get(sort.field)?.type === 'list') {
448
+ throw savedViewError('SAVED_VIEW_SORT_TYPE_UNSUPPORTED', `Saved view field cannot be sorted: ${sort.field}`, { field: sort.field });
449
+ }
450
+ }
451
+ return semantics;
452
+ }
453
+ const ISO_DATE_PREFIX_PATTERN = /^([+-]?\d{4,6})-(\d{2})-(\d{2})(?:$|[Tt\s])/;
454
+ function hasValidCalendarDatePrefix(value) {
455
+ const match = ISO_DATE_PREFIX_PATTERN.exec(value);
456
+ if (!match)
457
+ return true;
458
+ const year = Number(match[1]);
459
+ const month = Number(match[2]);
460
+ const day = Number(match[3]);
461
+ if (!Number.isInteger(year) || month < 1 || month > 12 || day < 1)
462
+ return false;
463
+ const daysInMonth = [
464
+ 31,
465
+ year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28,
466
+ 31,
467
+ 30,
468
+ 31,
469
+ 30,
470
+ 31,
471
+ 31,
472
+ 30,
473
+ 31,
474
+ 30,
475
+ 31,
476
+ ];
477
+ return day <= (daysInMonth[month - 1] ?? 0);
478
+ }
479
+ function normalizeDateValueToIso(value) {
480
+ if (typeof value === 'string') {
481
+ const trimmed = value.trim();
482
+ if (!trimmed || !hasValidCalendarDatePrefix(trimmed))
483
+ return null;
484
+ const date = new Date(trimmed);
485
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
486
+ }
487
+ if (typeof value === 'number' && Number.isFinite(value)) {
488
+ const date = new Date(value);
489
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
490
+ }
491
+ return null;
492
+ }
493
+ export function normalizeDatePropertyValue(value, mode) {
494
+ if (mode === 'datetime')
495
+ return normalizeDateValueToIso(value);
496
+ if (typeof value === 'string') {
497
+ const trimmed = value.trim();
498
+ if (/^(\d{4})-(\d{2})-(\d{2})$/.test(trimmed)) {
499
+ return hasValidCalendarDatePrefix(trimmed) ? trimmed : null;
500
+ }
501
+ }
502
+ return normalizeDateValueToIso(value)?.slice(0, 10) ?? null;
503
+ }
504
+ export function normalizeSavedViewDateValues(filter, semantics) {
505
+ const normalizeNode = (node) => {
506
+ if (node.kind === 'condition') {
507
+ const semantic = semantics.get(node.field);
508
+ if (semantic?.type === 'date' && typeof node.value === 'string') {
509
+ const normalized = normalizeDatePropertyValue(node.value, semantic.dateMode ?? 'date');
510
+ if (!normalized) {
511
+ throw savedViewError('SAVED_VIEW_INVALID_DATE_VALUE', `Filter value for ${node.field} is not a valid date`);
512
+ }
513
+ return { ...node, value: normalized };
514
+ }
515
+ return node;
516
+ }
517
+ if (node.kind === 'not') {
518
+ return { ...node, child: normalizeNode(node.child) };
519
+ }
520
+ return { ...node, children: node.children.map(normalizeNode) };
521
+ };
522
+ return {
523
+ ...filter,
524
+ root: filter.root ? normalizeNode(filter.root) : null,
525
+ };
526
+ }
527
+ function propertyJsonPath(field) {
528
+ const key = field.slice('prop.'.length);
529
+ const escaped = key.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
530
+ return `$."${escaped}"`;
531
+ }
532
+ export function compileSavedViewField(field) {
533
+ const systemSql = SYSTEM_FIELD_SQL[field];
534
+ if (systemSql)
535
+ return { sql: systemSql, params: [] };
536
+ if (!field.startsWith('prop.') || field.length <= 'prop.'.length) {
537
+ throw savedViewError('SAVED_VIEW_INVALID_FIELD', `Saved view field is invalid: ${field}`);
538
+ }
539
+ return {
540
+ sql: "json_extract(CASE WHEN json_valid(n.properties) THEN n.properties ELSE '{}' END, ?)",
541
+ params: [propertyJsonPath(field)],
542
+ };
543
+ }
544
+ function requireConditionValue(condition) {
545
+ if (condition.value === undefined || condition.value === null) {
546
+ throw savedViewError('SAVED_VIEW_FILTER_VALUE_REQUIRED', `Operator ${condition.operator} requires a value`);
547
+ }
548
+ return condition.value;
549
+ }
550
+ function normalizeSqlParameter(value) {
551
+ return typeof value === 'boolean' ? (value ? 1 : 0) : value;
552
+ }
553
+ function compileEmptyCheck(field, empty) {
554
+ const emptySql = `(${field.sql} IS NULL OR trim(CAST(${field.sql} AS TEXT)) = '' OR CAST(${field.sql} AS TEXT) = '[]')`;
555
+ return {
556
+ sql: empty ? emptySql : `(NOT ${emptySql})`,
557
+ params: [...field.params, ...field.params, ...field.params],
558
+ };
559
+ }
560
+ function comparison(field, operator, value, semantic, nullResult = 0) {
561
+ const isDate = semantic?.type === 'date';
562
+ const dateFunction = semantic?.dateMode === 'datetime' || (operator !== '=' && operator !== '!=')
563
+ ? 'julianday'
564
+ : 'date';
565
+ const left = isDate
566
+ ? `${dateFunction}(${field.sql})`
567
+ : semantic?.type === 'number'
568
+ ? `CAST(${field.sql} AS REAL)`
569
+ : field.sql;
570
+ const right = isDate
571
+ ? `${dateFunction}(?)`
572
+ : semantic?.type === 'number'
573
+ ? 'CAST(? AS REAL)'
574
+ : '?';
575
+ return {
576
+ sql: `COALESCE((${left} ${operator} ${right}), ${nullResult})`,
577
+ params: [...field.params, normalizeSqlParameter(value)],
578
+ };
579
+ }
580
+ function compileContains(field, value, negated, semantic) {
581
+ const normalizedValue = normalizeSqlParameter(value);
582
+ if (semantic?.type !== 'list') {
583
+ const predicate = `COALESCE(
584
+ instr(lower(CAST(${field.sql} AS TEXT)), lower(CAST(? AS TEXT))) > 0,
585
+ 0
586
+ )`;
587
+ return {
588
+ sql: negated ? `(NOT (${predicate}))` : predicate,
589
+ params: [...field.params, normalizedValue],
590
+ };
591
+ }
592
+ const predicate = `EXISTS (
593
+ SELECT 1
594
+ FROM (SELECT ${field.sql} AS field_value) AS saved_view_field
595
+ WHERE json_valid(CAST(saved_view_field.field_value AS TEXT))
596
+ AND json_type(CAST(saved_view_field.field_value AS TEXT)) = 'array'
597
+ AND EXISTS (
598
+ SELECT 1
599
+ FROM json_each(CAST(saved_view_field.field_value AS TEXT)) AS saved_view_item
600
+ WHERE CAST(saved_view_item.value AS TEXT) = CAST(? AS TEXT)
601
+ )
602
+ )`;
603
+ return {
604
+ sql: negated ? `(NOT (${predicate}))` : predicate,
605
+ params: [...field.params, normalizedValue],
606
+ };
607
+ }
608
+ function compileCondition(condition, semantics) {
609
+ const field = compileSavedViewField(condition.field);
610
+ const value = condition.value;
611
+ const semantic = semantics?.get(condition.field) ?? SYSTEM_FIELD_SEMANTICS[condition.field];
612
+ switch (condition.operator) {
613
+ case 'equals':
614
+ return value === undefined || value === null
615
+ ? compileEmptyCheck(field, true)
616
+ : comparison(field, '=', value, semantic, 0);
617
+ case 'notEquals':
618
+ return value === undefined || value === null
619
+ ? compileEmptyCheck(field, false)
620
+ : comparison(field, '!=', value, semantic, 1);
621
+ case 'contains':
622
+ return compileContains(field, requireConditionValue(condition), false, semantic);
623
+ case 'notContains':
624
+ return compileContains(field, requireConditionValue(condition), true, semantic);
625
+ case 'greaterThan':
626
+ return comparison(field, '>', requireConditionValue(condition), semantic);
627
+ case 'greaterThanOrEqual':
628
+ return comparison(field, '>=', requireConditionValue(condition), semantic);
629
+ case 'lessThan':
630
+ return comparison(field, '<', requireConditionValue(condition), semantic);
631
+ case 'lessThanOrEqual':
632
+ return comparison(field, '<=', requireConditionValue(condition), semantic);
633
+ case 'isEmpty':
634
+ return compileEmptyCheck(field, true);
635
+ case 'isNotEmpty':
636
+ return compileEmptyCheck(field, false);
637
+ }
638
+ }
639
+ function compileFilterNode(node, budget, depth, semantics) {
640
+ budget.nodes += 1;
641
+ if (depth > 8 || budget.nodes > 100) {
642
+ throw savedViewError('SAVED_VIEW_FILTER_TOO_COMPLEX', 'Saved view filter is too complex');
643
+ }
644
+ if (node.kind === 'condition')
645
+ return compileCondition(node, semantics);
646
+ if (node.kind === 'not') {
647
+ const child = compileFilterNode(node.child, budget, depth + 1, semantics);
648
+ return { sql: `(NOT (${child.sql}))`, params: child.params };
649
+ }
650
+ const children = node.children.map((child) => compileFilterNode(child, budget, depth + 1, semantics));
651
+ if (children.length === 0)
652
+ return { sql: node.operator === 'and' ? '1' : '0', params: [] };
653
+ return {
654
+ sql: `(${children.map((child) => `(${child.sql})`).join(node.operator === 'and' ? ' AND ' : ' OR ')})`,
655
+ params: children.flatMap((child) => child.params),
656
+ };
657
+ }
658
+ export function compileSavedViewFilter(filter, semantics) {
659
+ return filter.root
660
+ ? compileFilterNode(filter.root, { nodes: 0 }, 1, semantics)
661
+ : { sql: '1', params: [] };
662
+ }
663
+ export function compileSavedViewOrder(config, semantics) {
664
+ const clauses = [];
665
+ const params = [];
666
+ for (const sort of config.sort ?? []) {
667
+ const field = compileSavedViewField(sort.field);
668
+ const direction = sort.direction === 'asc' ? 'ASC' : 'DESC';
669
+ const semantic = semantics?.get(sort.field) ?? SYSTEM_FIELD_SEMANTICS[sort.field];
670
+ if (semantic?.type === 'list') {
671
+ throw savedViewError('SAVED_VIEW_SORT_TYPE_UNSUPPORTED', `Saved view field cannot be sorted: ${sort.field}`);
672
+ }
673
+ clauses.push(`CASE WHEN ${field.sql} IS NULL OR trim(CAST(${field.sql} AS TEXT)) = '' THEN 1 ELSE 0 END ASC`);
674
+ params.push(...field.params, ...field.params);
675
+ if (semantic?.type === 'select' && (semantic.options?.length ?? 0) > 0) {
676
+ const options = [...(semantic.options ?? [])].sort((left, right) => left.sortRank - right.sortRank || left.id.localeCompare(right.id));
677
+ clauses.push(`CASE WHEN ${field.sql} IN (${options.map(() => '?').join(', ')}) THEN 0 ELSE 1 END ASC`, `CASE ${field.sql} ${options.map(() => 'WHEN ? THEN ?').join(' ')} ELSE NULL END ${direction}`, `${field.sql} COLLATE NOCASE ${direction}`);
678
+ params.push(...field.params, ...options.map((option) => option.id), ...field.params, ...options.flatMap((option) => [option.id, option.sortRank]), ...field.params);
679
+ continue;
680
+ }
681
+ const expression = semantic?.type === 'date'
682
+ ? `julianday(${field.sql})`
683
+ : semantic?.type === 'number'
684
+ ? `CAST(${field.sql} AS REAL)`
685
+ : `${field.sql} COLLATE NOCASE`;
686
+ clauses.push(`${expression} ${direction}`);
687
+ params.push(...field.params);
688
+ }
689
+ if (clauses.length === 0)
690
+ clauses.push('COALESCE(n.updated_at, n.created_at, n.id) DESC');
691
+ clauses.push('n.id DESC');
692
+ return { sql: clauses.join(', '), params };
693
+ }
694
+ function catalogItem(field, name, propertyType, semantic, extra = {}) {
695
+ return {
696
+ field,
697
+ name,
698
+ type: semantic.type,
699
+ propertyType,
700
+ sortable: semantic.type !== 'list',
701
+ filterOperators: [...filterOperatorsForSemantic(semantic)],
702
+ available: true,
703
+ ...(semantic.options ? { options: semantic.options } : {}),
704
+ ...extra,
705
+ };
706
+ }
707
+ export async function listSavedViewFields(db, userId) {
708
+ const systemItems = SAVED_VIEW_SYSTEM_FIELDS.map((name) => {
709
+ const field = `sys.${name}`;
710
+ return catalogItem(field, SYSTEM_FIELD_NAMES[field] ?? name, SYSTEM_FIELD_PROPERTY_TYPES[field] ?? 'text', SYSTEM_FIELD_SEMANTICS[field]);
711
+ });
712
+ const rows = await loadPropertySemanticRows(db, userId);
713
+ const propertyItems = [];
714
+ for (const row of rows) {
715
+ if (!row.id?.trim() || !row.key?.trim() || !isPropertyType(row.type))
716
+ continue;
717
+ const field = `prop.${row.key}`;
718
+ const extra = {
719
+ propertyDefinitionId: row.id,
720
+ description: row.description,
721
+ icon: row.icon,
722
+ };
723
+ try {
724
+ propertyItems.push(catalogItem(field, row.name?.trim() || row.key, row.type, propertyDefToSemantic(row, row.type), extra));
725
+ }
726
+ catch (error) {
727
+ propertyItems.push({
728
+ ...catalogItem(field, row.name?.trim() || row.key, row.type, fallbackPropertySemantic(row.type), extra),
729
+ available: false,
730
+ error: {
731
+ code: error instanceof DinoxError ? error.code : 'SAVED_VIEW_PROPERTY_INVALID',
732
+ message: error instanceof Error ? error.message : String(error),
733
+ },
734
+ });
735
+ }
736
+ }
737
+ return [...systemItems, ...propertyItems];
738
+ }