kintone-migrator 0.35.3 → 0.35.5

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/dist/index.mjs CHANGED
@@ -795,7 +795,7 @@ function compareActions$1(local, remote) {
795
795
  const diffs = [];
796
796
  if (local.index !== remote.index) diffs.push(`index: ${remote.index} -> ${local.index}`);
797
797
  if (!deepEqual(local.destApp, remote.destApp)) diffs.push(`destApp: ${formatValue(remote.destApp)} -> ${formatValue(local.destApp)}`);
798
- if (local.filterCond !== remote.filterCond) diffs.push(`filterCond: "${remote.filterCond}" -> "${local.filterCond}"`);
798
+ if (local.filterCond !== remote.filterCond) diffs.push(`filterCond: ${formatValue(remote.filterCond)} -> ${formatValue(local.filterCond)}`);
799
799
  if (!deepEqual(local.mappings, remote.mappings)) diffs.push(`mappings: ${formatValue(remote.mappings)} -> ${formatValue(local.mappings)}`);
800
800
  if (!deepEqual(local.entities, remote.entities)) diffs.push(`entities: ${formatValue(remote.entities)} -> ${formatValue(local.entities)}`);
801
801
  return diffs;
@@ -1354,6 +1354,17 @@ const SKIP_REVISION_CHECK = Symbol("skip-revision-check");
1354
1354
  /** The generation read when a caller does not specify a target. */
1355
1355
  const DEFAULT_FORM_READ_TARGET = "preview";
1356
1356
  //#endregion
1357
+ //#region src/core/domain/formSchema/services/lookupListDecoder.ts
1358
+ /**
1359
+ * Reads a lookup list (`fieldMappings`, `lookupPickerFields`) from an external
1360
+ * value. kintone represents an unset list as `""`, which reads as the empty
1361
+ * list. Returns `undefined` when the value is not a list.
1362
+ */
1363
+ function decodeLookupList(value) {
1364
+ if (value === "") return [];
1365
+ return Array.isArray(value) ? value : void 0;
1366
+ }
1367
+ //#endregion
1357
1368
  //#region src/lib/charValidation.ts
1358
1369
  /** Returns true if the string contains any control characters (0x00–0x1f or 0x7f). */
1359
1370
  function hasControlChars(s) {
@@ -1375,6 +1386,16 @@ function sanitizeForDisplay$1(s) {
1375
1386
  }
1376
1387
  //#endregion
1377
1388
  //#region src/core/domain/formSchema/valueObject.ts
1389
+ const LookupRelatedApp = {
1390
+ /**
1391
+ * kintone returns `code: ""` for a related app without an app code, while a
1392
+ * schema leaves `code` out for the same app. Both build a related app
1393
+ * without `code`, so only a non-empty `code` is kept.
1394
+ */
1395
+ create: (app, code) => code === void 0 || code === "" ? { app } : {
1396
+ app,
1397
+ code
1398
+ } };
1378
1399
  function hasInvalidFieldCodeChars(code) {
1379
1400
  if (hasControlChars(code)) return true;
1380
1401
  for (let i = 0; i < code.length; i++) {
@@ -1468,6 +1489,42 @@ function normalizeDefaultValue(type, rest) {
1468
1489
  if (rest.defaultValue !== void 0 && typeof rest.defaultValue === "string") rest.defaultValue = rest.defaultValue === "" ? [] : [rest.defaultValue];
1469
1490
  }
1470
1491
  }
1492
+ function readKintoneLookupList(value, fieldPath) {
1493
+ const list = decodeLookupList(value);
1494
+ if (list === void 0) throw new SystemError(SystemErrorCode.ExternalApiError, `Expected array at ${fieldPath}, got ${typeof value}`);
1495
+ return list;
1496
+ }
1497
+ function fromKintoneLookup(value, fieldPath) {
1498
+ assertRecord(value, fieldPath);
1499
+ assertRecord(value.relatedApp, `${fieldPath}.relatedApp`);
1500
+ const { app, code } = value.relatedApp;
1501
+ assertString(app, `${fieldPath}.relatedApp.app`);
1502
+ if (code !== void 0) assertString(code, `${fieldPath}.relatedApp.code`);
1503
+ assertString(value.relatedKeyField, `${fieldPath}.relatedKeyField`);
1504
+ const fieldMappings = readKintoneLookupList(value.fieldMappings, `${fieldPath}.fieldMappings`).map((mapping, i) => {
1505
+ const mappingPath = `${fieldPath}.fieldMappings[${i}]`;
1506
+ assertRecord(mapping, mappingPath);
1507
+ assertString(mapping.field, `${mappingPath}.field`);
1508
+ assertString(mapping.relatedField, `${mappingPath}.relatedField`);
1509
+ return {
1510
+ field: mapping.field,
1511
+ relatedField: mapping.relatedField
1512
+ };
1513
+ });
1514
+ const lookupPickerFields = readKintoneLookupList(value.lookupPickerFields, `${fieldPath}.lookupPickerFields`);
1515
+ assertStringArray(lookupPickerFields, `${fieldPath}.lookupPickerFields`);
1516
+ const { filterCond, sort } = value;
1517
+ if (filterCond !== void 0) assertString(filterCond, `${fieldPath}.filterCond`);
1518
+ if (sort !== void 0) assertString(sort, `${fieldPath}.sort`);
1519
+ return {
1520
+ relatedApp: LookupRelatedApp.create(app, code),
1521
+ relatedKeyField: value.relatedKeyField,
1522
+ fieldMappings,
1523
+ lookupPickerFields,
1524
+ ...filterCond !== void 0 ? { filterCond } : {},
1525
+ ...sort !== void 0 ? { sort } : {}
1526
+ };
1527
+ }
1471
1528
  function fromKintoneProperty(prop) {
1472
1529
  const { type, code, label, noLabel, ...rest } = prop;
1473
1530
  const base = {
@@ -1518,10 +1575,15 @@ function fromKintoneProperty(prop) {
1518
1575
  }
1519
1576
  if (!KNOWN_FIELD_TYPES.has(type)) throw new SystemError(SystemErrorCode.ExternalApiError, `Unknown field type: ${type}`);
1520
1577
  normalizeDefaultValue(type, rest);
1578
+ const { lookup, ...others } = rest;
1579
+ const properties = lookup === void 0 ? others : {
1580
+ ...others,
1581
+ lookup: fromKintoneLookup(lookup, `${type} "${code}".lookup`)
1582
+ };
1521
1583
  return {
1522
1584
  ...base,
1523
1585
  type,
1524
- properties: rest
1586
+ properties
1525
1587
  };
1526
1588
  }
1527
1589
  function toKintoneProperty(field) {
@@ -4217,16 +4279,17 @@ async function applyAdminNotes({ container }) {
4217
4279
  //#endregion
4218
4280
  //#region src/core/domain/adminNotes/services/diffDetector.ts
4219
4281
  const CONTENT_TRUNCATE_LENGTH = 30;
4220
- function truncate(text, maxLength) {
4221
- if (text.length <= maxLength) return text;
4222
- return `${text.slice(0, maxLength)}...`;
4282
+ function formatContentPreview(text, maxLength) {
4283
+ const codePoints = Array.from(text);
4284
+ if (codePoints.length <= maxLength) return formatValue(text);
4285
+ return `${formatValue(codePoints.slice(0, maxLength).join(""))}...`;
4223
4286
  }
4224
4287
  function compareConfigs$2(local, remote) {
4225
4288
  const entries = [];
4226
4289
  if (local.content !== remote.content) entries.push({
4227
4290
  type: "modified",
4228
4291
  field: "content",
4229
- details: `"${truncate(remote.content, CONTENT_TRUNCATE_LENGTH)}" -> "${truncate(local.content, CONTENT_TRUNCATE_LENGTH)}"`
4292
+ details: `${formatContentPreview(remote.content, CONTENT_TRUNCATE_LENGTH)} -> ${formatContentPreview(local.content, CONTENT_TRUNCATE_LENGTH)}`
4230
4293
  });
4231
4294
  if (local.includeInTemplateAndDuplicates !== remote.includeInTemplateAndDuplicates) entries.push({
4232
4295
  type: "modified",
@@ -5923,6 +5986,9 @@ function validateLookupStructure(field) {
5923
5986
  const issues = [];
5924
5987
  if (lookup.relatedApp.app.trim().length === 0) issues.push(issue("error", field.code, field.type, "INVALID_LOOKUP", `Field "${field.code}" lookup must have a non-empty "relatedApp.app"`));
5925
5988
  if (lookup.relatedKeyField.trim().length === 0) issues.push(issue("error", field.code, field.type, "INVALID_LOOKUP", `Field "${field.code}" lookup must have a non-empty "relatedKeyField"`));
5989
+ lookup.fieldMappings.forEach((mapping, i) => {
5990
+ for (const key of ["field", "relatedField"]) if (mapping[key].trim().length === 0) issues.push(issue("error", field.code, field.type, "INVALID_LOOKUP", `Field "${field.code}" lookup must have a non-empty "fieldMappings[${i}].${key}"`));
5991
+ });
5926
5992
  return issues;
5927
5993
  }
5928
5994
  function validateReferenceTableRelatedApp(field) {
@@ -6205,6 +6271,42 @@ function buildFieldDefinition(base, fieldType, properties) {
6205
6271
  };
6206
6272
  }
6207
6273
  }
6274
+ function invalidLookup(code, detail) {
6275
+ return new BusinessRuleError(FormSchemaErrorCode.FsInvalidSchemaStructure, `Field "${code}" lookup ${detail}`);
6276
+ }
6277
+ function optionalString(value) {
6278
+ return value === void 0 || value === null ? void 0 : String(value);
6279
+ }
6280
+ /** Reads a lookup list; an omitted or valueless list is the empty list. */
6281
+ function optionalLookupList(code, key, value) {
6282
+ if (value === void 0 || value === null) return [];
6283
+ const list = decodeLookupList(value);
6284
+ if (list === void 0) throw invalidLookup(code, `must have a "${key}" array`);
6285
+ return list;
6286
+ }
6287
+ function parseLookup(code, raw) {
6288
+ if (!isRecord(raw)) throw invalidLookup(code, "must be an object");
6289
+ if (!isRecord(raw.relatedApp)) throw invalidLookup(code, "must have a \"relatedApp\" object");
6290
+ const rawFieldMappings = optionalLookupList(code, "fieldMappings", raw.fieldMappings);
6291
+ const lookupPickerFields = optionalLookupList(code, "lookupPickerFields", raw.lookupPickerFields).map((f) => String(f));
6292
+ const fieldMappings = rawFieldMappings.map((mapping) => {
6293
+ if (!isRecord(mapping)) throw invalidLookup(code, "must have objects in \"fieldMappings\"");
6294
+ return {
6295
+ field: String(mapping.field ?? ""),
6296
+ relatedField: String(mapping.relatedField ?? "")
6297
+ };
6298
+ });
6299
+ const filterCond = optionalString(raw.filterCond);
6300
+ const sort = optionalString(raw.sort);
6301
+ return {
6302
+ relatedApp: LookupRelatedApp.create(String(raw.relatedApp.app ?? ""), optionalString(raw.relatedApp.code)),
6303
+ relatedKeyField: String(raw.relatedKeyField ?? ""),
6304
+ fieldMappings,
6305
+ lookupPickerFields,
6306
+ ...filterCond !== void 0 ? { filterCond } : {},
6307
+ ...sort !== void 0 ? { sort } : {}
6308
+ };
6309
+ }
6208
6310
  function parseReferenceTableField(raw, code, base) {
6209
6311
  if (!isRecord(raw.referenceTable)) throw new BusinessRuleError(FormSchemaErrorCode.FsInvalidSchemaStructure, `Field "${code}" of type REFERENCE_TABLE must have a "referenceTable" property`);
6210
6312
  const refTable = raw.referenceTable;
@@ -6259,7 +6361,11 @@ function parseFieldDefinitionFromFlat(raw) {
6259
6361
  };
6260
6362
  }
6261
6363
  if (fieldType === "REFERENCE_TABLE") return parseReferenceTableField(raw, code, base);
6262
- const properties = extractProperties(raw);
6364
+ const { lookup, ...others } = extractProperties(raw);
6365
+ const properties = lookup === void 0 ? others : {
6366
+ ...others,
6367
+ lookup: parseLookup(code, lookup)
6368
+ };
6263
6369
  validateFieldProperties(code, fieldType, properties);
6264
6370
  return buildFieldDefinition(base, fieldType, properties);
6265
6371
  }
@@ -6599,6 +6705,13 @@ const VALID_ENTITY_TYPES$5 = /* @__PURE__ */ new Set([
6599
6705
  function isNotificationEntityType(value) {
6600
6706
  return VALID_ENTITY_TYPES$5.has(value);
6601
6707
  }
6708
+ /**
6709
+ * Normalizes a reminder timezone read at a boundary (kintone API or config
6710
+ * file) to the domain form: `null` and `""` mean unset (`undefined`).
6711
+ */
6712
+ function normalizeReminderTimezone(value) {
6713
+ return value === null || value === "" ? void 0 : value;
6714
+ }
6602
6715
  //#endregion
6603
6716
  //#region src/core/domain/notification/services/configParser.ts
6604
6717
  const REMINDER_OFFSET_MIN = -1e4;
@@ -6694,10 +6807,13 @@ function parseReminderConfig(raw) {
6694
6807
  if (!isRecord(raw)) throw new BusinessRuleError(NotificationErrorCode.NtInvalidConfigStructure, "\"reminder\" must be an object");
6695
6808
  if (!Array.isArray(raw.notifications)) throw new BusinessRuleError(NotificationErrorCode.NtInvalidConfigStructure, "\"reminder\" must have a \"notifications\" array");
6696
6809
  const notifications = raw.notifications.map((item, i) => parseReminderNotification(item, i));
6697
- return {
6698
- timezone: typeof raw.timezone === "string" ? raw.timezone : "Asia/Tokyo",
6810
+ const rawTimezone = raw.timezone ?? null;
6811
+ if (rawTimezone !== null && typeof rawTimezone !== "string") throw new BusinessRuleError(NotificationErrorCode.NtInvalidConfigStructure, `"reminder" has non-string "timezone": ${String(rawTimezone)}`);
6812
+ const timezone = normalizeReminderTimezone(rawTimezone);
6813
+ return timezone !== void 0 ? {
6814
+ timezone,
6699
6815
  notifications
6700
- };
6816
+ } : { notifications };
6701
6817
  }
6702
6818
  const NotificationConfigParser = { parse: (parsed) => {
6703
6819
  const obj = validateParsedConfig(parsed, NotificationErrorCode.NtInvalidConfigStructure, "Notification");
@@ -6746,8 +6862,7 @@ async function applyNotification({ container }) {
6746
6862
  if (config.reminder !== void 0) {
6747
6863
  const currentReminder = await container.notificationConfigurator.getReminderNotifications();
6748
6864
  await container.notificationConfigurator.updateReminderNotifications({
6749
- timezone: config.reminder.timezone,
6750
- notifications: config.reminder.notifications,
6865
+ ...config.reminder,
6751
6866
  revision: currentReminder.revision
6752
6867
  });
6753
6868
  }
@@ -8245,8 +8360,11 @@ var KintoneNotificationConfigurator = class {
8245
8360
  preview: true
8246
8361
  });
8247
8362
  const notifications = response.notifications.map(fromKintoneReminderNotification);
8363
+ const rawTimezone = response.timezone ?? null;
8364
+ if (rawTimezone !== null && typeof rawTimezone !== "string") throw new SystemError(SystemErrorCode.ExternalApiError, `Unexpected reminder timezone from kintone API: ${String(rawTimezone)}`);
8365
+ const timezone = normalizeReminderTimezone(rawTimezone);
8248
8366
  return {
8249
- timezone: response.timezone,
8367
+ ...timezone !== void 0 ? { timezone } : {},
8250
8368
  notifications,
8251
8369
  revision: response.revision
8252
8370
  };
@@ -8258,9 +8376,9 @@ var KintoneNotificationConfigurator = class {
8258
8376
  try {
8259
8377
  const requestParams = {
8260
8378
  app: this.appId,
8261
- timezone: params.timezone,
8262
8379
  notifications: params.notifications.map(toKintoneReminderNotification)
8263
8380
  };
8381
+ if (params.timezone !== void 0) requestParams.timezone = params.timezone;
8264
8382
  if (params.revision !== void 0) requestParams.revision = params.revision;
8265
8383
  return { revision: (await this.client.app.updateReminderNotifications(requestParams)).revision };
8266
8384
  } catch (error) {
@@ -9294,7 +9412,7 @@ function compareConfigs$1(local, remote) {
9294
9412
  if (lv !== rv) entries.push({
9295
9413
  type: "modified",
9296
9414
  field,
9297
- details: suppressValues ? `${field} changed` : `"${rv}" -> "${lv}"`
9415
+ details: suppressValues ? `${field} changed` : `${formatValue(rv)} -> ${formatValue(lv)}`
9298
9416
  });
9299
9417
  }
9300
9418
  function compareBoolean(field, l, r, defaultValue) {
@@ -9475,17 +9593,17 @@ function perRecordLabel(notif) {
9475
9593
  }
9476
9594
  function describePerRecordChanges(local, remote) {
9477
9595
  const diffs = [];
9478
- if (local.title !== remote.title) diffs.push(`title: "${remote.title}" -> "${local.title}"`);
9596
+ if (local.title !== remote.title) diffs.push(`title: ${formatValue(remote.title)} -> ${formatValue(local.title)}`);
9479
9597
  if (!deepEqual(local.targets, remote.targets)) diffs.push(`targets: ${formatValue(remote.targets)} -> ${formatValue(local.targets)}`);
9480
9598
  return diffs.length > 0 ? diffs.join("\n") : "changed";
9481
9599
  }
9482
9600
  function describeReminderChanges(local, remote) {
9483
9601
  const diffs = [];
9484
- if (local.title !== remote.title) diffs.push(`title: "${remote.title}" -> "${local.title}"`);
9602
+ if (local.title !== remote.title) diffs.push(`title: ${formatValue(remote.title)} -> ${formatValue(local.title)}`);
9485
9603
  if (local.daysLater !== remote.daysLater) diffs.push(`daysLater: ${remote.daysLater} -> ${local.daysLater}`);
9486
9604
  if ((local.hoursLater ?? 0) !== (remote.hoursLater ?? 0)) diffs.push(`hoursLater: ${remote.hoursLater ?? 0} -> ${local.hoursLater ?? 0}`);
9487
- if ((local.time ?? "") !== (remote.time ?? "")) diffs.push(`time: "${remote.time ?? ""}" -> "${local.time ?? ""}"`);
9488
- if (local.filterCond !== remote.filterCond) diffs.push(`filterCond: "${remote.filterCond}" -> "${local.filterCond}"`);
9605
+ if ((local.time ?? "") !== (remote.time ?? "")) diffs.push(`time: ${formatValue(remote.time ?? "")} -> ${formatValue(local.time ?? "")}`);
9606
+ if (local.filterCond !== remote.filterCond) diffs.push(`filterCond: ${formatValue(remote.filterCond)} -> ${formatValue(local.filterCond)}`);
9489
9607
  if (!deepEqual(local.targets, remote.targets)) diffs.push(`targets: ${formatValue(remote.targets)} -> ${formatValue(local.targets)}`);
9490
9608
  return diffs.length > 0 ? diffs.join("\n") : "changed";
9491
9609
  }
@@ -9527,7 +9645,7 @@ function compareReminderSection(local, remote) {
9527
9645
  type: "added",
9528
9646
  section: "reminder",
9529
9647
  name: code,
9530
- details: `"${localNotif.title}"`
9648
+ details: formatValue(localNotif.title)
9531
9649
  });
9532
9650
  else if (!deepEqual(localNotif, remoteNotif)) entries.push({
9533
9651
  type: "modified",
@@ -9564,11 +9682,11 @@ const NotificationDiffDetector = { detect: (local, remote) => {
9564
9682
  entries.push(...comparePerRecordSection(localPerRecord, remotePerRecord));
9565
9683
  if (local.reminder && remote.reminder) {
9566
9684
  entries.push(...compareReminderSection(local.reminder.notifications, remote.reminder.notifications));
9567
- if (local.reminder.timezone !== remote.reminder.timezone) entries.push({
9685
+ if (local.reminder.timezone !== void 0 && local.reminder.timezone !== remote.reminder.timezone) entries.push({
9568
9686
  type: "modified",
9569
9687
  section: "reminder",
9570
9688
  name: "timezone",
9571
- details: `${remote.reminder.timezone} -> ${local.reminder.timezone}`
9689
+ details: `${remote.reminder.timezone ?? "(unset)"} -> ${local.reminder.timezone}`
9572
9690
  });
9573
9691
  } else if (local.reminder && !remote.reminder) entries.push({
9574
9692
  type: "added",
@@ -9585,28 +9703,46 @@ const NotificationDiffDetector = { detect: (local, remote) => {
9585
9703
  return buildDiffResult(entries);
9586
9704
  } };
9587
9705
  //#endregion
9588
- //#region src/core/application/notification/detectNotificationDiff.ts
9589
- async function detectNotificationDiff({ container }) {
9590
- const [storageResult, generalResult, perRecordResult, reminderResult] = await Promise.all([
9591
- container.notificationStorage.get(),
9706
+ //#region src/core/application/notification/loadNotificationRemote.ts
9707
+ /**
9708
+ * Fetches the three remote sub-configs (general / perRecord / reminder) and
9709
+ * bundles them into a single {@link NotificationRemote}.
9710
+ *
9711
+ * The three getters each carry the app (preview) revision; since the revision is
9712
+ * app-scoped and the three are fetched at (essentially) the same moment, the
9713
+ * general getter's revision is used as the observed remote revision for the
9714
+ * TOCTOU guard. kintone always returns all three sections, so the bundled config
9715
+ * always has `general` / `perRecord` / `reminder` defined (an unset reminder
9716
+ * timezone stays absent).
9717
+ */
9718
+ async function loadNotificationRemote(container) {
9719
+ const [general, perRecord, reminder] = await Promise.all([
9592
9720
  container.notificationConfigurator.getGeneralNotifications(),
9593
9721
  container.notificationConfigurator.getPerRecordNotifications(),
9594
9722
  container.notificationConfigurator.getReminderNotifications()
9595
9723
  ]);
9596
- if (!storageResult.exists) throw new ValidationError(ValidationErrorCode.InvalidInput, "Notification config file not found");
9597
- const localConfig = parseNotificationConfigText(container.configCodec, storageResult.content);
9598
- const remoteConfig = {
9599
- general: {
9600
- notifyToCommenter: generalResult.notifyToCommenter,
9601
- notifications: generalResult.notifications
9724
+ return {
9725
+ config: {
9726
+ general: {
9727
+ notifyToCommenter: general.notifyToCommenter,
9728
+ notifications: general.notifications
9729
+ },
9730
+ perRecord: perRecord.notifications,
9731
+ reminder: {
9732
+ ...reminder.timezone !== void 0 ? { timezone: reminder.timezone } : {},
9733
+ notifications: reminder.notifications
9734
+ }
9602
9735
  },
9603
- perRecord: perRecordResult.notifications,
9604
- reminder: {
9605
- timezone: reminderResult.timezone,
9606
- notifications: reminderResult.notifications
9607
- }
9736
+ revision: general.revision
9608
9737
  };
9609
- return NotificationDiffDetector.detect(localConfig, remoteConfig);
9738
+ }
9739
+ //#endregion
9740
+ //#region src/core/application/notification/detectNotificationDiff.ts
9741
+ async function detectNotificationDiff({ container }) {
9742
+ const [storageResult, remote] = await Promise.all([container.notificationStorage.get(), loadNotificationRemote(container)]);
9743
+ if (!storageResult.exists) throw new ValidationError(ValidationErrorCode.InvalidInput, "Notification config file not found");
9744
+ const localConfig = parseNotificationConfigText(container.configCodec, storageResult.content);
9745
+ return NotificationDiffDetector.detect(localConfig, remote.config);
9610
9746
  }
9611
9747
  //#endregion
9612
9748
  //#region src/core/domain/plugin/services/diffDetector.ts
@@ -9619,11 +9755,11 @@ const PluginDiffDetector = { detect: (local, remote) => {
9619
9755
  if (!remotePlugin) entries.push({
9620
9756
  type: "added",
9621
9757
  pluginId: id,
9622
- details: `"${localPlugin.name}"`
9758
+ details: formatValue(localPlugin.name)
9623
9759
  });
9624
9760
  else {
9625
9761
  const diffs = [];
9626
- if (localPlugin.name !== remotePlugin.name) diffs.push(`name: "${remotePlugin.name}" -> "${localPlugin.name}"`);
9762
+ if (localPlugin.name !== remotePlugin.name) diffs.push(`name: ${formatValue(remotePlugin.name)} -> ${formatValue(localPlugin.name)}`);
9627
9763
  if (localPlugin.enabled !== remotePlugin.enabled) diffs.push(`enabled: ${String(remotePlugin.enabled)} -> ${String(localPlugin.enabled)}`);
9628
9764
  if (diffs.length > 0) entries.push({
9629
9765
  type: "modified",
@@ -9635,7 +9771,7 @@ const PluginDiffDetector = { detect: (local, remote) => {
9635
9771
  for (const [id, remotePlugin] of remoteMap) if (!localMap.has(id)) entries.push({
9636
9772
  type: "deleted",
9637
9773
  pluginId: id,
9638
- details: `"${remotePlugin.name}"`
9774
+ details: formatValue(remotePlugin.name)
9639
9775
  });
9640
9776
  return buildDiffResult(entries);
9641
9777
  } };
@@ -9670,7 +9806,7 @@ function isExecutableUserEqual(a, b) {
9670
9806
  }
9671
9807
  function compareActions(localAction, remoteAction) {
9672
9808
  const diffs = [];
9673
- if (localAction.filterCond !== remoteAction.filterCond) diffs.push(`filterCond: "${remoteAction.filterCond}" -> "${localAction.filterCond}"`);
9809
+ if (localAction.filterCond !== remoteAction.filterCond) diffs.push(`filterCond: ${formatValue(remoteAction.filterCond)} -> ${formatValue(localAction.filterCond)}`);
9674
9810
  if (localAction.type !== remoteAction.type) diffs.push(`type: ${remoteAction.type} -> ${localAction.type}`);
9675
9811
  if (!isExecutableUserEqual(localAction.executableUser, remoteAction.executableUser)) diffs.push(`executableUser: ${formatValue(remoteAction.executableUser)} -> ${formatValue(localAction.executableUser)}`);
9676
9812
  return diffs;
@@ -9729,7 +9865,7 @@ function compareActionEntries(local, remote, entries) {
9729
9865
  type: "added",
9730
9866
  category: "action",
9731
9867
  name: actionLabel(pair.local, pair.index),
9732
- details: `filterCond: "${pair.local.filterCond}"`
9868
+ details: `filterCond: ${formatValue(pair.local.filterCond)}`
9733
9869
  });
9734
9870
  break;
9735
9871
  case "remoteOnly":
@@ -9737,7 +9873,7 @@ function compareActionEntries(local, remote, entries) {
9737
9873
  type: "deleted",
9738
9874
  category: "action",
9739
9875
  name: actionLabel(pair.remote, pair.index),
9740
- details: `filterCond: "${pair.remote.filterCond}"`
9876
+ details: `filterCond: ${formatValue(pair.remote.filterCond)}`
9741
9877
  });
9742
9878
  break;
9743
9879
  case "both": {
@@ -9854,11 +9990,11 @@ async function detectRecordPermissionDiff({ container }) {
9854
9990
  //#region src/core/domain/report/services/diffDetector.ts
9855
9991
  function compareReports(local, remote) {
9856
9992
  const diffs = [];
9857
- if (local.name !== remote.name) diffs.push(`name: "${remote.name}" -> "${local.name}"`);
9993
+ if (local.name !== remote.name) diffs.push(`name: ${formatValue(remote.name)} -> ${formatValue(local.name)}`);
9858
9994
  if (local.chartType !== remote.chartType) diffs.push(`chartType: ${remote.chartType} -> ${local.chartType}`);
9859
9995
  if ((local.chartMode ?? "") !== (remote.chartMode ?? "")) diffs.push(`chartMode: ${remote.chartMode ?? "(unset)"} -> ${local.chartMode ?? "(unset)"}`);
9860
9996
  if (local.index !== remote.index) diffs.push(`index: ${remote.index} -> ${local.index}`);
9861
- if (local.filterCond !== remote.filterCond) diffs.push(`filterCond: "${remote.filterCond}" -> "${local.filterCond}"`);
9997
+ if (local.filterCond !== remote.filterCond) diffs.push(`filterCond: ${formatValue(remote.filterCond)} -> ${formatValue(local.filterCond)}`);
9862
9998
  if (!deepEqual(local.groups, remote.groups)) diffs.push(`groups: ${formatValue(remote.groups)} -> ${formatValue(local.groups)}`);
9863
9999
  if (!deepEqual(local.aggregations, remote.aggregations)) diffs.push(`aggregations: ${formatValue(remote.aggregations)} -> ${formatValue(local.aggregations)}`);
9864
10000
  if (!deepEqual(local.sorts, remote.sorts)) diffs.push(`sorts: ${formatValue(remote.sorts)} -> ${formatValue(local.sorts)}`);
@@ -9901,7 +10037,7 @@ async function detectReportDiff({ container }) {
9901
10037
  //#endregion
9902
10038
  //#region src/core/domain/view/services/diffDetector.ts
9903
10039
  function checkOptionalStringChange(changes, field, localVal, remoteVal) {
9904
- if ((localVal ?? "") !== (remoteVal ?? "")) changes.push(`${field}: "${remoteVal ?? ""}" -> "${localVal ?? ""}"`);
10040
+ if ((localVal ?? "") !== (remoteVal ?? "")) changes.push(`${field}: ${formatValue(remoteVal ?? "")} -> ${formatValue(localVal ?? "")}`);
9905
10041
  }
9906
10042
  function describeChanges(local, remote) {
9907
10043
  const changes = [];
@@ -11209,7 +11345,7 @@ const NotificationConfigSerializer = { serialize: (config) => {
11209
11345
  };
11210
11346
  if (config.perRecord !== void 0) serialized.perRecord = config.perRecord.map(serializePerRecordNotification);
11211
11347
  if (config.reminder !== void 0) serialized.reminder = {
11212
- timezone: config.reminder.timezone,
11348
+ ...config.reminder.timezone !== void 0 ? { timezone: config.reminder.timezone } : {},
11213
11349
  notifications: config.reminder.notifications.map(serializeReminderNotification)
11214
11350
  };
11215
11351
  return serialized;
@@ -11217,22 +11353,7 @@ const NotificationConfigSerializer = { serialize: (config) => {
11217
11353
  //#endregion
11218
11354
  //#region src/core/application/notification/captureNotification.ts
11219
11355
  async function captureNotification({ container }) {
11220
- const [general, perRecord, reminder] = await Promise.all([
11221
- container.notificationConfigurator.getGeneralNotifications(),
11222
- container.notificationConfigurator.getPerRecordNotifications(),
11223
- container.notificationConfigurator.getReminderNotifications()
11224
- ]);
11225
- const config = {
11226
- general: {
11227
- notifyToCommenter: general.notifyToCommenter,
11228
- notifications: general.notifications
11229
- },
11230
- perRecord: perRecord.notifications,
11231
- reminder: {
11232
- timezone: reminder.timezone,
11233
- notifications: reminder.notifications
11234
- }
11235
- };
11356
+ const { config } = await loadNotificationRemote(container);
11236
11357
  return {
11237
11358
  configText: stringifyConfig(container.configCodec, NotificationConfigSerializer.serialize(config)),
11238
11359
  hasExistingConfig: (await container.notificationStorage.get()).exists
@@ -13800,7 +13921,8 @@ var capture_default$8 = createCaptureCommand({
13800
13921
  * conflict-resolution namespace:
13801
13922
  *
13802
13923
  * - **reminder** = `code` stable key (`compareReminderSection`, diffDetector
13803
- * reminder map). Plus the `timezone` scalar as a single whole-entity 3-way.
13924
+ * reminder map). Plus the `timezone` scalar as a single whole-entity 3-way
13925
+ * when the local config manages it (sets a timezone).
13804
13926
  * - **perRecord** = group by `filterCond`, then within a group match by index
13805
13927
  * (`filterCond#i` composite key — `comparePerRecordSection`). `perRecordLabel`
13806
13928
  * is display-only and is NOT used as a match key.
@@ -13863,7 +13985,7 @@ function computeNotificationThreeWayMerge(base, local, remote) {
13863
13985
  const perRecord = classifyThreeWay(buildPerRecordMap(base.perRecord), buildPerRecordMap(local.perRecord), buildPerRecordMap(remote.perRecord), isPerRecordEqual);
13864
13986
  const reminder = classifyThreeWay(buildReminderMap(base.reminder), buildReminderMap(local.reminder), buildReminderMap(remote.reminder), isReminderEqual);
13865
13987
  const generalScalar = computeSingleThreeWayMerge(base.general?.notifyToCommenter ?? false, local.general?.notifyToCommenter ?? false, remote.general?.notifyToCommenter ?? false, (a, b) => a === b);
13866
- const reminderTimezone = computeSingleThreeWayMerge(base.reminder?.timezone ?? "", local.reminder?.timezone ?? "", remote.reminder?.timezone ?? "", (a, b) => a === b);
13988
+ const reminderTimezone = local.reminder?.timezone !== void 0 ? computeSingleThreeWayMerge(base.reminder?.timezone ?? null, local.reminder.timezone, remote.reminder?.timezone ?? null, (a, b) => a === b) : void 0;
13867
13989
  const entries = [
13868
13990
  ...general.entries,
13869
13991
  ...perRecord.entries,
@@ -13874,12 +13996,12 @@ function computeNotificationThreeWayMerge(base, local, remote) {
13874
13996
  ...perRecord.conflicts.map((e) => ({ key: e.key })),
13875
13997
  ...reminder.conflicts.map((e) => ({ key: e.key })),
13876
13998
  ...scalarConflictKey(generalScalar, GENERAL_SCALAR_KEY),
13877
- ...scalarConflictKey(reminderTimezone, REMINDER_TIMEZONE_KEY)
13999
+ ...reminderTimezone !== void 0 ? scalarConflictKey(reminderTimezone, REMINDER_TIMEZONE_KEY) : []
13878
14000
  ];
13879
14001
  return {
13880
14002
  entries,
13881
14003
  generalScalar,
13882
- reminderTimezone,
14004
+ ...reminderTimezone !== void 0 ? { reminderTimezone } : {},
13883
14005
  conflicts,
13884
14006
  hasConflict: conflicts.length > 0
13885
14007
  };
@@ -13929,7 +14051,7 @@ function collectReminderNotifications(entries, resolution) {
13929
14051
  */
13930
14052
  function resolveNotificationMerge(merge, resolution) {
13931
14053
  const notifyToCommenter = resolveSingleMerge(merge.generalScalar, resolution.get(GENERAL_SCALAR_KEY), NotificationErrorCode.NtUnresolvedConflict);
13932
- const timezone = resolveSingleMerge(merge.reminderTimezone, resolution.get(REMINDER_TIMEZONE_KEY), NotificationErrorCode.NtUnresolvedConflict);
14054
+ const timezone = merge.reminderTimezone !== void 0 ? resolveSingleMerge(merge.reminderTimezone, resolution.get(REMINDER_TIMEZONE_KEY), NotificationErrorCode.NtUnresolvedConflict) : null;
13933
14055
  return {
13934
14056
  general: {
13935
14057
  notifyToCommenter,
@@ -13937,7 +14059,7 @@ function resolveNotificationMerge(merge, resolution) {
13937
14059
  },
13938
14060
  perRecord: collectPerRecordNotifications(merge.entries, resolution),
13939
14061
  reminder: {
13940
- timezone,
14062
+ ...timezone !== null ? { timezone } : {},
13941
14063
  notifications: collectReminderNotifications(merge.entries, resolution)
13942
14064
  }
13943
14065
  };
@@ -13956,37 +14078,6 @@ const NotificationStateParser = { parse: (parsed) => ({ config: NotificationConf
13956
14078
  //#endregion
13957
14079
  //#region src/core/application/notification/loadNotificationThreeWayInputs.ts
13958
14080
  /**
13959
- * Fetches the three remote sub-configs (general / perRecord / reminder) and
13960
- * bundles them into a single {@link NotificationRemote}.
13961
- *
13962
- * The three getters each carry the app (preview) revision; since the revision is
13963
- * app-scoped and the three are fetched at (essentially) the same moment, the
13964
- * general getter's revision is used as the observed remote revision for the
13965
- * TOCTOU guard. kintone always returns all three sections, so the bundled config
13966
- * always has `general` / `perRecord` / `reminder` defined.
13967
- */
13968
- async function loadNotificationRemote(container) {
13969
- const [general, perRecord, reminder] = await Promise.all([
13970
- container.notificationConfigurator.getGeneralNotifications(),
13971
- container.notificationConfigurator.getPerRecordNotifications(),
13972
- container.notificationConfigurator.getReminderNotifications()
13973
- ]);
13974
- return {
13975
- config: {
13976
- general: {
13977
- notifyToCommenter: general.notifyToCommenter,
13978
- notifications: general.notifications
13979
- },
13980
- perRecord: perRecord.notifications,
13981
- reminder: {
13982
- timezone: reminder.timezone,
13983
- notifications: reminder.notifications
13984
- }
13985
- },
13986
- revision: general.revision
13987
- };
13988
- }
13989
- /**
13990
14081
  * Loads the four inputs of a 3-way notification sync (base snapshot, base app
13991
14082
  * revision, local YAML, remote config + revision) via the generic
13992
14083
  * {@link loadThreeWayInputs}. Notification bundles three sub-configs into one
@@ -14089,7 +14180,7 @@ function buildThreeWay(merge) {
14089
14180
  }
14090
14181
  }
14091
14182
  pushScalar(merge.generalScalar, "general:notifyToCommenter", "notifyToCommenter", localChanges, remoteDrift, conflicts, extras);
14092
- pushScalar(merge.reminderTimezone, "reminder:timezone", "timezone", localChanges, remoteDrift, conflicts, extras);
14183
+ if (merge.reminderTimezone !== void 0) pushScalar(merge.reminderTimezone, "reminder:timezone", "timezone", localChanges, remoteDrift, conflicts, extras);
14093
14184
  return {
14094
14185
  mode: "three-way",
14095
14186
  localChanges,
@@ -14312,7 +14403,7 @@ async function pushNotification({ container, input }) {
14312
14403
  return false;
14313
14404
  };
14314
14405
  const isDriftKind = (kind) => kind === "remoteOnly" || kind === "conflict";
14315
- if (merge.entries.some((e) => isManaged(e.key) && isDriftKind(e.change.kind)) || local.general !== void 0 && isDriftKind(merge.generalScalar.change.kind) || local.reminder !== void 0 && isDriftKind(merge.reminderTimezone.change.kind)) throw buildDriftConflict(NOTIFICATION_PULL_COMMAND);
14406
+ if (merge.entries.some((e) => isManaged(e.key) && isDriftKind(e.change.kind)) || local.general !== void 0 && isDriftKind(merge.generalScalar.change.kind) || merge.reminderTimezone !== void 0 && isDriftKind(merge.reminderTimezone.change.kind)) throw buildDriftConflict(NOTIFICATION_PULL_COMMAND);
14316
14407
  }
14317
14408
  const sendRevision = !(input.force || firstTime);
14318
14409
  let revision = remote.revision;
@@ -14338,11 +14429,14 @@ async function pushNotification({ container, input }) {
14338
14429
  }
14339
14430
  if (local.reminder !== void 0) {
14340
14431
  revision = (await container.notificationConfigurator.updateReminderNotifications({
14341
- timezone: local.reminder.timezone,
14342
- notifications: local.reminder.notifications,
14432
+ ...local.reminder,
14343
14433
  ...sendRevision ? { revision } : {}
14344
14434
  })).revision;
14345
- newBase.reminder = local.reminder;
14435
+ const timezone = local.reminder.timezone ?? remote.config.reminder?.timezone;
14436
+ newBase.reminder = {
14437
+ ...timezone !== void 0 ? { timezone } : {},
14438
+ notifications: local.reminder.notifications
14439
+ };
14346
14440
  }
14347
14441
  await saveNotificationSnapshotAndRevision(container, newBase, revision);
14348
14442
  return {