@dinoxx/dinox-cli 2.2.0 → 2.4.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 (102) hide show
  1. package/README.md +17 -3
  2. package/dist/cli.js +6 -0
  3. package/dist/commands/domain/options.d.ts +10 -0
  4. package/dist/commands/domain/options.js +75 -0
  5. package/dist/commands/domain/run.d.ts +24 -0
  6. package/dist/commands/domain/run.js +137 -0
  7. package/dist/commands/gateway/index.d.ts +4 -0
  8. package/dist/commands/gateway/index.js +300 -0
  9. package/dist/commands/notes/actions.js +47 -18
  10. package/dist/commands/notes/createFromTemplate.d.ts +35 -0
  11. package/dist/commands/notes/createFromTemplate.js +58 -0
  12. package/dist/commands/notes/descriptors.js +9 -4
  13. package/dist/commands/notes/repo/read.js +4 -23
  14. package/dist/commands/notes/repo/write.d.ts +2 -0
  15. package/dist/commands/notes/repo/write.js +1 -1
  16. package/dist/commands/properties/index.d.ts +16 -0
  17. package/dist/commands/properties/index.js +640 -0
  18. package/dist/commands/properties/repo.d.ts +150 -0
  19. package/dist/commands/properties/repo.js +272 -0
  20. package/dist/commands/savedViews/fields.d.ts +28 -0
  21. package/dist/commands/savedViews/fields.js +87 -0
  22. package/dist/commands/savedViews/index.js +203 -164
  23. package/dist/commands/savedViews/model.d.ts +27 -69
  24. package/dist/commands/savedViews/model.js +63 -279
  25. package/dist/commands/savedViews/repo.d.ts +64 -62
  26. package/dist/commands/savedViews/repo.js +309 -428
  27. package/dist/commands/schema/renderSkills.js +33 -5
  28. package/dist/commands/schema/spec.js +6 -0
  29. package/dist/commands/templates/index.d.ts +4 -0
  30. package/dist/commands/templates/index.js +290 -0
  31. package/dist/commands/templates/model.d.ts +12 -34
  32. package/dist/commands/templates/model.js +39 -83
  33. package/dist/commands/templates/repo.d.ts +53 -0
  34. package/dist/commands/templates/repo.js +78 -0
  35. package/dist/config/paths.d.ts +1 -0
  36. package/dist/config/paths.js +1 -1
  37. package/dist/daemon/server.d.ts +1 -0
  38. package/dist/daemon/server.js +31 -0
  39. package/dist/gateway/adapters/acp.d.ts +42 -0
  40. package/dist/gateway/adapters/acp.js +285 -0
  41. package/dist/gateway/adapters/echo.d.ts +12 -0
  42. package/dist/gateway/adapters/echo.js +51 -0
  43. package/dist/gateway/adapters/index.d.ts +9 -0
  44. package/dist/gateway/adapters/index.js +30 -0
  45. package/dist/gateway/assistantProjection.d.ts +32 -0
  46. package/dist/gateway/assistantProjection.js +193 -0
  47. package/dist/gateway/config.d.ts +50 -0
  48. package/dist/gateway/config.js +253 -0
  49. package/dist/gateway/detect.d.ts +40 -0
  50. package/dist/gateway/detect.js +145 -0
  51. package/dist/gateway/deviceHost.d.ts +82 -0
  52. package/dist/gateway/deviceHost.js +530 -0
  53. package/dist/gateway/mcpServer.d.ts +4 -0
  54. package/dist/gateway/mcpServer.js +118 -0
  55. package/dist/gateway/protocol.d.ts +29 -0
  56. package/dist/gateway/protocol.js +127 -0
  57. package/dist/gateway/runExecutor.d.ts +67 -0
  58. package/dist/gateway/runExecutor.js +316 -0
  59. package/dist/gateway/service.d.ts +26 -0
  60. package/dist/gateway/service.js +82 -0
  61. package/dist/gateway/streamPublisher.d.ts +33 -0
  62. package/dist/gateway/streamPublisher.js +93 -0
  63. package/dist/gateway/threadStore.d.ts +157 -0
  64. package/dist/gateway/threadStore.js +430 -0
  65. package/dist/gateway/types.d.ts +117 -0
  66. package/dist/gateway/types.js +9 -0
  67. package/dist/skills/registry.js +7 -0
  68. package/dist/utils/errorCodes.d.ts +212 -0
  69. package/dist/utils/errorCodes.js +53 -0
  70. package/dist/vendor/electron/domain/note-templates.d.ts +3 -0
  71. package/dist/vendor/electron/domain/note-templates.js +232 -0
  72. package/dist/vendor/electron/domain/properties.d.ts +10 -0
  73. package/dist/vendor/electron/domain/properties.js +1549 -0
  74. package/dist/vendor/electron/domain/saved-view-analytics.d.ts +45 -0
  75. package/dist/vendor/electron/domain/saved-view-analytics.js +365 -0
  76. package/dist/vendor/electron/domain/saved-view-relative-date.d.ts +21 -0
  77. package/dist/vendor/electron/domain/saved-view-relative-date.js +153 -0
  78. package/dist/vendor/electron/domain/saved-views.d.ts +61 -0
  79. package/dist/vendor/electron/domain/saved-views.js +1120 -0
  80. package/dist/vendor/electron/rpc.d.ts +22 -0
  81. package/dist/vendor/electron/rpc.js +27 -0
  82. package/dist/vendor/electron/shared/ai-errors.d.ts +11 -0
  83. package/dist/vendor/electron/shared/ai-errors.js +107 -0
  84. package/dist/vendor/electron/shared/external-types.d.ts +20 -0
  85. package/dist/vendor/electron/shared/external-types.js +1 -0
  86. package/dist/vendor/electron/shared/index.d.ts +9 -0
  87. package/dist/vendor/electron/shared/index.js +7 -0
  88. package/dist/vendor/electron/shared/note-properties.d.ts +328 -0
  89. package/dist/vendor/electron/shared/note-properties.js +1027 -0
  90. package/dist/vendor/electron/shared/note-templates.d.ts +82 -0
  91. package/dist/vendor/electron/shared/note-templates.js +84 -0
  92. package/dist/vendor/electron/shared/powersync-read-models.d.ts +1716 -0
  93. package/dist/vendor/electron/shared/powersync-read-models.js +1 -0
  94. package/dist/vendor/electron/shared/remote-device-agent.d.ts +373 -0
  95. package/dist/vendor/electron/shared/remote-device-agent.js +191 -0
  96. package/dist/vendor/electron/shared/saved-views.d.ts +394 -0
  97. package/dist/vendor/electron/shared/saved-views.js +981 -0
  98. package/dist/vendor/electron/sql-utils.d.ts +5 -0
  99. package/dist/vendor/electron/sql-utils.js +32 -0
  100. package/package.json +10 -4
  101. package/dist/commands/savedViews/semantics.d.ts +0 -46
  102. package/dist/commands/savedViews/semantics.js +0 -738
