kintone-migrator 0.35.2 → 0.35.4

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
@@ -121,7 +121,6 @@ const ProcessManagementErrorCode = {
121
121
  PmInvalidEntityType: "PM_INVALID_ENTITY_TYPE",
122
122
  PmInvalidBooleanField: "PM_INVALID_BOOLEAN_FIELD",
123
123
  PmInvalidActionReference: "PM_INVALID_ACTION_REFERENCE",
124
- PmDuplicateActionName: "PM_DUPLICATE_ACTION_NAME",
125
124
  PmUnresolvedConflict: "PM_UNRESOLVED_CONFLICT"
126
125
  };
127
126
  //#endregion
@@ -6600,6 +6599,13 @@ const VALID_ENTITY_TYPES$5 = /* @__PURE__ */ new Set([
6600
6599
  function isNotificationEntityType(value) {
6601
6600
  return VALID_ENTITY_TYPES$5.has(value);
6602
6601
  }
6602
+ /**
6603
+ * Normalizes a reminder timezone read at a boundary (kintone API or config
6604
+ * file) to the domain form: `null` and `""` mean unset (`undefined`).
6605
+ */
6606
+ function normalizeReminderTimezone(value) {
6607
+ return value === null || value === "" ? void 0 : value;
6608
+ }
6603
6609
  //#endregion
6604
6610
  //#region src/core/domain/notification/services/configParser.ts
6605
6611
  const REMINDER_OFFSET_MIN = -1e4;
@@ -6695,10 +6701,13 @@ function parseReminderConfig(raw) {
6695
6701
  if (!isRecord(raw)) throw new BusinessRuleError(NotificationErrorCode.NtInvalidConfigStructure, "\"reminder\" must be an object");
6696
6702
  if (!Array.isArray(raw.notifications)) throw new BusinessRuleError(NotificationErrorCode.NtInvalidConfigStructure, "\"reminder\" must have a \"notifications\" array");
6697
6703
  const notifications = raw.notifications.map((item, i) => parseReminderNotification(item, i));
6698
- return {
6699
- timezone: typeof raw.timezone === "string" ? raw.timezone : "Asia/Tokyo",
6704
+ const rawTimezone = raw.timezone ?? null;
6705
+ if (rawTimezone !== null && typeof rawTimezone !== "string") throw new BusinessRuleError(NotificationErrorCode.NtInvalidConfigStructure, `"reminder" has non-string "timezone": ${String(rawTimezone)}`);
6706
+ const timezone = normalizeReminderTimezone(rawTimezone);
6707
+ return timezone !== void 0 ? {
6708
+ timezone,
6700
6709
  notifications
6701
- };
6710
+ } : { notifications };
6702
6711
  }
6703
6712
  const NotificationConfigParser = { parse: (parsed) => {
6704
6713
  const obj = validateParsedConfig(parsed, NotificationErrorCode.NtInvalidConfigStructure, "Notification");
@@ -6747,8 +6756,7 @@ async function applyNotification({ container }) {
6747
6756
  if (config.reminder !== void 0) {
6748
6757
  const currentReminder = await container.notificationConfigurator.getReminderNotifications();
6749
6758
  await container.notificationConfigurator.updateReminderNotifications({
6750
- timezone: config.reminder.timezone,
6751
- notifications: config.reminder.notifications,
6759
+ ...config.reminder,
6752
6760
  revision: currentReminder.revision
6753
6761
  });
6754
6762
  }
@@ -6811,6 +6819,16 @@ async function applyPlugin({ container }) {
6811
6819
  };
6812
6820
  }
6813
6821
  //#endregion
6822
+ //#region src/core/domain/processManagement/valueObject.ts
6823
+ /**
6824
+ * Human-readable label of an action, e.g. `申請する (未申請 → 部長承認待ち)`.
6825
+ * The name alone is ambiguous: kintone allows several actions with the same
6826
+ * name, both from different states and as conditional branches from one state.
6827
+ */
6828
+ function describeProcessAction(action) {
6829
+ return `${action.name} (${action.from} → ${action.to})`;
6830
+ }
6831
+ //#endregion
6814
6832
  //#region src/core/domain/processManagement/services/configParser.ts
6815
6833
  const VALID_ASSIGNEE_TYPES$1 = /* @__PURE__ */ new Set([
6816
6834
  "ONE",
@@ -6890,15 +6908,10 @@ const ProcessManagementConfigParser = { parse: (parsed) => {
6890
6908
  for (const [name, value] of Object.entries(rawStates)) states[name] = parseState(value, name);
6891
6909
  if (!Array.isArray(obj.actions) && obj.actions !== void 0) throw new BusinessRuleError(ProcessManagementErrorCode.PmInvalidConfigStructure, "Config \"actions\" must be an array");
6892
6910
  const actions = (Array.isArray(obj.actions) ? obj.actions : []).map((item, i) => parseAction(item, i));
6893
- const actionNames = /* @__PURE__ */ new Set();
6894
- for (const action of actions) {
6895
- if (actionNames.has(action.name)) throw new BusinessRuleError(ProcessManagementErrorCode.PmDuplicateActionName, `Duplicate action name: "${action.name}"`);
6896
- actionNames.add(action.name);
6897
- }
6898
6911
  const stateNames = new Set(Object.keys(states));
6899
6912
  for (const action of actions) {
6900
- if (!stateNames.has(action.from)) throw new BusinessRuleError(ProcessManagementErrorCode.PmInvalidActionReference, `Action "${action.name}" references unknown "from" state: "${action.from}"`);
6901
- if (!stateNames.has(action.to)) throw new BusinessRuleError(ProcessManagementErrorCode.PmInvalidActionReference, `Action "${action.name}" references unknown "to" state: "${action.to}"`);
6913
+ if (!stateNames.has(action.from)) throw new BusinessRuleError(ProcessManagementErrorCode.PmInvalidActionReference, `Action "${describeProcessAction(action)}" references unknown "from" state: "${action.from}"`);
6914
+ if (!stateNames.has(action.to)) throw new BusinessRuleError(ProcessManagementErrorCode.PmInvalidActionReference, `Action "${describeProcessAction(action)}" references unknown "to" state: "${action.to}"`);
6902
6915
  }
6903
6916
  return {
6904
6917
  enable,
@@ -8241,8 +8254,11 @@ var KintoneNotificationConfigurator = class {
8241
8254
  preview: true
8242
8255
  });
8243
8256
  const notifications = response.notifications.map(fromKintoneReminderNotification);
8257
+ const rawTimezone = response.timezone ?? null;
8258
+ if (rawTimezone !== null && typeof rawTimezone !== "string") throw new SystemError(SystemErrorCode.ExternalApiError, `Unexpected reminder timezone from kintone API: ${String(rawTimezone)}`);
8259
+ const timezone = normalizeReminderTimezone(rawTimezone);
8244
8260
  return {
8245
- timezone: response.timezone,
8261
+ ...timezone !== void 0 ? { timezone } : {},
8246
8262
  notifications,
8247
8263
  revision: response.revision
8248
8264
  };
@@ -8254,9 +8270,9 @@ var KintoneNotificationConfigurator = class {
8254
8270
  try {
8255
8271
  const requestParams = {
8256
8272
  app: this.appId,
8257
- timezone: params.timezone,
8258
8273
  notifications: params.notifications.map(toKintoneReminderNotification)
8259
8274
  };
8275
+ if (params.timezone !== void 0) requestParams.timezone = params.timezone;
8260
8276
  if (params.revision !== void 0) requestParams.revision = params.revision;
8261
8277
  return { revision: (await this.client.app.updateReminderNotifications(requestParams)).revision };
8262
8278
  } catch (error) {
@@ -9363,6 +9379,56 @@ function groupByKey(items, keyFn) {
9363
9379
  return map;
9364
9380
  }
9365
9381
  //#endregion
9382
+ //#region src/lib/pairByKeyInOrder.ts
9383
+ /**
9384
+ * Pairs local and remote items by key. Items sharing a key are matched by
9385
+ * their order of appearance, so the n-th local item with a key pairs with the
9386
+ * n-th remote item with that key.
9387
+ *
9388
+ * Pairs are returned grouped by key: keys in local first-appearance order,
9389
+ * then remote-only keys in remote first-appearance order.
9390
+ */
9391
+ function pairByKeyInOrder(local, remote, keyFn) {
9392
+ const localGroups = groupByKey(local, keyFn);
9393
+ const remoteGroups = groupByKey(remote, keyFn);
9394
+ const pairs = [];
9395
+ for (const [key, localItems] of localGroups) {
9396
+ const remoteItems = remoteGroups.get(key) ?? [];
9397
+ const length = Math.max(localItems.length, remoteItems.length);
9398
+ for (let index = 0; index < length; index++) if (index >= remoteItems.length) pairs.push({
9399
+ kind: "localOnly",
9400
+ key,
9401
+ index,
9402
+ local: localItems[index]
9403
+ });
9404
+ else if (index >= localItems.length) pairs.push({
9405
+ kind: "remoteOnly",
9406
+ key,
9407
+ index,
9408
+ remote: remoteItems[index]
9409
+ });
9410
+ else pairs.push({
9411
+ kind: "both",
9412
+ key,
9413
+ index,
9414
+ local: localItems[index],
9415
+ remote: remoteItems[index]
9416
+ });
9417
+ }
9418
+ for (const [key, remoteItems] of remoteGroups) {
9419
+ if (localGroups.has(key)) continue;
9420
+ remoteItems.forEach((remoteItem, index) => {
9421
+ pairs.push({
9422
+ kind: "remoteOnly",
9423
+ key,
9424
+ index,
9425
+ remote: remoteItem
9426
+ });
9427
+ });
9428
+ }
9429
+ return pairs;
9430
+ }
9431
+ //#endregion
9366
9432
  //#region src/core/domain/notification/services/diffDetector.ts
9367
9433
  function serializeEntity$1(entity) {
9368
9434
  return `${entity.type}:${entity.code}`;
@@ -9416,14 +9482,6 @@ function compareGeneralSection(local, remote) {
9416
9482
  });
9417
9483
  return entries;
9418
9484
  }
9419
- /**
9420
- * Groups per-record notifications by filterCond. Multiple notifications can share
9421
- * the same filterCond, so notifications within a group are compared by position
9422
- * (index-based matching).
9423
- */
9424
- function buildPerRecordMultiMap(notifications) {
9425
- return groupByKey(notifications, (n) => n.filterCond);
9426
- }
9427
9485
  function perRecordLabel(notif) {
9428
9486
  return notif.title || notif.filterCond || "(empty filter)";
9429
9487
  }
@@ -9445,43 +9503,30 @@ function describeReminderChanges(local, remote) {
9445
9503
  }
9446
9504
  function comparePerRecordSection(local, remote) {
9447
9505
  const entries = [];
9448
- const localMulti = buildPerRecordMultiMap(local);
9449
- const remoteMulti = buildPerRecordMultiMap(remote);
9450
- for (const [key, localNotifs] of localMulti) {
9451
- const remoteNotifs = remoteMulti.get(key) ?? [];
9452
- const maxLen = Math.max(localNotifs.length, remoteNotifs.length);
9453
- for (let i = 0; i < maxLen; i++) {
9454
- const localNotif = localNotifs[i];
9455
- const remoteNotif = remoteNotifs[i];
9456
- if (localNotif && !remoteNotif) entries.push({
9506
+ for (const pair of pairByKeyInOrder(local, remote, (n) => n.filterCond)) switch (pair.kind) {
9507
+ case "localOnly":
9508
+ entries.push({
9457
9509
  type: "added",
9458
9510
  section: "perRecord",
9459
- name: perRecordLabel(localNotif),
9511
+ name: perRecordLabel(pair.local),
9460
9512
  details: "new notification"
9461
9513
  });
9462
- else if (!localNotif && remoteNotif) entries.push({
9514
+ break;
9515
+ case "remoteOnly":
9516
+ entries.push({
9463
9517
  type: "deleted",
9464
9518
  section: "perRecord",
9465
- name: perRecordLabel(remoteNotif),
9519
+ name: perRecordLabel(pair.remote),
9466
9520
  details: "removed"
9467
9521
  });
9468
- else if (localNotif && remoteNotif && !deepEqual(localNotif, remoteNotif)) {
9469
- const diffs = describePerRecordChanges(localNotif, remoteNotif);
9470
- entries.push({
9471
- type: "modified",
9472
- section: "perRecord",
9473
- name: perRecordLabel(localNotif),
9474
- details: diffs
9475
- });
9476
- }
9477
- }
9522
+ break;
9523
+ case "both": if (!deepEqual(pair.local, pair.remote)) entries.push({
9524
+ type: "modified",
9525
+ section: "perRecord",
9526
+ name: perRecordLabel(pair.local),
9527
+ details: describePerRecordChanges(pair.local, pair.remote)
9528
+ });
9478
9529
  }
9479
- for (const [key, remoteNotifs] of remoteMulti) if (!localMulti.has(key)) for (const remoteNotif of remoteNotifs) entries.push({
9480
- type: "deleted",
9481
- section: "perRecord",
9482
- name: perRecordLabel(remoteNotif),
9483
- details: "removed"
9484
- });
9485
9530
  return entries;
9486
9531
  }
9487
9532
  function compareReminderSection(local, remote) {
@@ -9531,11 +9576,11 @@ const NotificationDiffDetector = { detect: (local, remote) => {
9531
9576
  entries.push(...comparePerRecordSection(localPerRecord, remotePerRecord));
9532
9577
  if (local.reminder && remote.reminder) {
9533
9578
  entries.push(...compareReminderSection(local.reminder.notifications, remote.reminder.notifications));
9534
- if (local.reminder.timezone !== remote.reminder.timezone) entries.push({
9579
+ if (local.reminder.timezone !== void 0 && local.reminder.timezone !== remote.reminder.timezone) entries.push({
9535
9580
  type: "modified",
9536
9581
  section: "reminder",
9537
9582
  name: "timezone",
9538
- details: `${remote.reminder.timezone} -> ${local.reminder.timezone}`
9583
+ details: `${remote.reminder.timezone ?? "(unset)"} -> ${local.reminder.timezone}`
9539
9584
  });
9540
9585
  } else if (local.reminder && !remote.reminder) entries.push({
9541
9586
  type: "added",
@@ -9552,28 +9597,46 @@ const NotificationDiffDetector = { detect: (local, remote) => {
9552
9597
  return buildDiffResult(entries);
9553
9598
  } };
9554
9599
  //#endregion
9555
- //#region src/core/application/notification/detectNotificationDiff.ts
9556
- async function detectNotificationDiff({ container }) {
9557
- const [storageResult, generalResult, perRecordResult, reminderResult] = await Promise.all([
9558
- container.notificationStorage.get(),
9600
+ //#region src/core/application/notification/loadNotificationRemote.ts
9601
+ /**
9602
+ * Fetches the three remote sub-configs (general / perRecord / reminder) and
9603
+ * bundles them into a single {@link NotificationRemote}.
9604
+ *
9605
+ * The three getters each carry the app (preview) revision; since the revision is
9606
+ * app-scoped and the three are fetched at (essentially) the same moment, the
9607
+ * general getter's revision is used as the observed remote revision for the
9608
+ * TOCTOU guard. kintone always returns all three sections, so the bundled config
9609
+ * always has `general` / `perRecord` / `reminder` defined (an unset reminder
9610
+ * timezone stays absent).
9611
+ */
9612
+ async function loadNotificationRemote(container) {
9613
+ const [general, perRecord, reminder] = await Promise.all([
9559
9614
  container.notificationConfigurator.getGeneralNotifications(),
9560
9615
  container.notificationConfigurator.getPerRecordNotifications(),
9561
9616
  container.notificationConfigurator.getReminderNotifications()
9562
9617
  ]);
9563
- if (!storageResult.exists) throw new ValidationError(ValidationErrorCode.InvalidInput, "Notification config file not found");
9564
- const localConfig = parseNotificationConfigText(container.configCodec, storageResult.content);
9565
- const remoteConfig = {
9566
- general: {
9567
- notifyToCommenter: generalResult.notifyToCommenter,
9568
- notifications: generalResult.notifications
9618
+ return {
9619
+ config: {
9620
+ general: {
9621
+ notifyToCommenter: general.notifyToCommenter,
9622
+ notifications: general.notifications
9623
+ },
9624
+ perRecord: perRecord.notifications,
9625
+ reminder: {
9626
+ ...reminder.timezone !== void 0 ? { timezone: reminder.timezone } : {},
9627
+ notifications: reminder.notifications
9628
+ }
9569
9629
  },
9570
- perRecord: perRecordResult.notifications,
9571
- reminder: {
9572
- timezone: reminderResult.timezone,
9573
- notifications: reminderResult.notifications
9574
- }
9630
+ revision: general.revision
9575
9631
  };
9576
- return NotificationDiffDetector.detect(localConfig, remoteConfig);
9632
+ }
9633
+ //#endregion
9634
+ //#region src/core/application/notification/detectNotificationDiff.ts
9635
+ async function detectNotificationDiff({ container }) {
9636
+ const [storageResult, remote] = await Promise.all([container.notificationStorage.get(), loadNotificationRemote(container)]);
9637
+ if (!storageResult.exists) throw new ValidationError(ValidationErrorCode.InvalidInput, "Notification config file not found");
9638
+ const localConfig = parseNotificationConfigText(container.configCodec, storageResult.content);
9639
+ return NotificationDiffDetector.detect(localConfig, remote.config);
9577
9640
  }
9578
9641
  //#endregion
9579
9642
  //#region src/core/domain/plugin/services/diffDetector.ts
@@ -9637,8 +9700,6 @@ function isExecutableUserEqual(a, b) {
9637
9700
  }
9638
9701
  function compareActions(localAction, remoteAction) {
9639
9702
  const diffs = [];
9640
- if (localAction.from !== remoteAction.from) diffs.push(`from: ${remoteAction.from} -> ${localAction.from}`);
9641
- if (localAction.to !== remoteAction.to) diffs.push(`to: ${remoteAction.to} -> ${localAction.to}`);
9642
9703
  if (localAction.filterCond !== remoteAction.filterCond) diffs.push(`filterCond: "${remoteAction.filterCond}" -> "${localAction.filterCond}"`);
9643
9704
  if (localAction.type !== remoteAction.type) diffs.push(`type: ${remoteAction.type} -> ${localAction.type}`);
9644
9705
  if (!isExecutableUserEqual(localAction.executableUser, remoteAction.executableUser)) diffs.push(`executableUser: ${formatValue(remoteAction.executableUser)} -> ${formatValue(localAction.executableUser)}`);
@@ -9674,33 +9735,52 @@ function compareStates(local, remote, entries) {
9674
9735
  details: `assignee: ${remote.states[name].assignee.type}`
9675
9736
  });
9676
9737
  }
9738
+ /**
9739
+ * Identifies an action by name + from + to. kintone allows same-named actions
9740
+ * both from different states and as conditional branches from one state.
9741
+ * Repeats of the whole triple are not rejected either, so actions sharing a
9742
+ * triple are told apart by their order of appearance.
9743
+ */
9744
+ function transitionKey(action) {
9745
+ return JSON.stringify([
9746
+ action.name,
9747
+ action.from,
9748
+ action.to
9749
+ ]);
9750
+ }
9751
+ function actionLabel(action, index) {
9752
+ const label = describeProcessAction(action);
9753
+ return index === 0 ? label : `${label} #${index + 1}`;
9754
+ }
9677
9755
  function compareActionEntries(local, remote, entries) {
9678
- const localActionMap = new Map(local.actions.map((a) => [a.name, a]));
9679
- const remoteActionMap = new Map(remote.actions.map((a) => [a.name, a]));
9680
- for (const [name, localAction] of localActionMap) {
9681
- const remoteAction = remoteActionMap.get(name);
9682
- if (!remoteAction) entries.push({
9683
- type: "added",
9684
- category: "action",
9685
- name,
9686
- details: `${localAction.from} -> ${localAction.to}`
9687
- });
9688
- else {
9689
- const actionDiffs = compareActions(localAction, remoteAction);
9756
+ for (const pair of pairByKeyInOrder(local.actions, remote.actions, transitionKey)) switch (pair.kind) {
9757
+ case "localOnly":
9758
+ entries.push({
9759
+ type: "added",
9760
+ category: "action",
9761
+ name: actionLabel(pair.local, pair.index),
9762
+ details: `filterCond: "${pair.local.filterCond}"`
9763
+ });
9764
+ break;
9765
+ case "remoteOnly":
9766
+ entries.push({
9767
+ type: "deleted",
9768
+ category: "action",
9769
+ name: actionLabel(pair.remote, pair.index),
9770
+ details: `filterCond: "${pair.remote.filterCond}"`
9771
+ });
9772
+ break;
9773
+ case "both": {
9774
+ const actionDiffs = compareActions(pair.local, pair.remote);
9690
9775
  if (actionDiffs.length > 0) entries.push({
9691
9776
  type: "modified",
9692
9777
  category: "action",
9693
- name,
9778
+ name: actionLabel(pair.local, pair.index),
9694
9779
  details: actionDiffs.join("\n")
9695
9780
  });
9781
+ break;
9696
9782
  }
9697
9783
  }
9698
- for (const [name, remoteAction] of remoteActionMap) if (!localActionMap.has(name)) entries.push({
9699
- type: "deleted",
9700
- category: "action",
9701
- name,
9702
- details: `${remoteAction.from} -> ${remoteAction.to}`
9703
- });
9704
9784
  }
9705
9785
  function compareConfigs(local, remote) {
9706
9786
  const entries = [];
@@ -9758,24 +9838,27 @@ function describeRight(right) {
9758
9838
  });
9759
9839
  return perEntity.length > 0 ? perEntity.join(", ") : "no entities";
9760
9840
  }
9761
- function compareRightsForFilter(filterCond, localRights, remoteRights, entries) {
9762
- const maxLen = Math.max(localRights.length, remoteRights.length);
9763
- for (let i = 0; i < maxLen; i++) {
9764
- const localRight = localRights[i];
9765
- const remoteRight = remoteRights[i];
9766
- if (localRight && !remoteRight) entries.push({
9767
- type: "added",
9768
- filterCond,
9769
- details: describeRight(localRight)
9770
- });
9771
- else if (!localRight && remoteRight) entries.push({
9772
- type: "deleted",
9773
- filterCond,
9774
- details: describeRight(remoteRight)
9775
- });
9776
- else if (localRight && remoteRight) {
9777
- if (!isRightEqual(localRight, remoteRight)) {
9778
- const detail = localRight.entities.length !== remoteRight.entities.length ? `entities: ${remoteRight.entities.length} -> ${localRight.entities.length}` : `entities: ${describeRight(remoteRight)} -> ${describeRight(localRight)}`;
9841
+ const RecordPermissionDiffDetector = { detect: (local, remote) => {
9842
+ const entries = [];
9843
+ for (const pair of pairByKeyInOrder(local.rights, remote.rights, (r) => r.filterCond)) {
9844
+ const filterCond = pair.key;
9845
+ switch (pair.kind) {
9846
+ case "localOnly":
9847
+ entries.push({
9848
+ type: "added",
9849
+ filterCond,
9850
+ details: describeRight(pair.local)
9851
+ });
9852
+ break;
9853
+ case "remoteOnly":
9854
+ entries.push({
9855
+ type: "deleted",
9856
+ filterCond,
9857
+ details: describeRight(pair.remote)
9858
+ });
9859
+ break;
9860
+ case "both": if (!isRightEqual(pair.local, pair.remote)) {
9861
+ const detail = pair.local.entities.length !== pair.remote.entities.length ? `entities: ${pair.remote.entities.length} -> ${pair.local.entities.length}` : `entities: ${describeRight(pair.remote)} -> ${describeRight(pair.local)}`;
9779
9862
  entries.push({
9780
9863
  type: "modified",
9781
9864
  filterCond,
@@ -9784,17 +9867,6 @@ function compareRightsForFilter(filterCond, localRights, remoteRights, entries)
9784
9867
  }
9785
9868
  }
9786
9869
  }
9787
- }
9788
- const RecordPermissionDiffDetector = { detect: (local, remote) => {
9789
- const entries = [];
9790
- const localMulti = groupByKey(local.rights, (r) => r.filterCond);
9791
- const remoteMulti = groupByKey(remote.rights, (r) => r.filterCond);
9792
- for (const [filterCond, localRights] of localMulti) compareRightsForFilter(filterCond, localRights, remoteMulti.get(filterCond) ?? [], entries);
9793
- for (const [filterCond, remoteRights] of remoteMulti) if (!localMulti.has(filterCond)) for (const remoteRight of remoteRights) entries.push({
9794
- type: "deleted",
9795
- filterCond,
9796
- details: describeRight(remoteRight)
9797
- });
9798
9870
  return buildDiffResult(entries);
9799
9871
  } };
9800
9872
  //#endregion
@@ -11167,7 +11239,7 @@ const NotificationConfigSerializer = { serialize: (config) => {
11167
11239
  };
11168
11240
  if (config.perRecord !== void 0) serialized.perRecord = config.perRecord.map(serializePerRecordNotification);
11169
11241
  if (config.reminder !== void 0) serialized.reminder = {
11170
- timezone: config.reminder.timezone,
11242
+ ...config.reminder.timezone !== void 0 ? { timezone: config.reminder.timezone } : {},
11171
11243
  notifications: config.reminder.notifications.map(serializeReminderNotification)
11172
11244
  };
11173
11245
  return serialized;
@@ -11175,22 +11247,7 @@ const NotificationConfigSerializer = { serialize: (config) => {
11175
11247
  //#endregion
11176
11248
  //#region src/core/application/notification/captureNotification.ts
11177
11249
  async function captureNotification({ container }) {
11178
- const [general, perRecord, reminder] = await Promise.all([
11179
- container.notificationConfigurator.getGeneralNotifications(),
11180
- container.notificationConfigurator.getPerRecordNotifications(),
11181
- container.notificationConfigurator.getReminderNotifications()
11182
- ]);
11183
- const config = {
11184
- general: {
11185
- notifyToCommenter: general.notifyToCommenter,
11186
- notifications: general.notifications
11187
- },
11188
- perRecord: perRecord.notifications,
11189
- reminder: {
11190
- timezone: reminder.timezone,
11191
- notifications: reminder.notifications
11192
- }
11193
- };
11250
+ const { config } = await loadNotificationRemote(container);
11194
11251
  return {
11195
11252
  configText: stringifyConfig(container.configCodec, NotificationConfigSerializer.serialize(config)),
11196
11253
  hasExistingConfig: (await container.notificationStorage.get()).exists
@@ -13758,7 +13815,8 @@ var capture_default$8 = createCaptureCommand({
13758
13815
  * conflict-resolution namespace:
13759
13816
  *
13760
13817
  * - **reminder** = `code` stable key (`compareReminderSection`, diffDetector
13761
- * reminder map). Plus the `timezone` scalar as a single whole-entity 3-way.
13818
+ * reminder map). Plus the `timezone` scalar as a single whole-entity 3-way
13819
+ * when the local config manages it (sets a timezone).
13762
13820
  * - **perRecord** = group by `filterCond`, then within a group match by index
13763
13821
  * (`filterCond#i` composite key — `comparePerRecordSection`). `perRecordLabel`
13764
13822
  * is display-only and is NOT used as a match key.
@@ -13821,7 +13879,7 @@ function computeNotificationThreeWayMerge(base, local, remote) {
13821
13879
  const perRecord = classifyThreeWay(buildPerRecordMap(base.perRecord), buildPerRecordMap(local.perRecord), buildPerRecordMap(remote.perRecord), isPerRecordEqual);
13822
13880
  const reminder = classifyThreeWay(buildReminderMap(base.reminder), buildReminderMap(local.reminder), buildReminderMap(remote.reminder), isReminderEqual);
13823
13881
  const generalScalar = computeSingleThreeWayMerge(base.general?.notifyToCommenter ?? false, local.general?.notifyToCommenter ?? false, remote.general?.notifyToCommenter ?? false, (a, b) => a === b);
13824
- const reminderTimezone = computeSingleThreeWayMerge(base.reminder?.timezone ?? "", local.reminder?.timezone ?? "", remote.reminder?.timezone ?? "", (a, b) => a === b);
13882
+ 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;
13825
13883
  const entries = [
13826
13884
  ...general.entries,
13827
13885
  ...perRecord.entries,
@@ -13832,12 +13890,12 @@ function computeNotificationThreeWayMerge(base, local, remote) {
13832
13890
  ...perRecord.conflicts.map((e) => ({ key: e.key })),
13833
13891
  ...reminder.conflicts.map((e) => ({ key: e.key })),
13834
13892
  ...scalarConflictKey(generalScalar, GENERAL_SCALAR_KEY),
13835
- ...scalarConflictKey(reminderTimezone, REMINDER_TIMEZONE_KEY)
13893
+ ...reminderTimezone !== void 0 ? scalarConflictKey(reminderTimezone, REMINDER_TIMEZONE_KEY) : []
13836
13894
  ];
13837
13895
  return {
13838
13896
  entries,
13839
13897
  generalScalar,
13840
- reminderTimezone,
13898
+ ...reminderTimezone !== void 0 ? { reminderTimezone } : {},
13841
13899
  conflicts,
13842
13900
  hasConflict: conflicts.length > 0
13843
13901
  };
@@ -13887,7 +13945,7 @@ function collectReminderNotifications(entries, resolution) {
13887
13945
  */
13888
13946
  function resolveNotificationMerge(merge, resolution) {
13889
13947
  const notifyToCommenter = resolveSingleMerge(merge.generalScalar, resolution.get(GENERAL_SCALAR_KEY), NotificationErrorCode.NtUnresolvedConflict);
13890
- const timezone = resolveSingleMerge(merge.reminderTimezone, resolution.get(REMINDER_TIMEZONE_KEY), NotificationErrorCode.NtUnresolvedConflict);
13948
+ const timezone = merge.reminderTimezone !== void 0 ? resolveSingleMerge(merge.reminderTimezone, resolution.get(REMINDER_TIMEZONE_KEY), NotificationErrorCode.NtUnresolvedConflict) : null;
13891
13949
  return {
13892
13950
  general: {
13893
13951
  notifyToCommenter,
@@ -13895,7 +13953,7 @@ function resolveNotificationMerge(merge, resolution) {
13895
13953
  },
13896
13954
  perRecord: collectPerRecordNotifications(merge.entries, resolution),
13897
13955
  reminder: {
13898
- timezone,
13956
+ ...timezone !== null ? { timezone } : {},
13899
13957
  notifications: collectReminderNotifications(merge.entries, resolution)
13900
13958
  }
13901
13959
  };
@@ -13914,37 +13972,6 @@ const NotificationStateParser = { parse: (parsed) => ({ config: NotificationConf
13914
13972
  //#endregion
13915
13973
  //#region src/core/application/notification/loadNotificationThreeWayInputs.ts
13916
13974
  /**
13917
- * Fetches the three remote sub-configs (general / perRecord / reminder) and
13918
- * bundles them into a single {@link NotificationRemote}.
13919
- *
13920
- * The three getters each carry the app (preview) revision; since the revision is
13921
- * app-scoped and the three are fetched at (essentially) the same moment, the
13922
- * general getter's revision is used as the observed remote revision for the
13923
- * TOCTOU guard. kintone always returns all three sections, so the bundled config
13924
- * always has `general` / `perRecord` / `reminder` defined.
13925
- */
13926
- async function loadNotificationRemote(container) {
13927
- const [general, perRecord, reminder] = await Promise.all([
13928
- container.notificationConfigurator.getGeneralNotifications(),
13929
- container.notificationConfigurator.getPerRecordNotifications(),
13930
- container.notificationConfigurator.getReminderNotifications()
13931
- ]);
13932
- return {
13933
- config: {
13934
- general: {
13935
- notifyToCommenter: general.notifyToCommenter,
13936
- notifications: general.notifications
13937
- },
13938
- perRecord: perRecord.notifications,
13939
- reminder: {
13940
- timezone: reminder.timezone,
13941
- notifications: reminder.notifications
13942
- }
13943
- },
13944
- revision: general.revision
13945
- };
13946
- }
13947
- /**
13948
13975
  * Loads the four inputs of a 3-way notification sync (base snapshot, base app
13949
13976
  * revision, local YAML, remote config + revision) via the generic
13950
13977
  * {@link loadThreeWayInputs}. Notification bundles three sub-configs into one
@@ -14047,7 +14074,7 @@ function buildThreeWay(merge) {
14047
14074
  }
14048
14075
  }
14049
14076
  pushScalar(merge.generalScalar, "general:notifyToCommenter", "notifyToCommenter", localChanges, remoteDrift, conflicts, extras);
14050
- pushScalar(merge.reminderTimezone, "reminder:timezone", "timezone", localChanges, remoteDrift, conflicts, extras);
14077
+ if (merge.reminderTimezone !== void 0) pushScalar(merge.reminderTimezone, "reminder:timezone", "timezone", localChanges, remoteDrift, conflicts, extras);
14051
14078
  return {
14052
14079
  mode: "three-way",
14053
14080
  localChanges,
@@ -14270,7 +14297,7 @@ async function pushNotification({ container, input }) {
14270
14297
  return false;
14271
14298
  };
14272
14299
  const isDriftKind = (kind) => kind === "remoteOnly" || kind === "conflict";
14273
- 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);
14300
+ 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);
14274
14301
  }
14275
14302
  const sendRevision = !(input.force || firstTime);
14276
14303
  let revision = remote.revision;
@@ -14296,11 +14323,14 @@ async function pushNotification({ container, input }) {
14296
14323
  }
14297
14324
  if (local.reminder !== void 0) {
14298
14325
  revision = (await container.notificationConfigurator.updateReminderNotifications({
14299
- timezone: local.reminder.timezone,
14300
- notifications: local.reminder.notifications,
14326
+ ...local.reminder,
14301
14327
  ...sendRevision ? { revision } : {}
14302
14328
  })).revision;
14303
- newBase.reminder = local.reminder;
14329
+ const timezone = local.reminder.timezone ?? remote.config.reminder?.timezone;
14330
+ newBase.reminder = {
14331
+ ...timezone !== void 0 ? { timezone } : {},
14332
+ notifications: local.reminder.notifications
14333
+ };
14304
14334
  }
14305
14335
  await saveNotificationSnapshotAndRevision(container, newBase, revision);
14306
14336
  return {