@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.
- package/README.md +17 -3
- package/dist/cli.js +6 -0
- package/dist/commands/domain/options.d.ts +10 -0
- package/dist/commands/domain/options.js +75 -0
- package/dist/commands/domain/run.d.ts +24 -0
- package/dist/commands/domain/run.js +137 -0
- package/dist/commands/gateway/index.d.ts +4 -0
- package/dist/commands/gateway/index.js +300 -0
- package/dist/commands/notes/actions.js +47 -18
- package/dist/commands/notes/createFromTemplate.d.ts +35 -0
- package/dist/commands/notes/createFromTemplate.js +58 -0
- package/dist/commands/notes/descriptors.js +9 -4
- package/dist/commands/notes/repo/read.js +4 -23
- package/dist/commands/notes/repo/write.d.ts +2 -0
- package/dist/commands/notes/repo/write.js +1 -1
- package/dist/commands/properties/index.d.ts +16 -0
- package/dist/commands/properties/index.js +640 -0
- package/dist/commands/properties/repo.d.ts +150 -0
- package/dist/commands/properties/repo.js +272 -0
- package/dist/commands/savedViews/fields.d.ts +28 -0
- package/dist/commands/savedViews/fields.js +87 -0
- package/dist/commands/savedViews/index.js +203 -164
- package/dist/commands/savedViews/model.d.ts +27 -69
- package/dist/commands/savedViews/model.js +63 -279
- package/dist/commands/savedViews/repo.d.ts +64 -62
- package/dist/commands/savedViews/repo.js +309 -428
- package/dist/commands/schema/renderSkills.js +33 -5
- package/dist/commands/schema/spec.js +6 -0
- package/dist/commands/templates/index.d.ts +4 -0
- package/dist/commands/templates/index.js +290 -0
- package/dist/commands/templates/model.d.ts +12 -34
- package/dist/commands/templates/model.js +39 -83
- package/dist/commands/templates/repo.d.ts +53 -0
- package/dist/commands/templates/repo.js +78 -0
- package/dist/config/paths.d.ts +1 -0
- package/dist/config/paths.js +1 -1
- package/dist/daemon/server.d.ts +1 -0
- package/dist/daemon/server.js +31 -0
- package/dist/gateway/adapters/acp.d.ts +42 -0
- package/dist/gateway/adapters/acp.js +285 -0
- package/dist/gateway/adapters/echo.d.ts +12 -0
- package/dist/gateway/adapters/echo.js +51 -0
- package/dist/gateway/adapters/index.d.ts +9 -0
- package/dist/gateway/adapters/index.js +30 -0
- package/dist/gateway/assistantProjection.d.ts +32 -0
- package/dist/gateway/assistantProjection.js +193 -0
- package/dist/gateway/config.d.ts +50 -0
- package/dist/gateway/config.js +253 -0
- package/dist/gateway/detect.d.ts +40 -0
- package/dist/gateway/detect.js +145 -0
- package/dist/gateway/deviceHost.d.ts +82 -0
- package/dist/gateway/deviceHost.js +530 -0
- package/dist/gateway/mcpServer.d.ts +4 -0
- package/dist/gateway/mcpServer.js +118 -0
- package/dist/gateway/protocol.d.ts +29 -0
- package/dist/gateway/protocol.js +127 -0
- package/dist/gateway/runExecutor.d.ts +67 -0
- package/dist/gateway/runExecutor.js +316 -0
- package/dist/gateway/service.d.ts +26 -0
- package/dist/gateway/service.js +82 -0
- package/dist/gateway/streamPublisher.d.ts +33 -0
- package/dist/gateway/streamPublisher.js +93 -0
- package/dist/gateway/threadStore.d.ts +157 -0
- package/dist/gateway/threadStore.js +430 -0
- package/dist/gateway/types.d.ts +117 -0
- package/dist/gateway/types.js +9 -0
- package/dist/skills/registry.js +7 -0
- package/dist/utils/errorCodes.d.ts +212 -0
- package/dist/utils/errorCodes.js +53 -0
- package/dist/vendor/electron/domain/note-templates.d.ts +3 -0
- package/dist/vendor/electron/domain/note-templates.js +232 -0
- package/dist/vendor/electron/domain/properties.d.ts +10 -0
- package/dist/vendor/electron/domain/properties.js +1549 -0
- package/dist/vendor/electron/domain/saved-view-analytics.d.ts +45 -0
- package/dist/vendor/electron/domain/saved-view-analytics.js +365 -0
- package/dist/vendor/electron/domain/saved-view-relative-date.d.ts +21 -0
- package/dist/vendor/electron/domain/saved-view-relative-date.js +153 -0
- package/dist/vendor/electron/domain/saved-views.d.ts +61 -0
- package/dist/vendor/electron/domain/saved-views.js +1120 -0
- package/dist/vendor/electron/rpc.d.ts +22 -0
- package/dist/vendor/electron/rpc.js +27 -0
- package/dist/vendor/electron/shared/ai-errors.d.ts +11 -0
- package/dist/vendor/electron/shared/ai-errors.js +107 -0
- package/dist/vendor/electron/shared/external-types.d.ts +20 -0
- package/dist/vendor/electron/shared/external-types.js +1 -0
- package/dist/vendor/electron/shared/index.d.ts +9 -0
- package/dist/vendor/electron/shared/index.js +7 -0
- package/dist/vendor/electron/shared/note-properties.d.ts +328 -0
- package/dist/vendor/electron/shared/note-properties.js +1027 -0
- package/dist/vendor/electron/shared/note-templates.d.ts +82 -0
- package/dist/vendor/electron/shared/note-templates.js +84 -0
- package/dist/vendor/electron/shared/powersync-read-models.d.ts +1716 -0
- package/dist/vendor/electron/shared/powersync-read-models.js +1 -0
- package/dist/vendor/electron/shared/remote-device-agent.d.ts +373 -0
- package/dist/vendor/electron/shared/remote-device-agent.js +191 -0
- package/dist/vendor/electron/shared/saved-views.d.ts +394 -0
- package/dist/vendor/electron/shared/saved-views.js +981 -0
- package/dist/vendor/electron/sql-utils.d.ts +5 -0
- package/dist/vendor/electron/sql-utils.js +32 -0
- package/package.json +10 -4
- package/dist/commands/savedViews/semantics.d.ts +0 -46
- 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
|
+
}
|