@@ -0,0 +1,1549 @@
1
+ // Generated by scripts/sync-electron-domain.ts from dinox-electron/packages/main/src/modules/powersync/typed-rpc/domain/properties.ts. Do not edit manually.
2
+ /**
3
+ * Inputs: typed PowerSync read-model and command payloads for property definitions and note values.
4
+ * Outputs: catalog rows plus transactional definition, migration, and note-property mutations.
5
+ */
6
+ import { ACTIVE_PROPERTY_TYPES, applyNotePropertySlotOperation, buildPropertyDefConfigKey, coercePropertyValue, DEFAULT_STATUS_OPTION_INPUTS, findPropertyOptionById, findPropertyOptionByLabel, getDefaultPropertyDefConfig, isIssuedUniqueId, isNumberPropertyValueAllowed, isPropertyKeyValid, isPropertyType, mergeNotePropertiesPatch, NOTE_PROPERTY_LIMITS, NUMBER_PROPERTY_PRESETS, normalizeDatePropertyValue, normalizeFilePropertyValue, normalizeNumberPropertyValue, normalizePlacePropertyValue, normalizePropertyDefConfig, normalizePropertyOptionLabel, normalizePropertyOptions, normalizeUniqueIdPrefix, normalizeUrlPropertyValue, parseNumberPropertyPreset, parsePropertyDefConfigEnvelope, parsePropertyDefOptionsEnvelope, parseStoredNoteProperties, parseUniqueIdPropertyConfig, propertyOptionLabelKey, PROPERTY_DEF_CONFIG_KEY_PREFIX, PROPERTY_OPTION_COLORS, PROPERTY_OPTION_LABEL_MAX_LENGTH, PROPERTY_OPTION_LIMITS, savedViewFilterSchema, serializeNumberPropertyPreset, serializePropertyDefConfig, serializePropertyDefOptions, serializeRawNoteProperties, serializeUniqueIdPropertyConfig, STATUS_PROPERTY_GROUPS, tryParseRawNoteProperties, } from "../shared/index.js";
7
+ import { uuidv7 } from "uuidv7";
8
+ import { z } from "zod";
9
+ import { defineCommand, defineReadModel, } from "../rpc.js";
10
+ import { normalizeLimit, normalizeOptionalString } from "../sql-utils.js";
11
+ const PROPERTY_DEF_TABLE = "c_property_def";
12
+ const CONFIG_PROPERTY_TABLE = "c_config_property";
13
+ const NOTE_TABLE = "c_note";
14
+ const SAVED_VIEW_TABLE = "c_saved_view";
15
+ const PROPERTY_DEF_CONFIG_KEY_SQL_PREFIX = PROPERTY_DEF_CONFIG_KEY_PREFIX.replaceAll("'", "''");
16
+ const NOTE_PROPERTY_KEY = z
17
+ .string()
18
+ .min(1)
19
+ .max(NOTE_PROPERTY_LIMITS.keyLength)
20
+ .refine(isPropertyKeyValid);
21
+ const NOTE_PROPERTY_TEXT = z
22
+ .string()
23
+ .max(NOTE_PROPERTY_LIMITS.textLength)
24
+ .refine((value) => value.trim().length > 0, "Property value cannot be blank");
25
+ const NOTE_PROPERTY_VALUE = z.union([
26
+ NOTE_PROPERTY_TEXT,
27
+ z.number().finite(),
28
+ z.boolean(),
29
+ z.array(NOTE_PROPERTY_TEXT).min(1).max(NOTE_PROPERTY_LIMITS.arrayLength),
30
+ z.custom((value) => {
31
+ const files = normalizeFilePropertyValue(value);
32
+ return files !== null && files.length > 0;
33
+ }, "Invalid file property value"),
34
+ z.custom((value) => normalizePlacePropertyValue(value) !== null, "Invalid place property value"),
35
+ ]);
36
+ const NOTE_PROPERTIES_PATCH = z
37
+ .preprocess((patch, context) => {
38
+ if (patch && typeof patch === "object" && !Array.isArray(patch)) {
39
+ const invalidKey = Object.keys(patch).find((key) => !isPropertyKeyValid(key));
40
+ if (invalidKey !== undefined) {
41
+ context.addIssue({ code: "custom", message: "Invalid property key" });
42
+ return z.NEVER;
43
+ }
44
+ }
45
+ return patch;
46
+ }, z.record(NOTE_PROPERTY_KEY, NOTE_PROPERTY_VALUE.nullable()))
47
+ .refine((patch) => Object.keys(patch).length > 0, "Property patch cannot be empty")
48
+ .refine((patch) => Object.keys(patch).length <= NOTE_PROPERTY_LIMITS.mutationKeys, "Property patch is too large");
49
+ const NOTE_PROPERTY_VALUES = z
50
+ .preprocess((values, context) => {
51
+ if (values && typeof values === "object" && !Array.isArray(values)) {
52
+ const invalidKey = Object.keys(values).find((key) => !isPropertyKeyValid(key));
53
+ if (invalidKey !== undefined) {
54
+ context.addIssue({ code: "custom", message: "Invalid property key" });
55
+ return z.NEVER;
56
+ }
57
+ }
58
+ return values;
59
+ }, z.record(NOTE_PROPERTY_KEY, NOTE_PROPERTY_VALUE))
60
+ .refine((values) => Object.keys(values).length > 0, "Property values cannot be empty")
61
+ .refine((values) => Object.keys(values).length <= NOTE_PROPERTY_LIMITS.mutationKeys, "Property values are too large");
62
+ const PROPERTY_OPTION_INPUT_SCHEMA = z.object({
63
+ id: z.string().trim().min(1).max(200).optional(),
64
+ label: z.string().min(1).max(PROPERTY_OPTION_LABEL_MAX_LENGTH),
65
+ color: z.enum(PROPERTY_OPTION_COLORS).optional(),
66
+ sortRank: z.number().int().safe().optional(),
67
+ archived: z.boolean().optional(),
68
+ group: z.enum(STATUS_PROPERTY_GROUPS).optional(),
69
+ });
70
+ const PROPERTY_DEF_CONFIG_INPUT_SCHEMA = z.custom((value) => normalizePropertyDefConfig(value) !== undefined);
71
+ const PROPERTY_DEF_CATALOG_INPUT = z.object({
72
+ userId: z.string().trim().min(1),
73
+ limit: z.number().int().positive().max(5000).optional(),
74
+ });
75
+ const PROPERTY_DEF_ENSURE_INPUT = z.object({
76
+ key: z.string().trim().min(1).max(64).refine(isPropertyKeyValid),
77
+ name: z.string().trim().max(120).nullable().optional(),
78
+ type: z.enum(ACTIVE_PROPERTY_TYPES).nullable().optional(),
79
+ optionItems: z.array(PROPERTY_OPTION_INPUT_SCHEMA).max(PROPERTY_OPTION_LIMITS.items).optional(),
80
+ uniqueIdPrefix: z.string().max(64).nullable().optional(),
81
+ numberPreset: z.enum(NUMBER_PROPERTY_PRESETS).nullable().optional(),
82
+ config: PROPERTY_DEF_CONFIG_INPUT_SCHEMA.optional(),
83
+ description: z.string().max(2000).nullable().optional(),
84
+ icon: z.string().max(120).nullable().optional(),
85
+ source: z.enum(["manual", "ai"]).nullable().optional(),
86
+ userId: z.string().trim().min(1),
87
+ updatedAt: z.string().trim().min(1).max(64),
88
+ });
89
+ const PROPERTY_DEF_CONFIGURE_INPUT = z
90
+ .object({
91
+ id: z.string().trim().min(1),
92
+ userId: z.string().trim().min(1),
93
+ name: z.string().trim().max(120).nullable().optional(),
94
+ description: z.string().max(2000).nullable().optional(),
95
+ icon: z.string().max(120).nullable().optional(),
96
+ showOnCard: z.boolean().nullable().optional(),
97
+ sortRank: z.number().int().nullable().optional(),
98
+ options: z.array(PROPERTY_OPTION_INPUT_SCHEMA).max(PROPERTY_OPTION_LIMITS.items).optional(),
99
+ uniqueIdPrefix: z.string().max(64).nullable().optional(),
100
+ repairInvalidOptions: z.boolean().optional(),
101
+ config: PROPERTY_DEF_CONFIG_INPUT_SCHEMA.nullable().optional(),
102
+ updatedAt: z.string().trim().min(1).max(64),
103
+ })
104
+ .refine((input) => input.name !== undefined ||
105
+ input.description !== undefined ||
106
+ input.icon !== undefined ||
107
+ input.showOnCard !== undefined ||
108
+ input.sortRank !== undefined ||
109
+ input.options !== undefined ||
110
+ input.uniqueIdPrefix !== undefined ||
111
+ input.config !== undefined, {
112
+ message: "Property configure input cannot be empty",
113
+ })
114
+ .refine((input) => !input.repairInvalidOptions || input.options !== undefined, {
115
+ message: "repairInvalidOptions requires replacement options",
116
+ });
117
+ const PROPERTY_DEF_MIGRATE_LEGACY_OPTIONS_INPUT = z.object({
118
+ userId: z.string().trim().min(1),
119
+ updatedAt: z.string().trim().min(1).max(64),
120
+ });
121
+ const PROPERTY_DEF_UPDATE_INPUT = z.object({
122
+ id: z.string().trim().min(1),
123
+ userId: z.string().trim().min(1),
124
+ name: z.string().trim().max(120).nullable().optional(),
125
+ description: z.string().max(2000).nullable().optional(),
126
+ icon: z.string().max(120).nullable().optional(),
127
+ showOnCard: z.boolean().nullable().optional(),
128
+ sortRank: z.number().int().nullable().optional(),
129
+ updatedAt: z.string().trim().min(1).max(64),
130
+ });
131
+ const PROPERTY_DEF_SOFT_DELETE_INPUT = z.object({
132
+ id: z.string().trim().min(1),
133
+ userId: z.string().trim().min(1),
134
+ updatedAt: z.string().trim().min(1).max(64),
135
+ });
136
+ const NOTE_PROPERTIES_UPDATE_INPUT = z.object({
137
+ noteId: z.string().trim().min(1),
138
+ userId: z.string().trim().min(1),
139
+ patch: NOTE_PROPERTIES_PATCH,
140
+ updatedAt: z.string().trim().min(1).max(64),
141
+ });
142
+ const NOTE_PROPERTY_SLOTS_UPDATE_INPUT = z.object({
143
+ noteId: z.string().trim().min(1),
144
+ userId: z.string().trim().min(1),
145
+ operation: z.enum(["declare", "clear", "remove"]),
146
+ keys: z.array(NOTE_PROPERTY_KEY).min(1).max(NOTE_PROPERTY_LIMITS.mutationKeys),
147
+ updatedAt: z.string().trim().min(1).max(64),
148
+ });
149
+ const NOTE_PROPERTY_SLOTS_REORDER_INPUT = z.object({
150
+ noteId: z.string().trim().min(1),
151
+ userId: z.string().trim().min(1),
152
+ orderedKeys: z
153
+ .array(NOTE_PROPERTY_KEY)
154
+ .min(2)
155
+ .max(NOTE_PROPERTY_LIMITS.mutationKeys)
156
+ .refine((keys) => new Set(keys).size === keys.length, "Property slot order contains duplicates"),
157
+ updatedAt: z.string().trim().min(1).max(64),
158
+ });
159
+ const NOTE_PROPERTIES_FILL_EMPTY_INPUT = z.object({
160
+ noteId: z.string().trim().min(1),
161
+ userId: z.string().trim().min(1),
162
+ values: NOTE_PROPERTY_VALUES,
163
+ updatedAt: z.string().trim().min(1).max(64),
164
+ });
165
+ export function createPropertyReadModels() {
166
+ return [
167
+ defineReadModel({
168
+ name: "properties.catalog",
169
+ inputSchema: PROPERTY_DEF_CATALOG_INPUT,
170
+ tables: () => [PROPERTY_DEF_TABLE, CONFIG_PROPERTY_TABLE],
171
+ fetch: fetchPropertyCatalog,
172
+ }),
173
+ ];
174
+ }
175
+ export function createPropertyCommands() {
176
+ return [
177
+ defineCommand({
178
+ name: "properties.ensure",
179
+ inputSchema: PROPERTY_DEF_ENSURE_INPUT,
180
+ run: async (db, input) => {
181
+ const result = await ensurePropertyDef(db, input);
182
+ return {
183
+ result: result.id,
184
+ affectedTables: affectedDefinitionTables(result),
185
+ };
186
+ },
187
+ }),
188
+ defineCommand({
189
+ name: "properties.configure",
190
+ inputSchema: PROPERTY_DEF_CONFIGURE_INPUT,
191
+ run: async (db, input) => {
192
+ const result = await configurePropertyDef(db, input);
193
+ return {
194
+ result: result.changed,
195
+ affectedTables: affectedDefinitionTables(result),
196
+ };
197
+ },
198
+ }),
199
+ defineCommand({
200
+ name: "properties.migrateLegacyOptions",
201
+ inputSchema: PROPERTY_DEF_MIGRATE_LEGACY_OPTIONS_INPUT,
202
+ run: async (db, input) => {
203
+ const result = await migrateLegacyPropertyOptions(db, input);
204
+ return {
205
+ result,
206
+ affectedTables: [
207
+ ...(result.definitionsMigrated > 0 ? [PROPERTY_DEF_TABLE] : []),
208
+ ...(result.notesUpdated > 0 ? [NOTE_TABLE] : []),
209
+ ...(result.savedViewsUpdated > 0 ? [SAVED_VIEW_TABLE] : []),
210
+ ],
211
+ };
212
+ },
213
+ }),
214
+ defineCommand({
215
+ name: "properties.update",
216
+ inputSchema: PROPERTY_DEF_UPDATE_INPUT,
217
+ run: async (db, input) => {
218
+ const changed = await updatePropertyDef(db, input);
219
+ return {
220
+ result: changed,
221
+ affectedTables: changed ? [PROPERTY_DEF_TABLE] : [],
222
+ };
223
+ },
224
+ }),
225
+ defineCommand({
226
+ name: "properties.softDelete",
227
+ inputSchema: PROPERTY_DEF_SOFT_DELETE_INPUT,
228
+ run: async (db, input) => {
229
+ const result = await softDeletePropertyDef(db, input);
230
+ return {
231
+ result: result.definitionChanged,
232
+ affectedTables: affectedDefinitionTables(result),
233
+ };
234
+ },
235
+ }),
236
+ defineCommand({
237
+ name: "notes.updateProperties",
238
+ inputSchema: NOTE_PROPERTIES_UPDATE_INPUT,
239
+ run: async (db, input) => {
240
+ const result = await updateNoteProperties(db, input);
241
+ return {
242
+ result: result.noteChanged,
243
+ affectedTables: [
244
+ ...(result.noteChanged ? [NOTE_TABLE] : []),
245
+ ...(result.definitionsChanged ? [PROPERTY_DEF_TABLE] : []),
246
+ ],
247
+ };
248
+ },
249
+ }),
250
+ defineCommand({
251
+ name: "notes.updatePropertySlots",
252
+ inputSchema: NOTE_PROPERTY_SLOTS_UPDATE_INPUT,
253
+ run: async (db, input) => {
254
+ const changed = await updateNotePropertySlots(db, input);
255
+ return { result: changed, affectedTables: changed ? [NOTE_TABLE] : [] };
256
+ },
257
+ }),
258
+ defineCommand({
259
+ name: "notes.reorderPropertySlots",
260
+ inputSchema: NOTE_PROPERTY_SLOTS_REORDER_INPUT,
261
+ run: async (db, input) => {
262
+ const changed = await reorderNotePropertySlots(db, input);
263
+ return { result: changed, affectedTables: changed ? [NOTE_TABLE] : [] };
264
+ },
265
+ }),
266
+ defineCommand({
267
+ name: "notes.fillEmptyProperties",
268
+ inputSchema: NOTE_PROPERTIES_FILL_EMPTY_INPUT,
269
+ run: async (db, input) => {
270
+ const mutation = await fillEmptyNotePropertyValues(db, input);
271
+ return {
272
+ result: mutation.result,
273
+ affectedTables: [
274
+ ...(mutation.result.appliedKeys.length > 0 ? [NOTE_TABLE] : []),
275
+ ...(mutation.definitionsChanged ? [PROPERTY_DEF_TABLE] : []),
276
+ ],
277
+ };
278
+ },
279
+ }),
280
+ ];
281
+ }
282
+ function affectedDefinitionTables(result) {
283
+ return [
284
+ ...(result.definitionChanged ? [PROPERTY_DEF_TABLE] : []),
285
+ ...(result.configChanged ? [CONFIG_PROPERTY_TABLE] : []),
286
+ ];
287
+ }
288
+ function isOptionPropertyType(type) {
289
+ return type === "select" || type === "multi_select" || type === "status";
290
+ }
291
+ function requireStoredPropertyType(value) {
292
+ if (!isPropertyType(value))
293
+ throw new Error("PROPERTY_DEF_TYPE_INVALID");
294
+ return value;
295
+ }
296
+ function configValueSubquery(alias) {
297
+ return `(
298
+ SELECT config.c_value
299
+ FROM c_config_property AS config
300
+ WHERE config.c_key = '${PROPERTY_DEF_CONFIG_KEY_SQL_PREFIX}' || ${alias}.id
301
+ AND config.user_id = ${alias}.user_id
302
+ AND (config.is_del IS NULL OR config.is_del = 0)
303
+ ORDER BY COALESCE(NULLIF(config.updated_at, ''), NULLIF(config.created_at, ''), config.id) DESC,
304
+ config.id DESC
305
+ LIMIT 1
306
+ )`;
307
+ }
308
+ function propertyDefSelectList(alias) {
309
+ return `
310
+ ${alias}.id AS id,
311
+ ${alias}.user_id AS user_id,
312
+ ${alias}.key AS key,
313
+ ${alias}.name AS name,
314
+ ${alias}.type AS type,
315
+ ${alias}.options AS options,
316
+ ${configValueSubquery(alias)} AS config_json,
317
+ ${alias}.description AS description,
318
+ ${alias}.icon AS icon,
319
+ ${alias}.sort_rank AS sort_rank,
320
+ ${alias}.show_on_card AS show_on_card,
321
+ ${alias}.source AS source,
322
+ ${alias}.created_at AS created_at,
323
+ ${alias}.updated_at AS updated_at,
324
+ ${alias}.is_del AS is_del
325
+ `;
326
+ }
327
+ async function loadLatestDefinitionByKey(executor, userId, key) {
328
+ return executor.getOptional(`
329
+ SELECT ${propertyDefSelectList("definition")}
330
+ FROM c_property_def AS definition
331
+ WHERE definition.user_id = ?
332
+ AND definition.key = ?
333
+ ORDER BY COALESCE(definition.is_del, 0) ASC,
334
+ COALESCE(NULLIF(definition.updated_at, ''), NULLIF(definition.created_at, ''), definition.id) DESC,
335
+ definition.id DESC
336
+ LIMIT 1
337
+ `, [userId, key]);
338
+ }
339
+ async function loadDefinitionById(executor, id, userId) {
340
+ return executor.getOptional(`
341
+ SELECT ${propertyDefSelectList("definition")}
342
+ FROM c_property_def AS definition
343
+ WHERE definition.id = ?
344
+ AND definition.user_id = ?
345
+ AND (definition.is_del IS NULL OR definition.is_del = 0)
346
+ LIMIT 1
347
+ `, [id, userId]);
348
+ }
349
+ async function findPropertyConfigRow(executor, definitionId, userId) {
350
+ return executor.getOptional(`
351
+ SELECT id, c_value
352
+ FROM c_config_property
353
+ WHERE c_key = ?
354
+ AND user_id = ?
355
+ AND (is_del IS NULL OR is_del = 0)
356
+ ORDER BY COALESCE(NULLIF(updated_at, ''), NULLIF(created_at, ''), id) DESC,
357
+ id DESC
358
+ LIMIT 1
359
+ `, [buildPropertyDefConfigKey(definitionId), userId]);
360
+ }
361
+ async function upsertPropertyConfig(executor, definitionId, userId, config, updatedAt) {
362
+ const serialized = serializePropertyDefConfig(config);
363
+ const existing = await findPropertyConfigRow(executor, definitionId, userId);
364
+ if (existing?.c_value === serialized)
365
+ return false;
366
+ if (existing) {
367
+ await executor.execute(`
368
+ UPDATE c_config_property
369
+ SET c_key = ?, c_value = ?, updated_at = ?, is_del = 0, user_id = ?
370
+ WHERE id = ?
371
+ `, [buildPropertyDefConfigKey(definitionId), serialized, updatedAt, userId, existing.id]);
372
+ return true;
373
+ }
374
+ await executor.execute(`
375
+ INSERT INTO c_config_property (
376
+ id, c_key, c_value, created_at, updated_at, is_del, user_id, description
377
+ ) VALUES (?, ?, ?, ?, ?, 0, ?, ?)
378
+ `, [
379
+ uuidv7(),
380
+ buildPropertyDefConfigKey(definitionId),
381
+ serialized,
382
+ updatedAt,
383
+ updatedAt,
384
+ userId,
385
+ "Property definition config",
386
+ ]);
387
+ return true;
388
+ }
389
+ async function clearPropertyConfig(executor, definitionId, userId, updatedAt) {
390
+ const key = buildPropertyDefConfigKey(definitionId);
391
+ const existing = await executor.getAll(`
392
+ SELECT id FROM c_config_property
393
+ WHERE c_key = ? AND user_id = ? AND (is_del IS NULL OR is_del = 0)
394
+ `, [key, userId]);
395
+ if (existing.length === 0)
396
+ return false;
397
+ await executor.execute(`
398
+ UPDATE c_config_property SET is_del = 1, updated_at = ?
399
+ WHERE c_key = ? AND user_id = ? AND (is_del IS NULL OR is_del = 0)
400
+ `, [updatedAt, key, userId]);
401
+ return true;
402
+ }
403
+ function nextOptionSortRank(options) {
404
+ const next = options.reduce((rank, option) => Math.max(rank, option.sortRank), -1) + 1;
405
+ if (!Number.isSafeInteger(next))
406
+ throw new Error("PROPERTY_OPTION_SORT_RANK_EXHAUSTED");
407
+ return next;
408
+ }
409
+ function createPropertyOption(label, sortRank, input) {
410
+ return {
411
+ id: uuidv7(),
412
+ label,
413
+ color: input?.color ?? "gray",
414
+ sortRank,
415
+ archived: input?.archived ?? false,
416
+ ...(input?.group ? { group: input.group } : {}),
417
+ };
418
+ }
419
+ function createOptionsForNewDefinition(items) {
420
+ const options = (items ?? []).map((item, index) => {
421
+ if (item.id !== undefined)
422
+ throw new Error("PROPERTY_OPTION_NEW_ID_FORBIDDEN");
423
+ const label = normalizePropertyOptionLabel(item.label);
424
+ if (!label)
425
+ throw new Error("PROPERTY_OPTION_LABEL_INVALID");
426
+ return createPropertyOption(label, item.sortRank ?? index, item);
427
+ });
428
+ const normalized = normalizePropertyOptions(options);
429
+ if (!normalized)
430
+ throw new Error("PROPERTY_OPTIONS_INVALID");
431
+ return normalized;
432
+ }
433
+ function replaceStableOptions(existing, inputs) {
434
+ const existingById = new Map(existing.map((option) => [option.id, option]));
435
+ const representedIds = new Set();
436
+ let nextRank = nextOptionSortRank(existing);
437
+ const next = inputs.map((input) => {
438
+ const label = normalizePropertyOptionLabel(input.label);
439
+ if (!label)
440
+ throw new Error("PROPERTY_OPTION_LABEL_INVALID");
441
+ if (input.id !== undefined) {
442
+ const current = existingById.get(input.id);
443
+ if (!current)
444
+ throw new Error("PROPERTY_OPTION_ID_UNKNOWN");
445
+ if (representedIds.has(input.id))
446
+ throw new Error("PROPERTY_OPTION_ID_DUPLICATE");
447
+ representedIds.add(input.id);
448
+ const group = input.group ?? current.group;
449
+ return {
450
+ id: current.id,
451
+ label,
452
+ color: input.color ?? current.color,
453
+ sortRank: input.sortRank ?? current.sortRank,
454
+ archived: input.archived ?? current.archived,
455
+ ...(group ? { group } : {}),
456
+ };
457
+ }
458
+ const option = createPropertyOption(label, input.sortRank ?? nextRank, input);
459
+ if (input.sortRank === undefined)
460
+ nextRank += 1;
461
+ return option;
462
+ });
463
+ for (const option of existing) {
464
+ if (!representedIds.has(option.id)) {
465
+ throw new Error("PROPERTY_OPTION_REMOVAL_REQUIRES_ARCHIVE");
466
+ }
467
+ }
468
+ const normalized = normalizePropertyOptions(next);
469
+ if (!normalized)
470
+ throw new Error("PROPERTY_OPTIONS_INVALID");
471
+ return normalized;
472
+ }
473
+ function stableOptionsFromRow(row) {
474
+ const parsed = parsePropertyDefOptionsEnvelope(row.options);
475
+ if (parsed.status === "valid")
476
+ return parsed.options;
477
+ if (parsed.status === "empty" || parsed.status === "legacy") {
478
+ throw new Error("PROPERTY_OPTIONS_MIGRATION_REQUIRED");
479
+ }
480
+ throw new Error("PROPERTY_OPTIONS_INVALID");
481
+ }
482
+ function normalizeExplicitConfig(config, type) {
483
+ const normalized = normalizePropertyDefConfig(config);
484
+ if (!normalized)
485
+ throw new Error("PROPERTY_DEF_CONFIG_INVALID");
486
+ if (normalized.type !== type)
487
+ throw new Error("PROPERTY_DEF_CONFIG_TYPE_MISMATCH");
488
+ return normalized;
489
+ }
490
+ function optionsJsonForNewDefinition(type, input) {
491
+ if (type === "number") {
492
+ return input.numberPreset && input.numberPreset !== "number"
493
+ ? serializeNumberPropertyPreset(input.numberPreset)
494
+ : null;
495
+ }
496
+ if (type === "unique_id") {
497
+ return serializeUniqueIdPropertyConfig({
498
+ prefix: normalizeUniqueIdPrefix(input.uniqueIdPrefix, normalizeOptionalString(input.name)),
499
+ next: 1,
500
+ });
501
+ }
502
+ if (!isOptionPropertyType(type))
503
+ return null;
504
+ const items = type === "status" && input.optionItems === undefined
505
+ ? DEFAULT_STATUS_OPTION_INPUTS
506
+ : input.optionItems;
507
+ return serializePropertyDefOptions(createOptionsForNewDefinition(items));
508
+ }
509
+ async function ensurePropertyDef(db, input) {
510
+ const key = input.key.trim();
511
+ const userId = input.userId.trim();
512
+ const updatedAt = input.updatedAt.trim();
513
+ if (!isPropertyKeyValid(key) || !userId || !updatedAt) {
514
+ throw new Error("PROPERTY_DEF_ENSURE_INPUT_INVALID");
515
+ }
516
+ let result = null;
517
+ await db.writeTransaction(async (executor) => {
518
+ const existing = await loadLatestDefinitionByKey(executor, userId, key);
519
+ if (!existing) {
520
+ const type = input.type ?? "text";
521
+ const hasOptionInput = input.optionItems !== undefined;
522
+ if (type === "unique_id" && hasOptionInput) {
523
+ throw new Error("PROPERTY_OPTIONS_TYPE_MISMATCH");
524
+ }
525
+ if (!isOptionPropertyType(type) && type !== "unique_id" && hasOptionInput) {
526
+ throw new Error("PROPERTY_OPTIONS_TYPE_MISMATCH");
527
+ }
528
+ const options = optionsJsonForNewDefinition(type, input);
529
+ const explicitConfig = input.config === undefined ? undefined : normalizeExplicitConfig(input.config, type);
530
+ const config = explicitConfig ?? getDefaultPropertyDefConfig(type, "new");
531
+ const id = uuidv7();
532
+ await executor.execute(`
533
+ INSERT INTO c_property_def (
534
+ id, user_id, key, name, type, options, description, icon,
535
+ sort_rank, show_on_card, source, created_at, updated_at, is_del
536
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?, ?, 0)
537
+ `, [
538
+ id,
539
+ userId,
540
+ key,
541
+ normalizeOptionalString(input.name) ?? key,
542
+ type,
543
+ options,
544
+ normalizeOptionalString(input.description),
545
+ normalizeOptionalString(input.icon),
546
+ normalizeOptionalString(input.source) ?? "manual",
547
+ updatedAt,
548
+ updatedAt,
549
+ ]);
550
+ const configChanged = config
551
+ ? await upsertPropertyConfig(executor, id, userId, config, updatedAt)
552
+ : false;
553
+ result = { id, definitionChanged: true, configChanged };
554
+ return;
555
+ }
556
+ const existingType = requireStoredPropertyType(existing.type);
557
+ if (input.type !== undefined && input.type !== null && input.type !== existingType) {
558
+ throw new Error("PROPERTY_DEF_TYPE_CONFLICT");
559
+ }
560
+ const hasOptionInput = input.optionItems !== undefined;
561
+ if (!isOptionPropertyType(existingType) && hasOptionInput) {
562
+ throw new Error("PROPERTY_OPTIONS_TYPE_MISMATCH");
563
+ }
564
+ let serializedOptions = existing.options;
565
+ let optionsChanged = false;
566
+ if (isOptionPropertyType(existingType)) {
567
+ const parsed = parsePropertyDefOptionsEnvelope(existing.options);
568
+ if (parsed.status === "invalid" || parsed.status === "unsupported") {
569
+ throw new Error("PROPERTY_OPTIONS_INVALID");
570
+ }
571
+ if (input.optionItems !== undefined) {
572
+ if (parsed.status !== "valid")
573
+ throw new Error("PROPERTY_OPTIONS_MIGRATION_REQUIRED");
574
+ const configured = replaceStableOptions(parsed.options, input.optionItems);
575
+ const serialized = serializePropertyDefOptions(configured);
576
+ optionsChanged = serialized !== serializePropertyDefOptions(parsed.options);
577
+ serializedOptions = serialized;
578
+ }
579
+ }
580
+ let configChanged = false;
581
+ if (input.config !== undefined) {
582
+ configChanged = await upsertPropertyConfig(executor, existing.id, userId, normalizeExplicitConfig(input.config, existingType), updatedAt);
583
+ }
584
+ const isDeleted = Number(existing.is_del ?? 0) !== 0;
585
+ if (isDeleted) {
586
+ await executor.execute(`
587
+ UPDATE c_property_def
588
+ SET name = ?, type = ?, options = ?, description = ?, icon = ?,
589
+ sort_rank = ?, show_on_card = ?, source = ?, is_del = 0, updated_at = ?
590
+ WHERE id = ? AND user_id = ?
591
+ `, [
592
+ normalizeOptionalString(input.name) ?? normalizeOptionalString(existing.name) ?? key,
593
+ existingType,
594
+ serializedOptions,
595
+ normalizeOptionalString(input.description) ?? normalizeOptionalString(existing.description),
596
+ normalizeOptionalString(input.icon) ?? normalizeOptionalString(existing.icon),
597
+ Number.isFinite(existing.sort_rank) ? Math.trunc(Number(existing.sort_rank)) : 0,
598
+ Number(existing.show_on_card ?? 0) === 1 ? 1 : 0,
599
+ normalizeOptionalString(input.source) ??
600
+ normalizeOptionalString(existing.source) ??
601
+ "manual",
602
+ updatedAt,
603
+ existing.id,
604
+ userId,
605
+ ]);
606
+ result = {
607
+ id: existing.id,
608
+ definitionChanged: true,
609
+ configChanged,
610
+ };
611
+ return;
612
+ }
613
+ if (optionsChanged) {
614
+ await executor.execute(`UPDATE c_property_def SET options = ?, updated_at = ? WHERE id = ? AND user_id = ?`, [serializedOptions, updatedAt, existing.id, userId]);
615
+ }
616
+ result = {
617
+ id: existing.id,
618
+ definitionChanged: optionsChanged,
619
+ configChanged,
620
+ };
621
+ });
622
+ if (!result)
623
+ throw new Error("PROPERTY_DEF_ENSURE_FAILED");
624
+ return result;
625
+ }
626
+ async function configurePropertyDef(db, input) {
627
+ const id = input.id.trim();
628
+ const userId = input.userId.trim();
629
+ const updatedAt = input.updatedAt.trim();
630
+ let result = {
631
+ changed: false,
632
+ definitionChanged: false,
633
+ configChanged: false,
634
+ };
635
+ await db.writeTransaction(async (executor) => {
636
+ const row = await loadDefinitionById(executor, id, userId);
637
+ if (!row)
638
+ return;
639
+ const type = requireStoredPropertyType(row.type);
640
+ const assignments = [];
641
+ const params = [];
642
+ if (input.options !== undefined) {
643
+ if (!isOptionPropertyType(type)) {
644
+ if (input.options.length > 0)
645
+ throw new Error("PROPERTY_OPTIONS_TYPE_MISMATCH");
646
+ if (input.repairInvalidOptions)
647
+ throw new Error("PROPERTY_OPTIONS_TYPE_MISMATCH");
648
+ }
649
+ else {
650
+ const parsed = parsePropertyDefOptionsEnvelope(row.options);
651
+ let currentSerialized;
652
+ let next;
653
+ if (parsed.status === "valid") {
654
+ currentSerialized = serializePropertyDefOptions(parsed.options);
655
+ next = replaceStableOptions(parsed.options, input.options);
656
+ }
657
+ else if ((parsed.status === "invalid" || parsed.status === "unsupported") &&
658
+ input.repairInvalidOptions) {
659
+ if (input.options.some((option) => option.id !== undefined)) {
660
+ throw new Error("PROPERTY_OPTION_REPAIR_ID_FORBIDDEN");
661
+ }
662
+ currentSerialized = row.options ?? "";
663
+ next = createOptionsForNewDefinition(input.options);
664
+ }
665
+ else if (parsed.status === "empty" || parsed.status === "legacy") {
666
+ throw new Error("PROPERTY_OPTIONS_MIGRATION_REQUIRED");
667
+ }
668
+ else {
669
+ throw new Error("PROPERTY_OPTIONS_INVALID");
670
+ }
671
+ const nextSerialized = serializePropertyDefOptions(next);
672
+ if (currentSerialized !== nextSerialized) {
673
+ assignments.push("options = ?");
674
+ params.push(nextSerialized);
675
+ }
676
+ }
677
+ }
678
+ if (input.uniqueIdPrefix !== undefined) {
679
+ if (type !== "unique_id")
680
+ throw new Error("PROPERTY_UNIQUE_ID_TYPE_MISMATCH");
681
+ const current = parseUniqueIdPropertyConfig(row.options);
682
+ if (!current && row.options?.trim()) {
683
+ throw new Error("PROPERTY_UNIQUE_ID_CONFIG_INVALID");
684
+ }
685
+ const nextSerialized = serializeUniqueIdPropertyConfig({
686
+ prefix: normalizeUniqueIdPrefix(input.uniqueIdPrefix, row.name ?? row.key),
687
+ next: current?.next ?? 1,
688
+ });
689
+ if (nextSerialized !== (row.options ?? "")) {
690
+ assignments.push("options = ?");
691
+ params.push(nextSerialized);
692
+ }
693
+ }
694
+ if (input.name !== undefined) {
695
+ const value = normalizeOptionalString(input.name) ?? "";
696
+ if (value !== (row.name ?? "")) {
697
+ assignments.push("name = ?");
698
+ params.push(value);
699
+ }
700
+ }
701
+ if (input.description !== undefined) {
702
+ const value = normalizeOptionalString(input.description);
703
+ if (value !== normalizeOptionalString(row.description)) {
704
+ assignments.push("description = ?");
705
+ params.push(value);
706
+ }
707
+ }
708
+ if (input.icon !== undefined) {
709
+ const value = normalizeOptionalString(input.icon);
710
+ if (value !== normalizeOptionalString(row.icon)) {
711
+ assignments.push("icon = ?");
712
+ params.push(value);
713
+ }
714
+ }
715
+ if (input.showOnCard !== undefined) {
716
+ const value = input.showOnCard === true ? 1 : 0;
717
+ if (value !== (Number(row.show_on_card ?? 0) === 1 ? 1 : 0)) {
718
+ assignments.push("show_on_card = ?");
719
+ params.push(value);
720
+ }
721
+ }
722
+ if (input.sortRank !== undefined) {
723
+ const value = input.sortRank ?? 0;
724
+ if (value !== Number(row.sort_rank ?? 0)) {
725
+ assignments.push("sort_rank = ?");
726
+ params.push(value);
727
+ }
728
+ }
729
+ const normalizedConfig = input.config && input.config !== null
730
+ ? normalizeExplicitConfig(input.config, type)
731
+ : undefined;
732
+ const definitionChanged = assignments.length > 0;
733
+ if (definitionChanged) {
734
+ assignments.push("updated_at = ?");
735
+ params.push(updatedAt, id, userId);
736
+ await executor.execute(`UPDATE c_property_def SET ${assignments.join(", ")} WHERE id = ? AND user_id = ?`, params);
737
+ }
738
+ let configChanged = false;
739
+ if (input.config === null) {
740
+ configChanged = await clearPropertyConfig(executor, id, userId, updatedAt);
741
+ }
742
+ else if (normalizedConfig !== undefined) {
743
+ configChanged = await upsertPropertyConfig(executor, id, userId, normalizedConfig, updatedAt);
744
+ }
745
+ result = {
746
+ changed: definitionChanged || configChanged,
747
+ definitionChanged,
748
+ configChanged,
749
+ };
750
+ });
751
+ return result;
752
+ }
753
+ async function updatePropertyDef(db, input) {
754
+ const id = input.id.trim();
755
+ const userId = input.userId.trim();
756
+ const updatedAt = input.updatedAt.trim();
757
+ let changed = false;
758
+ await db.writeTransaction(async (executor) => {
759
+ const row = await loadDefinitionById(executor, id, userId);
760
+ if (!row)
761
+ return;
762
+ const nextName = input.name === undefined
763
+ ? (normalizeOptionalString(row.name) ?? normalizeOptionalString(row.key) ?? "")
764
+ : (normalizeOptionalString(input.name) ?? "");
765
+ const nextDescription = input.description === undefined
766
+ ? normalizeOptionalString(row.description)
767
+ : normalizeOptionalString(input.description);
768
+ const nextIcon = input.icon === undefined
769
+ ? normalizeOptionalString(row.icon)
770
+ : normalizeOptionalString(input.icon);
771
+ const nextShowOnCard = input.showOnCard === undefined
772
+ ? Number(row.show_on_card ?? 0) === 1
773
+ : input.showOnCard === true;
774
+ const nextSortRank = input.sortRank === undefined
775
+ ? Number.isFinite(row.sort_rank)
776
+ ? Math.trunc(Number(row.sort_rank))
777
+ : 0
778
+ : (input.sortRank ?? 0);
779
+ if (nextName === (row.name ?? "") &&
780
+ nextDescription === normalizeOptionalString(row.description) &&
781
+ nextIcon === normalizeOptionalString(row.icon) &&
782
+ nextShowOnCard === (Number(row.show_on_card ?? 0) === 1) &&
783
+ nextSortRank === Number(row.sort_rank ?? 0)) {
784
+ return;
785
+ }
786
+ await executor.execute(`
787
+ UPDATE c_property_def
788
+ SET name = ?, description = ?, icon = ?, show_on_card = ?, sort_rank = ?, updated_at = ?
789
+ WHERE id = ? AND user_id = ?
790
+ `, [
791
+ nextName,
792
+ nextDescription,
793
+ nextIcon,
794
+ nextShowOnCard ? 1 : 0,
795
+ nextSortRank,
796
+ updatedAt,
797
+ id,
798
+ userId,
799
+ ]);
800
+ changed = true;
801
+ });
802
+ return changed;
803
+ }
804
+ async function softDeletePropertyDef(db, input) {
805
+ const id = input.id.trim();
806
+ const userId = input.userId.trim();
807
+ const updatedAt = input.updatedAt.trim();
808
+ let result = {
809
+ definitionChanged: false,
810
+ configChanged: false,
811
+ };
812
+ await db.writeTransaction(async (executor) => {
813
+ const row = await executor.getOptional(`
814
+ SELECT id FROM c_property_def
815
+ WHERE id = ? AND user_id = ? AND (is_del IS NULL OR is_del = 0)
816
+ LIMIT 1
817
+ `, [id, userId]);
818
+ if (!row)
819
+ return;
820
+ await executor.execute(`UPDATE c_property_def SET is_del = 1, updated_at = ? WHERE id = ? AND user_id = ?`, [updatedAt, id, userId]);
821
+ result = {
822
+ definitionChanged: true,
823
+ configChanged: await clearPropertyConfig(executor, id, userId, updatedAt),
824
+ };
825
+ });
826
+ return result;
827
+ }
828
+ export async function fetchPropertyCatalog(db, input) {
829
+ const userId = input.userId.trim();
830
+ const limit = normalizeLimit(input.limit, 1000, 5000);
831
+ if (limit <= 0)
832
+ return [];
833
+ return db.getAll(`
834
+ WITH ranked_property_defs AS (
835
+ SELECT definition.*,
836
+ ROW_NUMBER() OVER (
837
+ PARTITION BY definition.key
838
+ ORDER BY
839
+ COALESCE(NULLIF(definition.updated_at, ''), NULLIF(definition.created_at, ''), definition.id) DESC,
840
+ definition.id DESC
841
+ ) AS key_rank
842
+ FROM c_property_def AS definition
843
+ WHERE definition.user_id = ?
844
+ AND definition.key IS NOT NULL
845
+ AND trim(definition.key) <> ''
846
+ AND (definition.is_del IS NULL OR definition.is_del = 0)
847
+ )
848
+ SELECT ${propertyDefSelectList("ranked")}
849
+ FROM ranked_property_defs AS ranked
850
+ WHERE ranked.key_rank = 1
851
+ ORDER BY COALESCE(ranked.sort_rank, 0) ASC,
852
+ COALESCE(NULLIF(ranked.created_at, ''), ranked.id) ASC,
853
+ ranked.id ASC
854
+ LIMIT ?
855
+ `, [userId, limit]);
856
+ }
857
+ function addMigrationLabel(definition, value, active) {
858
+ const label = normalizePropertyOptionLabel(value);
859
+ if (!label)
860
+ throw new Error("PROPERTY_LEGACY_OPTION_LABEL_INVALID");
861
+ const identity = propertyOptionLabelKey(label);
862
+ const existing = definition.labels.get(identity);
863
+ if (existing) {
864
+ if (active)
865
+ existing.active = true;
866
+ return;
867
+ }
868
+ if (definition.labels.size >= PROPERTY_OPTION_LIMITS.items) {
869
+ throw new Error("PROPERTY_OPTION_LIMIT_EXCEEDED");
870
+ }
871
+ definition.labels.set(identity, { label, active });
872
+ }
873
+ function parseSavedViewFilterForMigration(raw) {
874
+ if (!raw || !raw.trim())
875
+ return null;
876
+ let parsed;
877
+ try {
878
+ parsed = JSON.parse(raw);
879
+ }
880
+ catch {
881
+ return undefined;
882
+ }
883
+ const result = savedViewFilterSchema.safeParse(parsed);
884
+ if (!result.success)
885
+ return undefined;
886
+ return result.data;
887
+ }
888
+ function rawMentionsAnyPropertyKey(raw, keys, prefix = "", suffix = "") {
889
+ if (!raw)
890
+ return false;
891
+ for (const key of keys) {
892
+ if (raw.includes(`${prefix}${key}${suffix}`))
893
+ return true;
894
+ }
895
+ return false;
896
+ }
897
+ function rawMentionsAnySavedViewPropertyField(raw, keys) {
898
+ if (!raw)
899
+ return false;
900
+ const keySet = new Set(keys);
901
+ for (const match of raw.matchAll(/"field"\s*:\s*("(?:\\.|[^"\\])*")/g)) {
902
+ const token = match[1];
903
+ if (!token)
904
+ continue;
905
+ try {
906
+ const field = JSON.parse(token);
907
+ if (typeof field === "string" &&
908
+ field.startsWith("prop.") &&
909
+ keySet.has(field.slice("prop.".length))) {
910
+ return true;
911
+ }
912
+ }
913
+ catch {
914
+ // The complete field string token is malformed, so it cannot identify a key.
915
+ }
916
+ }
917
+ return false;
918
+ }
919
+ function visitSavedViewFilterConditions(node, visit) {
920
+ if (!node)
921
+ return;
922
+ if (node.kind === "condition") {
923
+ visit(node);
924
+ return;
925
+ }
926
+ if (node.kind === "not") {
927
+ visitSavedViewFilterConditions(node.child, visit);
928
+ return;
929
+ }
930
+ for (const child of node.children)
931
+ visitSavedViewFilterConditions(child, visit);
932
+ }
933
+ function buildMigrationOptions(definition) {
934
+ definition.options = [...definition.labels.values()].map((entry, index) => ({
935
+ id: uuidv7(),
936
+ label: entry.label,
937
+ color: "gray",
938
+ sortRank: index,
939
+ archived: !entry.active,
940
+ }));
941
+ definition.optionByLabel = new Map(definition.options.map((option) => [propertyOptionLabelKey(option.label), option]));
942
+ }
943
+ function migratedOptionForLabel(definition, value) {
944
+ const label = normalizePropertyOptionLabel(value);
945
+ if (!label)
946
+ throw new Error("PROPERTY_LEGACY_OPTION_LABEL_INVALID");
947
+ const option = definition.optionByLabel.get(propertyOptionLabelKey(label));
948
+ if (!option)
949
+ throw new Error("PROPERTY_LEGACY_OPTION_NOT_PLANNED");
950
+ return option;
951
+ }
952
+ function canonicalOptionIds(ids, options) {
953
+ const byId = new Map(options.map((option) => [option.id, option]));
954
+ return [...new Set(ids)].sort((leftId, rightId) => {
955
+ const left = byId.get(leftId);
956
+ const right = byId.get(rightId);
957
+ if (!left || !right)
958
+ return leftId < rightId ? -1 : leftId > rightId ? 1 : 0;
959
+ return (left.sortRank - right.sortRank ||
960
+ (left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
961
+ });
962
+ }
963
+ async function migrateLegacyPropertyOptions(db, input) {
964
+ const userId = input.userId.trim();
965
+ const updatedAt = input.updatedAt.trim();
966
+ let result = {
967
+ definitionsMigrated: 0,
968
+ optionsCreated: 0,
969
+ notesUpdated: 0,
970
+ savedViewsUpdated: 0,
971
+ };
972
+ await db.writeTransaction(async (executor) => {
973
+ const rows = await executor.getAll(`
974
+ WITH ranked_property_defs AS (
975
+ SELECT definition.*,
976
+ ROW_NUMBER() OVER (
977
+ PARTITION BY definition.key
978
+ ORDER BY
979
+ COALESCE(NULLIF(definition.updated_at, ''), NULLIF(definition.created_at, ''), definition.id) DESC,
980
+ definition.id DESC
981
+ ) AS key_rank
982
+ FROM c_property_def AS definition
983
+ WHERE definition.user_id = ?
984
+ AND definition.type IN ('select', 'multi_select')
985
+ AND (definition.is_del IS NULL OR definition.is_del = 0)
986
+ )
987
+ SELECT ${propertyDefSelectList("ranked")}
988
+ FROM ranked_property_defs AS ranked
989
+ WHERE ranked.key_rank = 1
990
+ ORDER BY ranked.key ASC, ranked.id ASC
991
+ `, [userId]);
992
+ const definitions = new Map();
993
+ for (const row of rows) {
994
+ if (!row.key || (row.type !== "select" && row.type !== "multi_select"))
995
+ continue;
996
+ const parsed = parsePropertyDefOptionsEnvelope(row.options);
997
+ if (parsed.status === "valid")
998
+ continue;
999
+ if (parsed.status === "invalid" || parsed.status === "unsupported") {
1000
+ throw new Error("PROPERTY_OPTIONS_INVALID");
1001
+ }
1002
+ const definition = {
1003
+ row,
1004
+ type: row.type,
1005
+ labels: new Map(),
1006
+ options: [],
1007
+ optionByLabel: new Map(),
1008
+ };
1009
+ for (const label of parsed.status === "legacy" ? parsed.labels : []) {
1010
+ addMigrationLabel(definition, label, true);
1011
+ }
1012
+ definitions.set(row.key, definition);
1013
+ }
1014
+ if (definitions.size === 0)
1015
+ return;
1016
+ const noteRows = await executor.getAll(`
1017
+ SELECT id, properties
1018
+ FROM c_note
1019
+ WHERE user_no = ? AND (is_del IS NULL OR is_del = 0)
1020
+ ORDER BY id ASC
1021
+ `, [userId]);
1022
+ const parsedNotes = noteRows.map((row) => {
1023
+ const properties = tryParseRawNoteProperties(row.properties);
1024
+ if (properties === null) {
1025
+ if (rawMentionsAnyPropertyKey(row.properties, definitions.keys(), '"', '"')) {
1026
+ throw new Error("NOTE_PROPERTIES_CORRUPT");
1027
+ }
1028
+ return { ...row, parsed: null };
1029
+ }
1030
+ for (const [key, definition] of definitions) {
1031
+ if (!Object.hasOwn(properties, key) || properties[key] === null)
1032
+ continue;
1033
+ const value = properties[key];
1034
+ if (definition.type === "select") {
1035
+ if (typeof value !== "string")
1036
+ throw new Error("PROPERTY_LEGACY_VALUE_INVALID");
1037
+ addMigrationLabel(definition, value, false);
1038
+ }
1039
+ else {
1040
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
1041
+ throw new Error("PROPERTY_LEGACY_VALUE_INVALID");
1042
+ }
1043
+ for (const item of value)
1044
+ addMigrationLabel(definition, item, false);
1045
+ }
1046
+ }
1047
+ return { ...row, parsed: properties };
1048
+ });
1049
+ const savedViewRows = await executor.getAll(`
1050
+ SELECT id, filter_json
1051
+ FROM c_saved_view
1052
+ WHERE user_id = ? AND (is_del IS NULL OR is_del = 0)
1053
+ ORDER BY id ASC
1054
+ `, [userId]);
1055
+ const parsedViews = savedViewRows.map((row) => {
1056
+ const filter = parseSavedViewFilterForMigration(row.filter_json);
1057
+ if (filter === undefined) {
1058
+ if (rawMentionsAnySavedViewPropertyField(row.filter_json, definitions.keys())) {
1059
+ throw new Error("SAVED_VIEW_FILTER_CORRUPT");
1060
+ }
1061
+ return { ...row, filter: null };
1062
+ }
1063
+ if (filter) {
1064
+ visitSavedViewFilterConditions(filter.root, (condition) => {
1065
+ if (!condition.field.startsWith("prop.") || typeof condition.value !== "string") {
1066
+ return;
1067
+ }
1068
+ const definition = definitions.get(condition.field.slice("prop.".length));
1069
+ if (definition)
1070
+ addMigrationLabel(definition, condition.value, false);
1071
+ });
1072
+ }
1073
+ return { ...row, filter };
1074
+ });
1075
+ for (const definition of definitions.values())
1076
+ buildMigrationOptions(definition);
1077
+ for (const note of parsedNotes) {
1078
+ if (!note.parsed)
1079
+ continue;
1080
+ const next = Object.fromEntries(Object.entries(note.parsed));
1081
+ let changed = false;
1082
+ for (const [key, definition] of definitions) {
1083
+ if (!Object.hasOwn(next, key) || next[key] === null)
1084
+ continue;
1085
+ const value = next[key];
1086
+ if (definition.type === "select") {
1087
+ const id = migratedOptionForLabel(definition, value).id;
1088
+ if (value !== id) {
1089
+ next[key] = id;
1090
+ changed = true;
1091
+ }
1092
+ }
1093
+ else {
1094
+ const ids = canonicalOptionIds(value.map((item) => migratedOptionForLabel(definition, item).id), definition.options);
1095
+ if (!Array.isArray(value) ||
1096
+ value.length !== ids.length ||
1097
+ value.some((item, index) => item !== ids[index])) {
1098
+ next[key] = ids;
1099
+ changed = true;
1100
+ }
1101
+ }
1102
+ }
1103
+ if (changed) {
1104
+ await executor.execute(`
1105
+ UPDATE c_note SET properties = ?
1106
+ WHERE id = ? AND user_no = ? AND (is_del IS NULL OR is_del = 0)
1107
+ `, [serializeRawNoteProperties(next), note.id, userId]);
1108
+ result.notesUpdated += 1;
1109
+ }
1110
+ }
1111
+ for (const view of parsedViews) {
1112
+ if (!view.filter)
1113
+ continue;
1114
+ let changed = false;
1115
+ visitSavedViewFilterConditions(view.filter.root, (condition) => {
1116
+ if (!condition.field.startsWith("prop.") || typeof condition.value !== "string") {
1117
+ return;
1118
+ }
1119
+ const definition = definitions.get(condition.field.slice("prop.".length));
1120
+ if (!definition)
1121
+ return;
1122
+ const id = migratedOptionForLabel(definition, condition.value).id;
1123
+ if (condition.value !== id) {
1124
+ condition.value = id;
1125
+ changed = true;
1126
+ }
1127
+ });
1128
+ if (changed) {
1129
+ await executor.execute(`
1130
+ UPDATE c_saved_view SET filter_json = ?
1131
+ WHERE id = ? AND user_id = ? AND (is_del IS NULL OR is_del = 0)
1132
+ `, [JSON.stringify(view.filter), view.id, userId]);
1133
+ result.savedViewsUpdated += 1;
1134
+ }
1135
+ }
1136
+ for (const definition of definitions.values()) {
1137
+ await executor.execute(`
1138
+ UPDATE c_property_def SET options = ?, updated_at = ?
1139
+ WHERE id = ? AND user_id = ? AND (is_del IS NULL OR is_del = 0)
1140
+ `, [
1141
+ serializePropertyDefOptions(definition.options),
1142
+ updatedAt,
1143
+ definition.row.id,
1144
+ userId,
1145
+ ]);
1146
+ result.definitionsMigrated += 1;
1147
+ result.optionsCreated += definition.options.length;
1148
+ }
1149
+ });
1150
+ return result;
1151
+ }
1152
+ function definitionStateForWrite(row) {
1153
+ const type = requireStoredPropertyType(row.type);
1154
+ const parsedConfig = parsePropertyDefConfigEnvelope(row.config_json);
1155
+ let config;
1156
+ if (parsedConfig.status === "empty") {
1157
+ config = getDefaultPropertyDefConfig(type, "legacy");
1158
+ }
1159
+ else if (parsedConfig.status === "valid" && parsedConfig.config.type === type) {
1160
+ config = parsedConfig.config;
1161
+ }
1162
+ else {
1163
+ throw new Error("PROPERTY_DEF_CONFIG_INVALID");
1164
+ }
1165
+ let options = [];
1166
+ if (isOptionPropertyType(type))
1167
+ options = stableOptionsFromRow(row);
1168
+ return { row, type, config, options, optionsChanged: false };
1169
+ }
1170
+ const UUID_TOKEN_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1171
+ function resolveOptionToken(state, value, allowedArchivedIds) {
1172
+ if (typeof value !== "string")
1173
+ throw new Error("PROPERTY_OPTION_VALUE_INVALID");
1174
+ const token = value.trim();
1175
+ if (!token)
1176
+ throw new Error("PROPERTY_OPTION_VALUE_INVALID");
1177
+ const byId = findPropertyOptionById(state.options, token);
1178
+ if (byId) {
1179
+ if (byId.archived && !allowedArchivedIds.has(byId.id)) {
1180
+ throw new Error("PROPERTY_OPTION_ARCHIVED");
1181
+ }
1182
+ return byId.id;
1183
+ }
1184
+ const byLabel = findPropertyOptionByLabel(state.options, token);
1185
+ if (byLabel) {
1186
+ if (byLabel.archived)
1187
+ throw new Error("PROPERTY_OPTION_ARCHIVED");
1188
+ return byLabel.id;
1189
+ }
1190
+ if (UUID_TOKEN_PATTERN.test(token))
1191
+ throw new Error("PROPERTY_OPTION_NOT_FOUND");
1192
+ const isOpen = (state.type === "select" &&
1193
+ state.config?.type === "select" &&
1194
+ state.config.mode === "open") ||
1195
+ (state.type === "multi_select" &&
1196
+ state.config?.type === "multi_select" &&
1197
+ state.config.mode === "open");
1198
+ if (!isOpen)
1199
+ throw new Error("PROPERTY_OPTION_NOT_FOUND");
1200
+ const label = normalizePropertyOptionLabel(token);
1201
+ if (!label)
1202
+ throw new Error("PROPERTY_OPTION_LABEL_INVALID");
1203
+ if (state.options.length >= PROPERTY_OPTION_LIMITS.items) {
1204
+ throw new Error("PROPERTY_OPTION_LIMIT_EXCEEDED");
1205
+ }
1206
+ const option = createPropertyOption(label, nextOptionSortRank(state.options));
1207
+ state.options = [...state.options, option];
1208
+ state.optionsChanged = true;
1209
+ return option.id;
1210
+ }
1211
+ function normalizeKnownPropertyValue(state, value, allowedArchivedIds) {
1212
+ switch (state.type) {
1213
+ case "text":
1214
+ case "email":
1215
+ case "phone": {
1216
+ const normalized = coercePropertyValue(state.type, value);
1217
+ if (typeof normalized !== "string")
1218
+ throw new Error("PROPERTY_VALUE_INVALID");
1219
+ return normalized;
1220
+ }
1221
+ case "checkbox": {
1222
+ const normalized = coercePropertyValue("checkbox", value);
1223
+ if (typeof normalized !== "boolean")
1224
+ throw new Error("PROPERTY_VALUE_INVALID");
1225
+ return normalized;
1226
+ }
1227
+ case "number": {
1228
+ if (state.config?.type !== "number")
1229
+ throw new Error("PROPERTY_DEF_CONFIG_INVALID");
1230
+ const normalized = normalizeNumberPropertyValue(value, state.config);
1231
+ if (normalized === null)
1232
+ throw new Error("PROPERTY_NUMBER_INVALID");
1233
+ // Presets set by dinox-app (rating 0–5 integers, progress 0–1) bind here too.
1234
+ const preset = parseNumberPropertyPreset(state.row.options);
1235
+ if (!isNumberPropertyValueAllowed(normalized, preset)) {
1236
+ throw new Error("PROPERTY_NUMBER_INVALID");
1237
+ }
1238
+ return normalized;
1239
+ }
1240
+ case "date": {
1241
+ if (state.config?.type !== "date")
1242
+ throw new Error("PROPERTY_DEF_CONFIG_INVALID");
1243
+ const normalized = normalizeDatePropertyValue(value, state.config.mode);
1244
+ if (normalized === null)
1245
+ throw new Error("PROPERTY_DATE_INVALID");
1246
+ return normalized;
1247
+ }
1248
+ case "url": {
1249
+ if (state.config?.type !== "url")
1250
+ throw new Error("PROPERTY_DEF_CONFIG_INVALID");
1251
+ const normalized = normalizeUrlPropertyValue(value, state.config);
1252
+ if (normalized === null)
1253
+ throw new Error("PROPERTY_URL_INVALID");
1254
+ return normalized;
1255
+ }
1256
+ case "select":
1257
+ case "status":
1258
+ return resolveOptionToken(state, value, allowedArchivedIds);
1259
+ case "multi_select": {
1260
+ if (state.config?.type !== "multi_select") {
1261
+ throw new Error("PROPERTY_DEF_CONFIG_INVALID");
1262
+ }
1263
+ const values = Array.isArray(value) ? value : [value];
1264
+ const ids = canonicalOptionIds(values.map((item) => resolveOptionToken(state, item, allowedArchivedIds)), state.options);
1265
+ const config = state.config;
1266
+ if (ids.length === 0)
1267
+ throw new Error("PROPERTY_MULTI_SELECT_EMPTY");
1268
+ if (config.maxSelections !== undefined && ids.length > config.maxSelections) {
1269
+ throw new Error("PROPERTY_MULTI_SELECT_LIMIT_EXCEEDED");
1270
+ }
1271
+ return ids;
1272
+ }
1273
+ case "relation": {
1274
+ const values = Array.isArray(value) ? value : [value];
1275
+ const ids = [];
1276
+ const seen = new Set();
1277
+ for (const item of values) {
1278
+ if (typeof item !== "string")
1279
+ throw new Error("PROPERTY_RELATION_INVALID");
1280
+ const id = item.trim();
1281
+ if (!id || id.length > 200)
1282
+ throw new Error("PROPERTY_RELATION_INVALID");
1283
+ if (seen.has(id))
1284
+ continue;
1285
+ seen.add(id);
1286
+ ids.push(id);
1287
+ }
1288
+ if (ids.length === 0)
1289
+ throw new Error("PROPERTY_RELATION_EMPTY");
1290
+ if (ids.length > NOTE_PROPERTY_LIMITS.arrayLength) {
1291
+ throw new Error("PROPERTY_RELATION_LIMIT");
1292
+ }
1293
+ return ids;
1294
+ }
1295
+ case "files": {
1296
+ const files = normalizeFilePropertyValue(value);
1297
+ if (!files || files.length === 0)
1298
+ throw new Error("PROPERTY_FILES_INVALID");
1299
+ return files;
1300
+ }
1301
+ case "place": {
1302
+ const place = normalizePlacePropertyValue(value);
1303
+ if (!place)
1304
+ throw new Error("PROPERTY_PLACE_INVALID");
1305
+ return place;
1306
+ }
1307
+ case "unique_id": {
1308
+ if (!isIssuedUniqueId(value))
1309
+ throw new Error("PROPERTY_UNIQUE_ID_INVALID");
1310
+ return value;
1311
+ }
1312
+ default:
1313
+ throw new Error("PROPERTY_TYPE_NOT_ACTIVE");
1314
+ }
1315
+ }
1316
+ async function loadDefinitionsByKeys(executor, userId, keys) {
1317
+ const uniqueKeys = [...new Set(keys)];
1318
+ if (uniqueKeys.length === 0)
1319
+ return new Map();
1320
+ const placeholders = uniqueKeys.map(() => "?").join(", ");
1321
+ const rows = await executor.getAll(`
1322
+ WITH ranked_property_defs AS (
1323
+ SELECT definition.*,
1324
+ ROW_NUMBER() OVER (
1325
+ PARTITION BY definition.key
1326
+ ORDER BY
1327
+ COALESCE(NULLIF(definition.updated_at, ''), NULLIF(definition.created_at, ''), definition.id) DESC,
1328
+ definition.id DESC
1329
+ ) AS key_rank
1330
+ FROM c_property_def AS definition
1331
+ WHERE definition.user_id = ?
1332
+ AND definition.key IN (${placeholders})
1333
+ AND (definition.is_del IS NULL OR definition.is_del = 0)
1334
+ )
1335
+ SELECT ${propertyDefSelectList("ranked")}
1336
+ FROM ranked_property_defs AS ranked
1337
+ WHERE ranked.key_rank = 1
1338
+ `, [userId, ...uniqueKeys]);
1339
+ return new Map(rows.filter((row) => row.key).map((row) => [row.key, row]));
1340
+ }
1341
+ async function normalizeValuesForDefinitions(executor, userId, values, persistOptionKeys, allowedArchivedByKey = new Map()) {
1342
+ const definitions = await loadDefinitionsByKeys(executor, userId, Object.keys(values));
1343
+ const normalizedEntries = [];
1344
+ const optionUpdates = [];
1345
+ for (const [key, value] of Object.entries(values)) {
1346
+ const row = definitions.get(key);
1347
+ if (!row) {
1348
+ normalizedEntries.push([key, value]);
1349
+ continue;
1350
+ }
1351
+ const state = definitionStateForWrite(row);
1352
+ const normalized = normalizeKnownPropertyValue(state, value, allowedArchivedByKey.get(key) ?? new Set());
1353
+ normalizedEntries.push([key, normalized]);
1354
+ if (state.optionsChanged && persistOptionKeys.has(key))
1355
+ optionUpdates.push(state);
1356
+ }
1357
+ return { values: Object.fromEntries(normalizedEntries), optionUpdates };
1358
+ }
1359
+ async function persistOptionUpdates(executor, updates, userId, updatedAt) {
1360
+ for (const state of updates) {
1361
+ await executor.execute(`
1362
+ UPDATE c_property_def SET options = ?, updated_at = ?
1363
+ WHERE id = ? AND user_id = ? AND (is_del IS NULL OR is_del = 0)
1364
+ `, [serializePropertyDefOptions(state.options), updatedAt, state.row.id, userId]);
1365
+ }
1366
+ }
1367
+ async function readNotePropertiesForMutation(executor, noteId, userId) {
1368
+ const row = await executor.getOptional(`
1369
+ SELECT properties
1370
+ FROM c_note
1371
+ WHERE id = ? AND user_no = ? AND (is_del IS NULL OR is_del = 0)
1372
+ LIMIT 1
1373
+ `, [noteId, userId]);
1374
+ if (!row)
1375
+ throw new Error(`Note not found: ${noteId}`);
1376
+ const properties = tryParseRawNoteProperties(row.properties);
1377
+ if (properties === null)
1378
+ throw new Error("NOTE_PROPERTIES_CORRUPT");
1379
+ return properties;
1380
+ }
1381
+ async function writeNoteProperties(executor, noteId, userId, updatedAt, properties) {
1382
+ await executor.execute(`
1383
+ UPDATE c_note SET properties = ?, updated_at = ?
1384
+ WHERE id = ? AND user_no = ? AND (is_del IS NULL OR is_del = 0)
1385
+ `, [serializeRawNoteProperties(properties), updatedAt, noteId, userId]);
1386
+ }
1387
+ function currentOptionIdsByKey(current, keys) {
1388
+ const result = new Map();
1389
+ for (const key of keys) {
1390
+ const value = current[key];
1391
+ if (typeof value === "string") {
1392
+ result.set(key, new Set([value]));
1393
+ }
1394
+ else if (Array.isArray(value)) {
1395
+ result.set(key, new Set(value.filter((item) => typeof item === "string")));
1396
+ }
1397
+ }
1398
+ return result;
1399
+ }
1400
+ async function assignMissingUniqueIds(executor, userId, current, values, updatedAt) {
1401
+ const definitions = await loadDefinitionsByKeys(executor, userId, Object.keys(values));
1402
+ const next = { ...values };
1403
+ let changed = false;
1404
+ for (const key of Object.keys(values)) {
1405
+ const row = definitions.get(key);
1406
+ if (!row || row.type !== "unique_id")
1407
+ continue;
1408
+ if (isIssuedUniqueId(current[key])) {
1409
+ next[key] = current[key];
1410
+ continue;
1411
+ }
1412
+ const config = parseUniqueIdPropertyConfig(row.options);
1413
+ if (!config && row.options?.trim()) {
1414
+ throw new Error("PROPERTY_UNIQUE_ID_CONFIG_INVALID");
1415
+ }
1416
+ const resolved = config ?? {
1417
+ prefix: normalizeUniqueIdPrefix(undefined, row.name ?? row.key),
1418
+ next: 1,
1419
+ };
1420
+ const issued = resolved.next;
1421
+ next[key] = issued;
1422
+ const serialized = serializeUniqueIdPropertyConfig({
1423
+ prefix: resolved.prefix,
1424
+ next: issued + 1,
1425
+ });
1426
+ await executor.execute(`
1427
+ UPDATE c_property_def SET options = ?, updated_at = ?
1428
+ WHERE id = ? AND user_id = ? AND (is_del IS NULL OR is_del = 0)
1429
+ `, [serialized, updatedAt, row.id, userId]);
1430
+ row.options = serialized;
1431
+ changed = true;
1432
+ }
1433
+ return { values: next, changed };
1434
+ }
1435
+ async function updateNoteProperties(db, input) {
1436
+ const noteId = input.noteId.trim();
1437
+ const userId = input.userId.trim();
1438
+ const updatedAt = input.updatedAt.trim();
1439
+ let result = { noteChanged: false, definitionsChanged: false };
1440
+ await db.writeTransaction(async (executor) => {
1441
+ const current = await readNotePropertiesForMutation(executor, noteId, userId);
1442
+ const values = Object.fromEntries(Object.entries(input.patch).filter((entry) => entry[1] !== null));
1443
+ const issued = await assignMissingUniqueIds(executor, userId, current, values, updatedAt);
1444
+ const normalized = await normalizeValuesForDefinitions(executor, userId, issued.values, new Set(Object.keys(issued.values)), currentOptionIdsByKey(current, Object.keys(issued.values)));
1445
+ const normalizedPatch = { ...input.patch, ...normalized.values };
1446
+ const mutation = mergeNotePropertiesPatch(current, normalizedPatch);
1447
+ await persistOptionUpdates(executor, normalized.optionUpdates, userId, updatedAt);
1448
+ if (mutation.changedKeys.length > 0) {
1449
+ await writeNoteProperties(executor, noteId, userId, updatedAt, mutation.next);
1450
+ }
1451
+ result = {
1452
+ noteChanged: mutation.changedKeys.length > 0,
1453
+ definitionsChanged: normalized.optionUpdates.length > 0 || issued.changed,
1454
+ };
1455
+ });
1456
+ return result;
1457
+ }
1458
+ async function fillEmptyNotePropertyValues(db, input) {
1459
+ const noteId = input.noteId.trim();
1460
+ const userId = input.userId.trim();
1461
+ const updatedAt = input.updatedAt.trim();
1462
+ let mutationResult = {
1463
+ result: { appliedKeys: [], skippedKeys: [] },
1464
+ definitionsChanged: false,
1465
+ };
1466
+ await db.writeTransaction(async (executor) => {
1467
+ const current = await readNotePropertiesForMutation(executor, noteId, userId);
1468
+ const eligibleKeys = new Set(Object.keys(input.values).filter((key) => Object.hasOwn(current, key) && current[key] === null));
1469
+ const eligibleValues = Object.fromEntries(Object.entries(input.values).filter(([key]) => eligibleKeys.has(key)));
1470
+ const issued = await assignMissingUniqueIds(executor, userId, current, eligibleValues, updatedAt);
1471
+ const normalized = await normalizeValuesForDefinitions(executor, userId, issued.values, eligibleKeys);
1472
+ const mutation = mergeNotePropertiesPatch(current, normalized.values);
1473
+ await persistOptionUpdates(executor, normalized.optionUpdates, userId, updatedAt);
1474
+ if (mutation.changedKeys.length > 0) {
1475
+ await writeNoteProperties(executor, noteId, userId, updatedAt, mutation.next);
1476
+ }
1477
+ mutationResult = {
1478
+ result: {
1479
+ appliedKeys: mutation.changedKeys,
1480
+ skippedKeys: Object.keys(input.values).filter((key) => !eligibleKeys.has(key)),
1481
+ },
1482
+ definitionsChanged: normalized.optionUpdates.length > 0 || issued.changed,
1483
+ };
1484
+ });
1485
+ return mutationResult;
1486
+ }
1487
+ async function updateNotePropertySlots(db, input) {
1488
+ const result = await mutateNotePropertiesBlob(db, input, (current) => applyNotePropertySlotOperation(current, input.operation, input.keys));
1489
+ return result.changedKeys.length > 0;
1490
+ }
1491
+ async function reorderNotePropertySlots(db, input) {
1492
+ const noteId = input.noteId.trim();
1493
+ const userId = input.userId.trim();
1494
+ const updatedAt = input.updatedAt.trim();
1495
+ let changed = false;
1496
+ await db.writeTransaction(async (executor) => {
1497
+ const row = await executor.getOptional(`
1498
+ SELECT properties
1499
+ FROM c_note
1500
+ WHERE id = ? AND user_no = ? AND (is_del IS NULL OR is_del = 0)
1501
+ LIMIT 1
1502
+ `, [noteId, userId]);
1503
+ if (!row)
1504
+ throw new Error("NOTE_NOT_FOUND");
1505
+ const current = tryParseRawNoteProperties(row.properties);
1506
+ if (!current)
1507
+ throw new Error("NOTE_PROPERTIES_INVALID");
1508
+ const visibleKeys = Object.keys(parseStoredNoteProperties(row.properties));
1509
+ const visibleKeySet = new Set(visibleKeys);
1510
+ if (input.orderedKeys.some((key) => !visibleKeySet.has(key))) {
1511
+ throw new Error("NOTE_PROPERTY_ORDER_STALE");
1512
+ }
1513
+ const requested = new Set(input.orderedKeys);
1514
+ const nextOrder = [
1515
+ ...input.orderedKeys,
1516
+ ...visibleKeys.filter((key) => !requested.has(key)),
1517
+ ];
1518
+ const currentKeys = Object.keys(current);
1519
+ const nextVisibleKeys = new Set(nextOrder);
1520
+ const nextKeys = [
1521
+ ...nextOrder,
1522
+ ...currentKeys.filter((key) => !nextVisibleKeys.has(key)),
1523
+ ];
1524
+ if (nextKeys.length === currentKeys.length &&
1525
+ nextKeys.every((key, index) => key === currentKeys[index])) {
1526
+ return;
1527
+ }
1528
+ const next = Object.fromEntries(nextKeys.map((key) => [key, current[key]]));
1529
+ await writeNoteProperties(executor, noteId, userId, updatedAt, next);
1530
+ changed = true;
1531
+ });
1532
+ return changed;
1533
+ }
1534
+ async function mutateNotePropertiesBlob(db, input, mutate) {
1535
+ const noteId = input.noteId.trim();
1536
+ const userId = input.userId.trim();
1537
+ const updatedAt = input.updatedAt.trim();
1538
+ let result = null;
1539
+ await db.writeTransaction(async (executor) => {
1540
+ const current = await readNotePropertiesForMutation(executor, noteId, userId);
1541
+ result = mutate(current);
1542
+ if (result.changedKeys.length > 0) {
1543
+ await writeNoteProperties(executor, noteId, userId, updatedAt, result.next);
1544
+ }
1545
+ });
1546
+ if (!result)
1547
+ throw new Error("NOTE_PROPERTIES_MUTATION_FAILED");
1548
+ return result;
1549
+ }