@im-nobsidian/core 0.12.0 → 0.13.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/dist/index.d.ts +195 -16
- package/dist/index.js +2279 -425
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -457,6 +457,10 @@ function notionIdsEqual(a, b) {
|
|
|
457
457
|
function compactNotionId(id) {
|
|
458
458
|
return id.replace(/-/g, "").toLowerCase();
|
|
459
459
|
}
|
|
460
|
+
var SHORT_NOTION_ID_LENGTH = 8;
|
|
461
|
+
function shortNotionId(id) {
|
|
462
|
+
return compactNotionId(id).slice(0, SHORT_NOTION_ID_LENGTH);
|
|
463
|
+
}
|
|
460
464
|
|
|
461
465
|
// src/utils/logger.ts
|
|
462
466
|
var consoleLogger = {
|
|
@@ -1679,6 +1683,9 @@ function isFrontmatterDate(value) {
|
|
|
1679
1683
|
}
|
|
1680
1684
|
var RANGE_END_SUFFIX = "_end";
|
|
1681
1685
|
var DATE_VALUED_TYPES = /* @__PURE__ */ new Set(["date", "formula", "rollup"]);
|
|
1686
|
+
function mayHaveRangeEnd(type) {
|
|
1687
|
+
return DATE_VALUED_TYPES.has(type);
|
|
1688
|
+
}
|
|
1682
1689
|
var TIMESTAMP_TYPES = /* @__PURE__ */ new Set(["created_time", "last_edited_time"]);
|
|
1683
1690
|
var READ_ONLY_TYPES = /* @__PURE__ */ new Set([
|
|
1684
1691
|
"created_time",
|
|
@@ -2804,6 +2811,10 @@ var DiscoveryTooLargeError = class extends Error {
|
|
|
2804
2811
|
};
|
|
2805
2812
|
var NOT_IDEMPOTENT = { idempotent: false };
|
|
2806
2813
|
var CREATES_BLOCKS = { idempotent: false, createsBlocks: true };
|
|
2814
|
+
function rowParentOf(params) {
|
|
2815
|
+
if (params.parentType === "page") return { page_id: params.parentId };
|
|
2816
|
+
return params.dataSourceId ? { type: "data_source_id", data_source_id: params.dataSourceId } : { database_id: params.parentId };
|
|
2817
|
+
}
|
|
2807
2818
|
function parentDatabaseIdOf(page) {
|
|
2808
2819
|
const parent = page.parent;
|
|
2809
2820
|
if (parent?.type !== "data_source_id" && parent?.type !== "database_id") return null;
|
|
@@ -2951,7 +2962,7 @@ var NotionClient = class _NotionClient {
|
|
|
2951
2962
|
);
|
|
2952
2963
|
}
|
|
2953
2964
|
async createPage(params) {
|
|
2954
|
-
const parent = params
|
|
2965
|
+
const parent = rowParentOf(params);
|
|
2955
2966
|
const properties = params.parentType === "page" ? { title: { title: [{ text: { content: params.title } }] } } : params.properties ?? {
|
|
2956
2967
|
title: { title: [{ text: { content: params.title } }] }
|
|
2957
2968
|
};
|
|
@@ -3021,7 +3032,7 @@ var NotionClient = class _NotionClient {
|
|
|
3021
3032
|
return ds.properties ?? {};
|
|
3022
3033
|
}
|
|
3023
3034
|
async createPageWithMarkdown(params) {
|
|
3024
|
-
const parent = params
|
|
3035
|
+
const parent = rowParentOf(params);
|
|
3025
3036
|
const properties = params.parentType === "page" ? { title: { title: [{ text: { content: params.title } }] } } : params.properties ?? {
|
|
3026
3037
|
title: { title: [{ text: { content: params.title } }] }
|
|
3027
3038
|
};
|
|
@@ -3183,6 +3194,7 @@ var NotionClient = class _NotionClient {
|
|
|
3183
3194
|
const dataSources = db.data_sources ?? [];
|
|
3184
3195
|
const merged = {};
|
|
3185
3196
|
const owners = [];
|
|
3197
|
+
const own = /* @__PURE__ */ new Map();
|
|
3186
3198
|
let anyAccessible = false;
|
|
3187
3199
|
for (const ds of dataSources) {
|
|
3188
3200
|
try {
|
|
@@ -3192,6 +3204,7 @@ var NotionClient = class _NotionClient {
|
|
|
3192
3204
|
owners.push(dsObj.parent?.database_id);
|
|
3193
3205
|
if (dsObj.properties && Object.keys(dsObj.properties).length > 0) {
|
|
3194
3206
|
anyAccessible = true;
|
|
3207
|
+
own.set(ds.id, dsObj.properties);
|
|
3195
3208
|
for (const [name, prop] of Object.entries(dsObj.properties)) {
|
|
3196
3209
|
if (!(name in merged)) merged[name] = prop;
|
|
3197
3210
|
}
|
|
@@ -3203,7 +3216,11 @@ var NotionClient = class _NotionClient {
|
|
|
3203
3216
|
const meta = {
|
|
3204
3217
|
databaseId,
|
|
3205
3218
|
title: Array.isArray(db.title) ? db.title[0]?.plain_text ?? "" : "",
|
|
3206
|
-
dataSources: dataSources.map((ds) => ({
|
|
3219
|
+
dataSources: dataSources.map((ds) => ({
|
|
3220
|
+
id: ds.id,
|
|
3221
|
+
name: ds.name ?? "",
|
|
3222
|
+
...own.has(ds.id) ? { properties: own.get(ds.id) } : {}
|
|
3223
|
+
})),
|
|
3207
3224
|
properties: anyAccessible ? merged : db.properties ?? {},
|
|
3208
3225
|
isInline: db.is_inline === true,
|
|
3209
3226
|
appearance: {
|
|
@@ -3412,7 +3429,7 @@ var NotionClient = class _NotionClient {
|
|
|
3412
3429
|
if (metas.length > 1) {
|
|
3413
3430
|
const label = metas.map((m) => m.name || m.id).join(", ");
|
|
3414
3431
|
getLogger().warn(
|
|
3415
|
-
`[Notion] DB ${databaseId} \uC5D0 data source ${metas.length}\uAC1C \uBC1C\uACAC \u2014 \uC804 \uC18C\uC2A4 \uD589\uC744 \uD55C \uD3F4\uB354\uB85C \uBCD1\uD569 \uB3D9\uAE30\uD654\uD569\uB2C8\uB2E4 (${label}). \uC18C\uC2A4\uBCC4 \uD0ED \uAD6C\uBD84\uC740 .base \uBDF0\uC5D0 \uBCF4\uC874\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`
|
|
3432
|
+
`[Notion] DB ${shortNotionId(databaseId)} \uC5D0 data source ${metas.length}\uAC1C \uBC1C\uACAC \u2014 \uC804 \uC18C\uC2A4 \uD589\uC744 \uD55C \uD3F4\uB354\uB85C \uBCD1\uD569 \uB3D9\uAE30\uD654\uD569\uB2C8\uB2E4 (${label}). \uC18C\uC2A4\uBCC4 \uD0ED \uAD6C\uBD84\uC740 .base \uBDF0\uC5D0 \uBCF4\uC874\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`
|
|
3416
3433
|
);
|
|
3417
3434
|
}
|
|
3418
3435
|
const all = [];
|
|
@@ -3902,14 +3919,14 @@ var NotionClient = class _NotionClient {
|
|
|
3902
3919
|
const blockOwner = /* @__PURE__ */ new Map();
|
|
3903
3920
|
const resolveBlockOwner = async (blockId) => {
|
|
3904
3921
|
const seen = /* @__PURE__ */ new Set();
|
|
3905
|
-
const
|
|
3922
|
+
const settle2 = (owner) => {
|
|
3906
3923
|
for (const b of seen) blockOwner.set(b, owner);
|
|
3907
3924
|
return owner;
|
|
3908
3925
|
};
|
|
3909
3926
|
let bid = blockId;
|
|
3910
3927
|
for (let i = 0; i < 20 && bid && !seen.has(bid); i++) {
|
|
3911
3928
|
const cached = blockOwner.get(bid);
|
|
3912
|
-
if (cached !== void 0) return
|
|
3929
|
+
if (cached !== void 0) return settle2(cached);
|
|
3913
3930
|
seen.add(bid);
|
|
3914
3931
|
let bp;
|
|
3915
3932
|
try {
|
|
@@ -3918,14 +3935,14 @@ var NotionClient = class _NotionClient {
|
|
|
3918
3935
|
getLogger().warn(
|
|
3919
3936
|
`[Im-Nobsidian] \uBD80\uBAA8 \uBE14\uB85D \uD574\uC18C \uC2E4\uD328 (${bid}) \u2014 \uC774 \uBE14\uB85D\uC5D0 \uC911\uCCA9\uB41C \uD398\uC774\uC9C0\uB294 search \uB514\uC2A4\uCEE4\uBC84\uB9AC\uC5D0\uC11C \uC81C\uC678\uB428: ${error instanceof Error ? error.message : String(error)}`
|
|
3920
3937
|
);
|
|
3921
|
-
return
|
|
3938
|
+
return settle2(null);
|
|
3922
3939
|
}
|
|
3923
|
-
if (bp.type === "page_id" && bp.page_id) return
|
|
3940
|
+
if (bp.type === "page_id" && bp.page_id) return settle2(bp.page_id);
|
|
3924
3941
|
if (bp.type === "block_id" && bp.block_id) {
|
|
3925
3942
|
bid = bp.block_id;
|
|
3926
3943
|
continue;
|
|
3927
3944
|
}
|
|
3928
|
-
return
|
|
3945
|
+
return settle2(null);
|
|
3929
3946
|
}
|
|
3930
3947
|
return null;
|
|
3931
3948
|
};
|
|
@@ -4019,6 +4036,15 @@ var NotionClient = class _NotionClient {
|
|
|
4019
4036
|
async deleteView(viewId) {
|
|
4020
4037
|
await this.withRateLimit(() => this.client.views.delete({ view_id: viewId }));
|
|
4021
4038
|
}
|
|
4039
|
+
/**
|
|
4040
|
+
* 보기를 고친다(ADR-046). 보내지 않은 항목은 그대로 두고 null 은 지운다. `configuration.properties` 는 통째로 바뀐다 —
|
|
4041
|
+
* 목록에 없는 속성은 숨겨지고 목록 차례가 열 차례다(2026-10-29 실측).
|
|
4042
|
+
*/
|
|
4043
|
+
async updateView(viewId, request) {
|
|
4044
|
+
await this.withRateLimit(
|
|
4045
|
+
() => this.client.views.update({ ...request, view_id: viewId })
|
|
4046
|
+
);
|
|
4047
|
+
}
|
|
4022
4048
|
async getView(viewId) {
|
|
4023
4049
|
const view = await this.withRateLimit(() => this.client.views.retrieve({ view_id: viewId }));
|
|
4024
4050
|
const v = view;
|
|
@@ -6962,8 +6988,8 @@ function assemble(p, fitted) {
|
|
|
6962
6988
|
addLines(linesAt, spot.line, spot.exact ? lines : requote(lines, neighbor(p, spot.line)));
|
|
6963
6989
|
}
|
|
6964
6990
|
}
|
|
6965
|
-
for (const
|
|
6966
|
-
addLines(linesAt,
|
|
6991
|
+
for (const group2 of groupLoose(p, loose)) {
|
|
6992
|
+
addLines(linesAt, group2.at, fitLines(p, group2.at, group2.lines));
|
|
6967
6993
|
}
|
|
6968
6994
|
return joinLines(p, linesAt, inlineAt);
|
|
6969
6995
|
}
|
|
@@ -8042,6 +8068,53 @@ function mergeRowFrontmatter(localNote, pulled, owned) {
|
|
|
8042
8068
|
}
|
|
8043
8069
|
return frontmatterLines(values);
|
|
8044
8070
|
}
|
|
8071
|
+
function rekeyRowFrontmatter(localNote, renamed, dropped) {
|
|
8072
|
+
let local;
|
|
8073
|
+
try {
|
|
8074
|
+
local = splitFrontmatter(localNote);
|
|
8075
|
+
} catch {
|
|
8076
|
+
return null;
|
|
8077
|
+
}
|
|
8078
|
+
if (!local.hasFrontmatter) return null;
|
|
8079
|
+
const keys = Object.keys(local.data);
|
|
8080
|
+
const drops = new Set(keys.filter((key) => dropped(key, local.data[key])));
|
|
8081
|
+
const staying = new Set(keys.filter((key) => !drops.has(key) && !renamed.has(key)));
|
|
8082
|
+
const target = /* @__PURE__ */ new Map();
|
|
8083
|
+
for (const key of keys) {
|
|
8084
|
+
const next = renamed.get(key);
|
|
8085
|
+
if (next === void 0 || next === key || drops.has(key) || staying.has(next)) continue;
|
|
8086
|
+
if ([...target.values()].includes(next)) continue;
|
|
8087
|
+
target.set(key, next);
|
|
8088
|
+
}
|
|
8089
|
+
if (drops.size === 0 && target.size === 0) return null;
|
|
8090
|
+
const values = {};
|
|
8091
|
+
for (const key of keys) {
|
|
8092
|
+
if (drops.has(key)) continue;
|
|
8093
|
+
values[target.get(key) ?? key] = plainFrontmatterValue(local.data[key]);
|
|
8094
|
+
}
|
|
8095
|
+
const chunks = localChunks(localNote, local.data);
|
|
8096
|
+
if (chunks !== null) {
|
|
8097
|
+
const lines = [];
|
|
8098
|
+
for (const chunk of chunks) {
|
|
8099
|
+
if (chunk.key !== "" && !drops.has(chunk.key)) {
|
|
8100
|
+
const next = target.get(chunk.key);
|
|
8101
|
+
lines.push(...next === void 0 ? chunk.body : renamedChunk(chunk, next, values[next]));
|
|
8102
|
+
}
|
|
8103
|
+
lines.push(...chunk.trailing);
|
|
8104
|
+
}
|
|
8105
|
+
if (readsAs(lines, values)) return lines;
|
|
8106
|
+
}
|
|
8107
|
+
return frontmatterLines(values);
|
|
8108
|
+
}
|
|
8109
|
+
function renamedChunk(chunk, next, value) {
|
|
8110
|
+
const [first = "", ...rest] = chunk.body;
|
|
8111
|
+
const tokens = [chunk.key, JSON.stringify(chunk.key), `'${chunk.key.replace(/'/g, "''")}'`];
|
|
8112
|
+
const token2 = tokens.find((t) => first.startsWith(`${t}:`));
|
|
8113
|
+
if (token2 === void 0) return frontmatterLines({ [next]: value });
|
|
8114
|
+
const sample = frontmatterLines({ [next]: 0 })[0] ?? "";
|
|
8115
|
+
const nextToken = sample.slice(0, sample.lastIndexOf(":"));
|
|
8116
|
+
return [`${nextToken}${first.slice(token2.length)}`, ...rest];
|
|
8117
|
+
}
|
|
8045
8118
|
function mergedKeyOrder(localKeys, pulledKeys, isOwned) {
|
|
8046
8119
|
const pulledSet = new Set(pulledKeys);
|
|
8047
8120
|
const order = localKeys.filter((key) => !isOwned(key) || pulledSet.has(key));
|
|
@@ -8075,15 +8148,15 @@ function localChunks(localNote, data) {
|
|
|
8075
8148
|
}
|
|
8076
8149
|
const chunks = [];
|
|
8077
8150
|
const seen = [];
|
|
8078
|
-
for (const [i,
|
|
8079
|
-
let end =
|
|
8080
|
-
while (end > 0 && TRIVIA_RE.test(
|
|
8151
|
+
for (const [i, group2] of groups.entries()) {
|
|
8152
|
+
let end = group2.length;
|
|
8153
|
+
while (end > 0 && TRIVIA_RE.test(group2[end - 1])) end--;
|
|
8081
8154
|
if (i === 0) {
|
|
8082
8155
|
if (end > 0) return null;
|
|
8083
|
-
chunks.push({ key: "", body: [], trailing:
|
|
8156
|
+
chunks.push({ key: "", body: [], trailing: group2 });
|
|
8084
8157
|
continue;
|
|
8085
8158
|
}
|
|
8086
|
-
const body =
|
|
8159
|
+
const body = group2.slice(0, end);
|
|
8087
8160
|
let parsed;
|
|
8088
8161
|
try {
|
|
8089
8162
|
const split = splitFrontmatter(`---
|
|
@@ -8100,7 +8173,7 @@ ${body.join("\n")}
|
|
|
8100
8173
|
const key = keys[0];
|
|
8101
8174
|
if (!Object.hasOwn(data, key) || !sameFrontmatterValue(parsed[key], data[key])) return null;
|
|
8102
8175
|
seen.push(key);
|
|
8103
|
-
chunks.push({ key, body, trailing:
|
|
8176
|
+
chunks.push({ key, body, trailing: group2.slice(end) });
|
|
8104
8177
|
}
|
|
8105
8178
|
const dataKeys = Object.keys(data);
|
|
8106
8179
|
return seen.length === dataKeys.length && seen.every((key, i) => key === dataKeys[i]) ? chunks : null;
|
|
@@ -9023,8 +9096,8 @@ ${brokenLines(note2)}`;
|
|
|
9023
9096
|
const lines = Math.max(note.split("\n").length, pulled.split("\n").length);
|
|
9024
9097
|
const limit = Math.min(TRIAL_LIMIT, Math.floor(TRIAL_LINE_LIMIT / lines));
|
|
9025
9098
|
const trial = new Trial(sent, target, slots, limit - 2);
|
|
9026
|
-
const failed = trial.bisect(spots, (
|
|
9027
|
-
for (const spot of
|
|
9099
|
+
const failed = trial.bisect(spots, (group2, local2) => {
|
|
9100
|
+
for (const spot of group2) trial.slots[spot.slot] = [...local2 ? spot.local : spot.pulled];
|
|
9028
9101
|
});
|
|
9029
9102
|
for (const { slot, run } of failed) {
|
|
9030
9103
|
if (run) trial.refine(run.local, run.pulled, (lines2) => trial.put(slot, lines2));
|
|
@@ -9186,20 +9259,20 @@ var Trial = class {
|
|
|
9186
9259
|
*/
|
|
9187
9260
|
bisect(items, swap, check = () => this.fits(), tried = true) {
|
|
9188
9261
|
const failed = [];
|
|
9189
|
-
const visit = (
|
|
9190
|
-
if (
|
|
9262
|
+
const visit = (group2, seen) => {
|
|
9263
|
+
if (group2.length === 0) return;
|
|
9191
9264
|
if (!seen) {
|
|
9192
|
-
swap(
|
|
9265
|
+
swap(group2, true);
|
|
9193
9266
|
if (check()) return;
|
|
9194
|
-
swap(
|
|
9267
|
+
swap(group2, false);
|
|
9195
9268
|
}
|
|
9196
|
-
if (
|
|
9197
|
-
failed.push(...
|
|
9269
|
+
if (group2.length === 1 || this.budget <= 0) {
|
|
9270
|
+
failed.push(...group2);
|
|
9198
9271
|
return;
|
|
9199
9272
|
}
|
|
9200
|
-
const half =
|
|
9201
|
-
visit(
|
|
9202
|
-
visit(
|
|
9273
|
+
const half = group2.length >> 1;
|
|
9274
|
+
visit(group2.slice(0, half), false);
|
|
9275
|
+
visit(group2.slice(half), false);
|
|
9203
9276
|
};
|
|
9204
9277
|
visit(items, tried);
|
|
9205
9278
|
return failed;
|
|
@@ -9239,8 +9312,8 @@ var Trial = class {
|
|
|
9239
9312
|
const differs = own.flatMap((lines, k) => sameLines(lines, theirs[k]) ? [] : [k]);
|
|
9240
9313
|
const failed = this.bisect(
|
|
9241
9314
|
differs,
|
|
9242
|
-
(
|
|
9243
|
-
for (const k of
|
|
9315
|
+
(group2, useLocal) => {
|
|
9316
|
+
for (const k of group2) chosen[k] = [...(useLocal ? own : theirs)[k]];
|
|
9244
9317
|
},
|
|
9245
9318
|
() => put(chosen.flat())
|
|
9246
9319
|
);
|
|
@@ -9309,8 +9382,8 @@ var Trial = class {
|
|
|
9309
9382
|
);
|
|
9310
9383
|
this.bisect(
|
|
9311
9384
|
spots,
|
|
9312
|
-
(
|
|
9313
|
-
for (const spot of
|
|
9385
|
+
(group2, useLocal) => {
|
|
9386
|
+
for (const spot of group2) {
|
|
9314
9387
|
chosen[spot.index] = [...useLocal ? spot.local : spot.pulled];
|
|
9315
9388
|
}
|
|
9316
9389
|
},
|
|
@@ -10283,8 +10356,8 @@ function parseRows(body) {
|
|
|
10283
10356
|
}
|
|
10284
10357
|
function parseCols(body) {
|
|
10285
10358
|
const head = body.search(ROW_START_RE);
|
|
10286
|
-
const
|
|
10287
|
-
return [...
|
|
10359
|
+
const group2 = COLGROUP_RE.exec(head === -1 ? body : body.slice(0, head))?.[1] ?? "";
|
|
10360
|
+
return [...group2.matchAll(COL_RE)].map((col) => parseNfmAttrs(col[1] ?? ""));
|
|
10288
10361
|
}
|
|
10289
10362
|
function sameAttrs(a, b) {
|
|
10290
10363
|
return a.length === b.length && a.every(([k, v]) => b.some(([bk, bv]) => bk === k && bv === v));
|
|
@@ -13604,6 +13677,44 @@ function keepLargeFile(content, fileLink, download, pageId, blockId) {
|
|
|
13604
13677
|
return withLargeFileLink(content, fileLink, link);
|
|
13605
13678
|
}
|
|
13606
13679
|
|
|
13680
|
+
// src/view/relative-dates.ts
|
|
13681
|
+
var RELATIVE_DATE_VALUES = {
|
|
13682
|
+
today: "today()",
|
|
13683
|
+
tomorrow: 'today() + "1d"',
|
|
13684
|
+
yesterday: 'today() - "1d"',
|
|
13685
|
+
one_week_ago: 'today() - "7d"',
|
|
13686
|
+
one_week_from_now: 'today() + "7d"',
|
|
13687
|
+
one_month_ago: 'today() - "1M"',
|
|
13688
|
+
one_month_from_now: 'today() + "1M"'
|
|
13689
|
+
};
|
|
13690
|
+
var RELATIVE_DATE_RANGES = {
|
|
13691
|
+
past_week: { toward: "past", span: "7d" },
|
|
13692
|
+
past_month: { toward: "past", span: "1M" },
|
|
13693
|
+
past_year: { toward: "past", span: "1y" },
|
|
13694
|
+
next_week: { toward: "next", span: "7d" },
|
|
13695
|
+
next_month: { toward: "next", span: "1M" },
|
|
13696
|
+
next_year: { toward: "next", span: "1y" }
|
|
13697
|
+
};
|
|
13698
|
+
function dateOnly(ref) {
|
|
13699
|
+
return `${ref}.date()`;
|
|
13700
|
+
}
|
|
13701
|
+
function relativeRangeExprs(ref, range) {
|
|
13702
|
+
const day = dateOnly(ref);
|
|
13703
|
+
return range.toward === "past" ? [`${day} >= today() - "${range.span}"`, `${day} <= today()`] : [`${day} >= today()`, `${day} <= today() + "${range.span}"`];
|
|
13704
|
+
}
|
|
13705
|
+
|
|
13706
|
+
// src/view/bases-file-properties.ts
|
|
13707
|
+
var BASES_TITLE = "file.name";
|
|
13708
|
+
var BASES_TIMESTAMPS = {
|
|
13709
|
+
created_time: "file.ctime",
|
|
13710
|
+
last_edited_time: "file.mtime"
|
|
13711
|
+
};
|
|
13712
|
+
function notionTimestampOf(column) {
|
|
13713
|
+
return Object.keys(BASES_TIMESTAMPS).find(
|
|
13714
|
+
(timestamp) => BASES_TIMESTAMPS[timestamp] === column
|
|
13715
|
+
);
|
|
13716
|
+
}
|
|
13717
|
+
|
|
13607
13718
|
// src/view/notion-filter-translator.ts
|
|
13608
13719
|
function translateNotionFilter(filter, schema, resolveName) {
|
|
13609
13720
|
const ctx = { resolveName, schema, untranslated: /* @__PURE__ */ new Set() };
|
|
@@ -13651,7 +13762,7 @@ function translateNode(node, ctx) {
|
|
|
13651
13762
|
function translateLeaf(leaf2, ctx) {
|
|
13652
13763
|
const timestamp = leaf2["timestamp"];
|
|
13653
13764
|
if (typeof timestamp === "string") {
|
|
13654
|
-
const ref2 = timestamp === "created_time" ? "
|
|
13765
|
+
const ref2 = BASES_TIMESTAMPS[timestamp === "created_time" ? "created_time" : "last_edited_time"];
|
|
13655
13766
|
const cond = leaf2[timestamp];
|
|
13656
13767
|
if (!isRecord(cond)) return unsupported(ctx, `timestamp '${timestamp}' \uD544\uD130`);
|
|
13657
13768
|
return translateDate(ref2, cond, ctx, `timestamp '${timestamp}'`);
|
|
@@ -13775,8 +13886,8 @@ function translateStatus(ref, cond, ctx, label, propName) {
|
|
|
13775
13886
|
expanded.push(name);
|
|
13776
13887
|
continue;
|
|
13777
13888
|
}
|
|
13778
|
-
const
|
|
13779
|
-
const members =
|
|
13889
|
+
const group2 = prop?.groups?.find((g) => g.name === name);
|
|
13890
|
+
const members = group2 && prop ? expandGroup(group2, prop) : null;
|
|
13780
13891
|
if (members === null) {
|
|
13781
13892
|
return unsupported(ctx, `${label}: \uC0C1\uD0DC '${name}' \uC744 \uC635\uC158\uC73C\uB85C \uC804\uAC1C\uD558\uC9C0 \uBABB\uD568`);
|
|
13782
13893
|
}
|
|
@@ -13787,8 +13898,8 @@ function translateStatus(ref, cond, ctx, label, propName) {
|
|
|
13787
13898
|
}
|
|
13788
13899
|
return equalityOverNames(ref, op, dedupe(expanded), ctx, label);
|
|
13789
13900
|
}
|
|
13790
|
-
function expandGroup(
|
|
13791
|
-
const ids =
|
|
13901
|
+
function expandGroup(group2, prop) {
|
|
13902
|
+
const ids = group2.optionIds;
|
|
13792
13903
|
if (!ids || ids.length === 0) return [];
|
|
13793
13904
|
const byId = new Map((prop.options ?? []).map((o) => [o.id, o.name]));
|
|
13794
13905
|
const names = [];
|
|
@@ -13828,29 +13939,32 @@ function translateEmptiness(ref, cond, ctx, label) {
|
|
|
13828
13939
|
const [op] = firstEntry(cond);
|
|
13829
13940
|
return unsupported(ctx, `${label}: \uC5F0\uC0B0\uC790 '${op}' \uBBF8\uC9C0\uC6D0`);
|
|
13830
13941
|
}
|
|
13942
|
+
var DATE_COMPARISONS = {
|
|
13943
|
+
equals: "==",
|
|
13944
|
+
before: "<",
|
|
13945
|
+
after: ">",
|
|
13946
|
+
on_or_before: "<=",
|
|
13947
|
+
on_or_after: ">="
|
|
13948
|
+
};
|
|
13831
13949
|
function translateDate(ref, cond, ctx, label) {
|
|
13832
13950
|
const empt = translateEmptinessOnly(ref, cond);
|
|
13833
13951
|
if (empt) return empt;
|
|
13834
13952
|
const [op, value] = firstEntry(cond);
|
|
13953
|
+
const range = Object.hasOwn(RELATIVE_DATE_RANGES, op) ? RELATIVE_DATE_RANGES[op] : void 0;
|
|
13954
|
+
if (range) return { and: relativeRangeExprs(ref, range) };
|
|
13835
13955
|
if (typeof value !== "string") {
|
|
13836
13956
|
return unsupported(ctx, `${label}: \uC0C1\uB300 \uB0A0\uC9DC \uC870\uAC74 '${op}' \uC740 \uC62E\uAE30\uC9C0 \uC54A\uC74C`);
|
|
13837
13957
|
}
|
|
13838
|
-
const
|
|
13839
|
-
|
|
13840
|
-
|
|
13841
|
-
|
|
13842
|
-
|
|
13843
|
-
|
|
13844
|
-
case "after":
|
|
13845
|
-
return `${ref} > ${lit}`;
|
|
13846
|
-
case "on_or_before":
|
|
13847
|
-
return `${ref} <= ${lit}`;
|
|
13848
|
-
case "on_or_after":
|
|
13849
|
-
return `${ref} >= ${lit}`;
|
|
13850
|
-
default:
|
|
13851
|
-
return unsupported(ctx, `${label}: \uB0A0\uC9DC \uC5F0\uC0B0\uC790 '${op}' \uBBF8\uC9C0\uC6D0`);
|
|
13958
|
+
const symbol = Object.hasOwn(DATE_COMPARISONS, op) ? DATE_COMPARISONS[op] : void 0;
|
|
13959
|
+
if (!symbol) return unsupported(ctx, `${label}: \uB0A0\uC9DC \uC5F0\uC0B0\uC790 '${op}' \uBBF8\uC9C0\uC6D0`);
|
|
13960
|
+
const relative2 = Object.hasOwn(RELATIVE_DATE_VALUES, value) ? RELATIVE_DATE_VALUES[value] : void 0;
|
|
13961
|
+
if (relative2) return `${dateOnly(ref)} ${symbol} ${relative2}`;
|
|
13962
|
+
if (!ISO_DATE_RE.test(value)) {
|
|
13963
|
+
return unsupported(ctx, `${label}: \uB0A0\uC9DC \uAC12 '${value}' \uC744 \uD574\uC11D\uD558\uC9C0 \uBABB\uD568`);
|
|
13852
13964
|
}
|
|
13965
|
+
return `${ref} ${symbol} ${str(value)}`;
|
|
13853
13966
|
}
|
|
13967
|
+
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(?:[T ][\d:.]+(?:Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
13854
13968
|
function translateEmptinessOnly(ref, cond) {
|
|
13855
13969
|
if (cond["is_empty"] === true) return `${ref}.isEmpty()`;
|
|
13856
13970
|
if (cond["is_not_empty"] === true) return `!${ref}.isEmpty()`;
|
|
@@ -13938,6 +14052,10 @@ function inFolderFilter(folderPath) {
|
|
|
13938
14052
|
var IN_FOLDER_FILTER_RE = /file\.inFolder\("([^"\n]*)"\)/g;
|
|
13939
14053
|
var BaseFileGenerator = class {
|
|
13940
14054
|
generate(options) {
|
|
14055
|
+
return this.generateWithViews(options).content;
|
|
14056
|
+
}
|
|
14057
|
+
/** `.base` 내용과 함께 쓴 보기마다 어느 Notion 보기에서 왔는지 돌려준다 — pull 이 기록해 push 가 견준다. */
|
|
14058
|
+
generateWithViews(options) {
|
|
13941
14059
|
const lines = [];
|
|
13942
14060
|
lines.push(`${BASE_FILE_HEADER} from Notion DB: ${options.databaseName}`);
|
|
13943
14061
|
lines.push(`# Database ID: ${options.databaseId}`);
|
|
@@ -13945,8 +14063,13 @@ var BaseFileGenerator = class {
|
|
|
13945
14063
|
this.writeFilters(lines, options.folderPath);
|
|
13946
14064
|
this.writeFormulas(lines, options.viewsConfig.views);
|
|
13947
14065
|
this.writePropertyDisplayNames(lines, options.schema);
|
|
13948
|
-
|
|
13949
|
-
|
|
14066
|
+
const views = this.writeViews(
|
|
14067
|
+
lines,
|
|
14068
|
+
options.viewsConfig.views,
|
|
14069
|
+
options.schema,
|
|
14070
|
+
options.rangeEnds
|
|
14071
|
+
);
|
|
14072
|
+
return { content: lines.join("\n") + "\n", views };
|
|
13950
14073
|
}
|
|
13951
14074
|
writeFilters(lines, folderPath) {
|
|
13952
14075
|
lines.push("filters:");
|
|
@@ -13977,12 +14100,13 @@ var BaseFileGenerator = class {
|
|
|
13977
14100
|
lines.push("");
|
|
13978
14101
|
}
|
|
13979
14102
|
writeViews(lines, views, schema, rangeEnds = /* @__PURE__ */ new Map()) {
|
|
13980
|
-
const
|
|
14103
|
+
const converted = views.map((v) => this.convertView(v, schema, rangeEnds)).filter((v) => v !== null);
|
|
14104
|
+
const basesViews = converted.map((c) => c.view);
|
|
13981
14105
|
if (basesViews.length === 0) {
|
|
13982
14106
|
lines.push("views:");
|
|
13983
14107
|
lines.push(" - type: table");
|
|
13984
14108
|
lines.push(" name: Table");
|
|
13985
|
-
return;
|
|
14109
|
+
return [];
|
|
13986
14110
|
}
|
|
13987
14111
|
this.dedupeViewNames(basesViews);
|
|
13988
14112
|
lines.push("views:");
|
|
@@ -14014,6 +14138,7 @@ var BaseFileGenerator = class {
|
|
|
14014
14138
|
this.writeFilterValue(lines, view.filters, " ");
|
|
14015
14139
|
}
|
|
14016
14140
|
}
|
|
14141
|
+
return converted.map(({ viewId, lossyFilter }) => ({ viewId, lossyFilter }));
|
|
14017
14142
|
}
|
|
14018
14143
|
/** `filters:` 키와 그 값을 쓴다. 단일 식이면 한 줄, 그룹이면 블록. */
|
|
14019
14144
|
writeFilterValue(lines, node, indent) {
|
|
@@ -14073,6 +14198,7 @@ var BaseFileGenerator = class {
|
|
|
14073
14198
|
convertView(view, schema, rangeEnds) {
|
|
14074
14199
|
const basesType = basesViewMappingOf(view.type).type;
|
|
14075
14200
|
if (!basesType) return null;
|
|
14201
|
+
let lossyFilter = false;
|
|
14076
14202
|
const result = {
|
|
14077
14203
|
type: basesType,
|
|
14078
14204
|
name: view.name || view.type
|
|
@@ -14086,7 +14212,7 @@ var BaseFileGenerator = class {
|
|
|
14086
14212
|
return end === void 0 ? [this.toNoteRef(name)] : [this.toNoteRef(name), this.toNoteRef(end)];
|
|
14087
14213
|
});
|
|
14088
14214
|
if (visible.length > 0) {
|
|
14089
|
-
result.order = [
|
|
14215
|
+
result.order = [BASES_TITLE, ...visible];
|
|
14090
14216
|
}
|
|
14091
14217
|
}
|
|
14092
14218
|
if (view.sorts && view.sorts.length > 0) {
|
|
@@ -14096,7 +14222,7 @@ var BaseFileGenerator = class {
|
|
|
14096
14222
|
const propName = this.resolvePropertyName(s.property, schema) ?? s.property;
|
|
14097
14223
|
property = this.toNoteRef(propName);
|
|
14098
14224
|
} else if (s.timestamp) {
|
|
14099
|
-
property = s.timestamp
|
|
14225
|
+
property = BASES_TIMESTAMPS[s.timestamp];
|
|
14100
14226
|
} else {
|
|
14101
14227
|
return null;
|
|
14102
14228
|
}
|
|
@@ -14132,14 +14258,15 @@ var BaseFileGenerator = class {
|
|
|
14132
14258
|
}
|
|
14133
14259
|
}
|
|
14134
14260
|
if (view.filter !== void 0 && view.filter !== null) {
|
|
14135
|
-
const { node } = translateNotionFilter(
|
|
14261
|
+
const { node, untranslated } = translateNotionFilter(
|
|
14136
14262
|
view.filter,
|
|
14137
14263
|
schema,
|
|
14138
14264
|
(id) => this.resolvePropertyName(id, schema)
|
|
14139
14265
|
);
|
|
14140
14266
|
if (node) result.filters = node;
|
|
14267
|
+
lossyFilter = untranslated.length > 0 || node === null;
|
|
14141
14268
|
}
|
|
14142
|
-
return result;
|
|
14269
|
+
return { view: result, viewId: view.id, lossyFilter };
|
|
14143
14270
|
}
|
|
14144
14271
|
toNoteRef(propertyName) {
|
|
14145
14272
|
return propertyName;
|
|
@@ -15230,18 +15357,17 @@ var DbBaseFiles = class {
|
|
|
15230
15357
|
get(databaseId) {
|
|
15231
15358
|
return this.load().get(compactNotionId(databaseId));
|
|
15232
15359
|
}
|
|
15360
|
+
/** 기록한 DB 모두 — 키는 줄인 DB id. */
|
|
15361
|
+
all() {
|
|
15362
|
+
return [...this.load().entries()];
|
|
15363
|
+
}
|
|
15233
15364
|
set(databaseId, info) {
|
|
15234
15365
|
const entries = this.load();
|
|
15235
15366
|
const key = compactNotionId(databaseId);
|
|
15236
15367
|
const known = entries.get(key);
|
|
15237
|
-
|
|
15238
|
-
|
|
15239
|
-
|
|
15240
|
-
entries.set(key, {
|
|
15241
|
-
basePath: info.basePath,
|
|
15242
|
-
title: info.title,
|
|
15243
|
-
...info.hash ? { hash: info.hash } : {}
|
|
15244
|
-
});
|
|
15368
|
+
const next = normalize(info);
|
|
15369
|
+
if (known && JSON.stringify(known) === JSON.stringify(next)) return;
|
|
15370
|
+
entries.set(key, next);
|
|
15245
15371
|
this.loadedRaw = JSON.stringify(Object.fromEntries(entries));
|
|
15246
15372
|
this.stateDb.setMeta(DB_BASE_FILES_META_KEY, this.loadedRaw);
|
|
15247
15373
|
}
|
|
@@ -15258,11 +15384,16 @@ var DbBaseFiles = class {
|
|
|
15258
15384
|
for (const [key, value] of Object.entries(parsed)) {
|
|
15259
15385
|
const entry = value;
|
|
15260
15386
|
if (typeof entry?.basePath === "string" && typeof entry.title === "string") {
|
|
15261
|
-
this.entries.set(
|
|
15262
|
-
|
|
15263
|
-
|
|
15264
|
-
|
|
15265
|
-
|
|
15387
|
+
this.entries.set(
|
|
15388
|
+
compactNotionId(key),
|
|
15389
|
+
normalize({
|
|
15390
|
+
basePath: entry.basePath,
|
|
15391
|
+
title: entry.title,
|
|
15392
|
+
...typeof entry.hash === "string" ? { hash: entry.hash } : {},
|
|
15393
|
+
...Array.isArray(entry.views) ? { views: entry.views } : {},
|
|
15394
|
+
...typeof entry.rest === "string" ? { rest: entry.rest } : {}
|
|
15395
|
+
})
|
|
15396
|
+
);
|
|
15266
15397
|
}
|
|
15267
15398
|
}
|
|
15268
15399
|
} catch {
|
|
@@ -15270,182 +15401,761 @@ var DbBaseFiles = class {
|
|
|
15270
15401
|
return this.entries;
|
|
15271
15402
|
}
|
|
15272
15403
|
};
|
|
15404
|
+
function normalize(info) {
|
|
15405
|
+
return {
|
|
15406
|
+
basePath: info.basePath,
|
|
15407
|
+
title: info.title,
|
|
15408
|
+
...info.hash ? { hash: info.hash } : {},
|
|
15409
|
+
...info.views && info.views.length > 0 ? { views: info.views } : {},
|
|
15410
|
+
...info.views && info.views.length > 0 && info.rest ? { rest: info.rest } : {}
|
|
15411
|
+
};
|
|
15412
|
+
}
|
|
15273
15413
|
|
|
15274
|
-
// src/sync/
|
|
15275
|
-
var
|
|
15276
|
-
|
|
15277
|
-
|
|
15278
|
-
var LINKED_DBS_META_KEY = "linked_dbs";
|
|
15279
|
-
function parseDiscoveredDbs(raw) {
|
|
15280
|
-
if (!raw) return [];
|
|
15414
|
+
// src/sync/base-view-record.ts
|
|
15415
|
+
var PUSHABLE_KEYS = ["name", "order", "sort", "filters"];
|
|
15416
|
+
function readBaseFile(text) {
|
|
15417
|
+
let doc;
|
|
15281
15418
|
try {
|
|
15282
|
-
|
|
15283
|
-
|
|
15419
|
+
doc = parseFrontmatter(`---
|
|
15420
|
+
${text}
|
|
15421
|
+
---
|
|
15422
|
+
`).data;
|
|
15284
15423
|
} catch {
|
|
15285
|
-
return
|
|
15424
|
+
return null;
|
|
15286
15425
|
}
|
|
15426
|
+
const views = doc["views"] ?? [];
|
|
15427
|
+
if (!Array.isArray(views)) return null;
|
|
15428
|
+
if (views.some((view) => !view || typeof view !== "object" || Array.isArray(view))) return null;
|
|
15429
|
+
return { doc, views };
|
|
15287
15430
|
}
|
|
15288
|
-
function
|
|
15289
|
-
const
|
|
15290
|
-
|
|
15291
|
-
|
|
15292
|
-
|
|
15293
|
-
|
|
15294
|
-
|
|
15295
|
-
|
|
15296
|
-
}
|
|
15431
|
+
function pushableOf(view) {
|
|
15432
|
+
const order = Array.isArray(view["order"]) ? view["order"].map((c) => columnName(c)) : [];
|
|
15433
|
+
const sort = Array.isArray(view["sort"]) ? view["sort"].map((s) => {
|
|
15434
|
+
const entry = s ?? {};
|
|
15435
|
+
return {
|
|
15436
|
+
property: columnName(entry["property"]),
|
|
15437
|
+
direction: String(entry["direction"] ?? "ASC").toUpperCase()
|
|
15438
|
+
};
|
|
15439
|
+
}) : [];
|
|
15440
|
+
return {
|
|
15441
|
+
name: typeof view["name"] === "string" ? view["name"] : "",
|
|
15442
|
+
...order.length > 0 ? { order } : {},
|
|
15443
|
+
...sort.length > 0 ? { sort } : {},
|
|
15444
|
+
...view["filters"] !== void 0 && view["filters"] !== null ? { filters: view["filters"] } : {}
|
|
15445
|
+
};
|
|
15297
15446
|
}
|
|
15298
|
-
function
|
|
15299
|
-
const
|
|
15300
|
-
if (
|
|
15301
|
-
|
|
15302
|
-
|
|
15303
|
-
|
|
15304
|
-
|
|
15305
|
-
return /* @__PURE__ */ new Map();
|
|
15306
|
-
}
|
|
15447
|
+
function changedFields(record, edited) {
|
|
15448
|
+
const changed = [];
|
|
15449
|
+
if (record.name !== edited.name) changed.push("name");
|
|
15450
|
+
if (canonical(record.order) !== canonical(edited.order)) changed.push("columns");
|
|
15451
|
+
if (canonical(record.sort) !== canonical(edited.sort)) changed.push("sort");
|
|
15452
|
+
if (canonical(record.filters) !== canonical(edited.filters)) changed.push("filter");
|
|
15453
|
+
return changed;
|
|
15307
15454
|
}
|
|
15308
|
-
|
|
15309
|
-
|
|
15310
|
-
|
|
15311
|
-
const folders = moves.filter((move) => !isMarkdownPath(move.from));
|
|
15312
|
-
if (folders.length === 0) return;
|
|
15313
|
-
const databases = parseDiscoveredDbs(stateDb.getMeta(DISCOVERED_DBS_META_KEY));
|
|
15314
|
-
let changed = false;
|
|
15315
|
-
for (const db of databases) {
|
|
15316
|
-
const folder = db.localFolder.replace(/\/+$/, "");
|
|
15317
|
-
const to = finalPath(folder, folders);
|
|
15318
|
-
if (to === folder) continue;
|
|
15319
|
-
db.localFolder = to;
|
|
15320
|
-
changed = true;
|
|
15321
|
-
}
|
|
15322
|
-
if (changed) stateDb.setMeta(DISCOVERED_DBS_META_KEY, JSON.stringify(databases));
|
|
15323
|
-
const raw = stateDb.getMeta(DB_BASE_FILES_META_KEY);
|
|
15324
|
-
if (!raw) return;
|
|
15325
|
-
let entries;
|
|
15326
|
-
try {
|
|
15327
|
-
entries = JSON.parse(raw);
|
|
15328
|
-
} catch {
|
|
15329
|
-
return;
|
|
15330
|
-
}
|
|
15331
|
-
if (!entries || typeof entries !== "object") return;
|
|
15332
|
-
let moved = false;
|
|
15333
|
-
for (const entry of Object.values(entries)) {
|
|
15334
|
-
if (typeof entry?.basePath !== "string") continue;
|
|
15335
|
-
const to = finalPath(entry.basePath, folders);
|
|
15336
|
-
if (to === entry.basePath) continue;
|
|
15337
|
-
entry.basePath = to;
|
|
15338
|
-
moved = true;
|
|
15339
|
-
}
|
|
15340
|
-
if (moved) stateDb.setMeta(DB_BASE_FILES_META_KEY, JSON.stringify(entries));
|
|
15455
|
+
function columnName(raw) {
|
|
15456
|
+
const name = String(raw ?? "");
|
|
15457
|
+
return name.startsWith("note.") ? name.slice("note.".length) : name;
|
|
15341
15458
|
}
|
|
15342
|
-
|
|
15343
|
-
const
|
|
15344
|
-
|
|
15345
|
-
|
|
15346
|
-
|
|
15347
|
-
|
|
15348
|
-
|
|
15349
|
-
|
|
15350
|
-
|
|
15351
|
-
|
|
15352
|
-
|
|
15353
|
-
|
|
15354
|
-
|
|
15459
|
+
function matchViews(records, views) {
|
|
15460
|
+
const used = /* @__PURE__ */ new Set();
|
|
15461
|
+
const out = views.map((view) => {
|
|
15462
|
+
const found2 = records.find((r) => !used.has(r.viewId) && r.name === view["name"]);
|
|
15463
|
+
if (found2) used.add(found2.viewId);
|
|
15464
|
+
return found2;
|
|
15465
|
+
});
|
|
15466
|
+
views.forEach((view, i) => {
|
|
15467
|
+
if (out[i]) return;
|
|
15468
|
+
const record = records[i];
|
|
15469
|
+
if (record && !used.has(record.viewId) && record.type === view["type"]) {
|
|
15470
|
+
used.add(record.viewId);
|
|
15471
|
+
out[i] = record;
|
|
15355
15472
|
}
|
|
15356
|
-
}
|
|
15357
|
-
|
|
15358
|
-
|
|
15359
|
-
|
|
15473
|
+
});
|
|
15474
|
+
return out;
|
|
15475
|
+
}
|
|
15476
|
+
function restSignature(parsed, matched) {
|
|
15477
|
+
const without = (from, keys) => Object.fromEntries(Object.entries(from).filter(([key]) => !keys.includes(key)));
|
|
15478
|
+
const views = {};
|
|
15479
|
+
parsed.views.forEach((view, i) => {
|
|
15480
|
+
const key = matched[i] ? `id:${matched[i].viewId}` : `new:${String(view["name"])}`;
|
|
15481
|
+
views[key] = without(view, PUSHABLE_KEYS);
|
|
15482
|
+
});
|
|
15483
|
+
return computeHash(canonical({ doc: without(parsed.doc, ["views"]), views }));
|
|
15484
|
+
}
|
|
15485
|
+
function viewFingerprint(view) {
|
|
15486
|
+
return computeHash(
|
|
15487
|
+
canonical({
|
|
15488
|
+
name: view.name,
|
|
15489
|
+
type: view.type,
|
|
15490
|
+
filter: view.filter ?? null,
|
|
15491
|
+
sorts: view.sorts ?? null,
|
|
15492
|
+
configuration: view.configuration ?? null
|
|
15493
|
+
})
|
|
15494
|
+
);
|
|
15495
|
+
}
|
|
15496
|
+
function recordBaseViews(content, written, views) {
|
|
15497
|
+
const parsed = readBaseFile(content);
|
|
15498
|
+
if (!parsed || written.length !== parsed.views.length) return null;
|
|
15499
|
+
const byId = new Map(views.map((view) => [view.id, view]));
|
|
15500
|
+
const records = [];
|
|
15501
|
+
for (const [i, source] of written.entries()) {
|
|
15502
|
+
const view = byId.get(source.viewId);
|
|
15503
|
+
if (!view) return null;
|
|
15504
|
+
records.push({
|
|
15505
|
+
viewId: source.viewId,
|
|
15506
|
+
type: String(parsed.views[i]["type"]),
|
|
15507
|
+
...pushableOf(parsed.views[i]),
|
|
15508
|
+
...source.lossyFilter ? { lossyFilter: true } : {},
|
|
15509
|
+
notion: viewFingerprint(view)
|
|
15510
|
+
});
|
|
15360
15511
|
}
|
|
15512
|
+
return { views: records, rest: restSignature(parsed, records) };
|
|
15361
15513
|
}
|
|
15362
|
-
function
|
|
15363
|
-
|
|
15364
|
-
|
|
15365
|
-
|
|
15366
|
-
|
|
15367
|
-
|
|
15514
|
+
function canonical(value) {
|
|
15515
|
+
return JSON.stringify(sortKeys3(value)) ?? "null";
|
|
15516
|
+
}
|
|
15517
|
+
function sortKeys3(value) {
|
|
15518
|
+
if (Array.isArray(value)) return value.map(sortKeys3);
|
|
15519
|
+
if (value instanceof Date) return value.toISOString();
|
|
15520
|
+
if (value && typeof value === "object") {
|
|
15521
|
+
return Object.fromEntries(
|
|
15522
|
+
Object.keys(value).sort().map((key) => [key, sortKeys3(value[key])])
|
|
15523
|
+
);
|
|
15368
15524
|
}
|
|
15369
|
-
return
|
|
15525
|
+
return value;
|
|
15370
15526
|
}
|
|
15371
15527
|
|
|
15372
|
-
// src/sync/
|
|
15373
|
-
var
|
|
15374
|
-
|
|
15528
|
+
// src/sync/sidecar-bases.ts
|
|
15529
|
+
var SIDECAR_BASES_META_KEY = "sidecar_bases";
|
|
15530
|
+
var SidecarBases = class {
|
|
15531
|
+
constructor(stateDb) {
|
|
15375
15532
|
this.stateDb = stateDb;
|
|
15376
|
-
this.vaultFs = vaultFs;
|
|
15377
|
-
this.configuredDatabases = configuredDatabases;
|
|
15378
15533
|
}
|
|
15379
15534
|
stateDb;
|
|
15380
|
-
|
|
15381
|
-
|
|
15382
|
-
/**
|
|
15383
|
-
* 함께 옮길 것 — 폴더로 추적하는 페이지는 그 폴더, 폴더 노트(`X/X.md`)는 든 폴더, 노트는 그 파일과 옆의 자식
|
|
15384
|
-
* 폴더(`X/`, 있으면).
|
|
15385
|
-
*/
|
|
15386
|
-
unitsOf(record) {
|
|
15387
|
-
if (isFolderRecord(record)) return [record.obsidianPath];
|
|
15388
|
-
if (record.fileType !== "file") return [parentFolderOf(record.obsidianPath)];
|
|
15389
|
-
const children = stripMarkdownExtension(record.obsidianPath);
|
|
15390
|
-
const hasChildren = this.stateDb.getAll().some((r) => within(r.obsidianPath, children));
|
|
15391
|
-
return hasChildren ? [record.obsidianPath, children] : [record.obsidianPath];
|
|
15535
|
+
get(databaseId) {
|
|
15536
|
+
return this.load()[compactNotionId(databaseId)];
|
|
15392
15537
|
}
|
|
15393
|
-
|
|
15394
|
-
|
|
15395
|
-
|
|
15396
|
-
if (
|
|
15397
|
-
|
|
15398
|
-
|
|
15399
|
-
|
|
15400
|
-
|
|
15538
|
+
set(databaseId, base) {
|
|
15539
|
+
const all = this.load();
|
|
15540
|
+
const key = compactNotionId(databaseId);
|
|
15541
|
+
if (JSON.stringify(all[key]) === JSON.stringify(base)) return;
|
|
15542
|
+
all[key] = base;
|
|
15543
|
+
this.stateDb.setMeta(SIDECAR_BASES_META_KEY, JSON.stringify(all));
|
|
15544
|
+
}
|
|
15545
|
+
/** 기록한 DB 마다 — 하이픈 없는 id 와 그 기록. */
|
|
15546
|
+
entries() {
|
|
15547
|
+
return Object.entries(this.load());
|
|
15548
|
+
}
|
|
15549
|
+
load() {
|
|
15550
|
+
const raw = this.stateDb.getMeta(SIDECAR_BASES_META_KEY);
|
|
15551
|
+
if (!raw) return {};
|
|
15552
|
+
try {
|
|
15553
|
+
const parsed = JSON.parse(raw);
|
|
15554
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
15555
|
+
const out = {};
|
|
15556
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
15557
|
+
const entry = value;
|
|
15558
|
+
if (typeof entry?.path === "string" && typeof entry.hash === "string" && Array.isArray(entry.properties)) {
|
|
15559
|
+
out[key] = { path: entry.path, hash: entry.hash, properties: entry.properties };
|
|
15560
|
+
}
|
|
15561
|
+
}
|
|
15562
|
+
return out;
|
|
15563
|
+
} catch {
|
|
15564
|
+
return {};
|
|
15401
15565
|
}
|
|
15402
|
-
|
|
15403
|
-
|
|
15566
|
+
}
|
|
15567
|
+
};
|
|
15568
|
+
|
|
15569
|
+
// src/sync/schema-ledger.ts
|
|
15570
|
+
var DB_SCHEMAS_META_KEY = "db_schemas";
|
|
15571
|
+
function seenSchemaOf(meta) {
|
|
15572
|
+
const seen = {};
|
|
15573
|
+
if (meta.dataSources.length === 0) {
|
|
15574
|
+
if (Object.keys(meta.properties).length > 0) {
|
|
15575
|
+
seen[compactNotionId(meta.databaseId)] = schemaOf(meta.properties);
|
|
15404
15576
|
}
|
|
15405
|
-
|
|
15406
|
-
|
|
15577
|
+
} else {
|
|
15578
|
+
for (const ds of meta.dataSources) {
|
|
15579
|
+
if (ds.properties) seen[compactNotionId(ds.id)] = schemaOf(ds.properties);
|
|
15407
15580
|
}
|
|
15408
|
-
return null;
|
|
15409
15581
|
}
|
|
15410
|
-
|
|
15411
|
-
|
|
15412
|
-
|
|
15582
|
+
return Object.keys(seen).length > 0 ? seen : null;
|
|
15583
|
+
}
|
|
15584
|
+
function dataSourceIdOf(page) {
|
|
15585
|
+
const parent = page.parent;
|
|
15586
|
+
return parent?.type === "data_source_id" && parent.data_source_id ? parent.data_source_id : null;
|
|
15587
|
+
}
|
|
15588
|
+
function sourceKeyOf(page, databaseId) {
|
|
15589
|
+
return compactNotionId(dataSourceIdOf(page) ?? databaseId);
|
|
15590
|
+
}
|
|
15591
|
+
function schemaOf(properties) {
|
|
15592
|
+
const schema = {};
|
|
15593
|
+
for (const [name, raw] of Object.entries(properties)) {
|
|
15594
|
+
const prop = raw;
|
|
15595
|
+
if (typeof prop?.["id"] !== "string" || typeof prop["type"] !== "string") continue;
|
|
15596
|
+
if (prop["type"] === "title") continue;
|
|
15597
|
+
const values = valuesOf(prop["type"], prop[prop["type"]]);
|
|
15598
|
+
schema[prop["id"]] = { name, type: prop["type"], ...values ? { values } : {} };
|
|
15599
|
+
}
|
|
15600
|
+
return schema;
|
|
15601
|
+
}
|
|
15602
|
+
function valuesOf(type, config) {
|
|
15603
|
+
const c = config ?? {};
|
|
15604
|
+
let decides;
|
|
15605
|
+
if (type === "select" || type === "multi_select" || type === "status") {
|
|
15606
|
+
const options = Array.isArray(c["options"]) ? c["options"] : [];
|
|
15607
|
+
decides = options.map((o) => String(o?.name ?? "")).sort();
|
|
15608
|
+
} else if (type === "formula") {
|
|
15609
|
+
decides = c["expression"] ?? null;
|
|
15610
|
+
} else if (type === "rollup") {
|
|
15611
|
+
decides = [c["relation_property_id"], c["rollup_property_id"], c["function"]];
|
|
15612
|
+
} else {
|
|
15613
|
+
return void 0;
|
|
15413
15614
|
}
|
|
15414
|
-
|
|
15415
|
-
|
|
15416
|
-
|
|
15417
|
-
|
|
15418
|
-
|
|
15419
|
-
|
|
15420
|
-
|
|
15421
|
-
|
|
15422
|
-
|
|
15423
|
-
|
|
15615
|
+
return computeHash(JSON.stringify(decides)).slice(0, 16);
|
|
15616
|
+
}
|
|
15617
|
+
function diffSourceSchema(before, after) {
|
|
15618
|
+
const renamed = /* @__PURE__ */ new Map();
|
|
15619
|
+
const removed = /* @__PURE__ */ new Set();
|
|
15620
|
+
const refreshed = /* @__PURE__ */ new Set();
|
|
15621
|
+
const withEnd = (name, type) => mayHaveRangeEnd(type) ? [name, name + RANGE_END_SUFFIX] : [name];
|
|
15622
|
+
for (const [id, old] of Object.entries(before)) {
|
|
15623
|
+
const now = after[id];
|
|
15624
|
+
if (!now) {
|
|
15625
|
+
for (const key of withEnd(old.name, old.type)) removed.add(key);
|
|
15626
|
+
continue;
|
|
15627
|
+
}
|
|
15628
|
+
if (now.name !== old.name) {
|
|
15629
|
+
renamed.set(old.name, now.name);
|
|
15630
|
+
if (mayHaveRangeEnd(old.type) && mayHaveRangeEnd(now.type)) {
|
|
15631
|
+
renamed.set(old.name + RANGE_END_SUFFIX, now.name + RANGE_END_SUFFIX);
|
|
15424
15632
|
}
|
|
15425
|
-
} catch (error) {
|
|
15426
|
-
for (const move of done.reverse())
|
|
15427
|
-
await this.vaultFs.moveFile(move.to, move.from).catch(() => {
|
|
15428
|
-
});
|
|
15429
|
-
throw error;
|
|
15430
15633
|
}
|
|
15431
|
-
|
|
15432
|
-
|
|
15433
|
-
|
|
15434
|
-
|
|
15435
|
-
});
|
|
15436
|
-
await retargetBaseFiles(this.vaultFs, moves);
|
|
15634
|
+
const revalued = old.values !== void 0 && now.values !== void 0 && old.values !== now.values;
|
|
15635
|
+
if (now.type !== old.type || revalued) {
|
|
15636
|
+
for (const key of withEnd(now.name, now.type)) refreshed.add(key);
|
|
15637
|
+
}
|
|
15437
15638
|
}
|
|
15438
|
-
|
|
15439
|
-
|
|
15440
|
-
(db) => within(db.localFolder.replace(/\/+$/, ""), folder)
|
|
15441
|
-
);
|
|
15639
|
+
for (const [id, now] of Object.entries(after)) {
|
|
15640
|
+
if (!before[id]) for (const key of withEnd(now.name, now.type)) refreshed.add(key);
|
|
15442
15641
|
}
|
|
15443
|
-
|
|
15444
|
-
|
|
15445
|
-
|
|
15446
|
-
|
|
15447
|
-
|
|
15448
|
-
|
|
15642
|
+
return { renamed, removed, refreshed };
|
|
15643
|
+
}
|
|
15644
|
+
function isEmptySchemaChange(change) {
|
|
15645
|
+
return change.renamed.size === 0 && change.removed.size === 0 && change.refreshed.size === 0;
|
|
15646
|
+
}
|
|
15647
|
+
var SchemaLedger = class {
|
|
15648
|
+
constructor(stateDb) {
|
|
15649
|
+
this.stateDb = stateDb;
|
|
15650
|
+
}
|
|
15651
|
+
stateDb;
|
|
15652
|
+
get(databaseId) {
|
|
15653
|
+
return this.load()[compactNotionId(databaseId)];
|
|
15654
|
+
}
|
|
15655
|
+
/** 이번에 본 소스로 바꾼다 — 이번에 읽지 못한 소스는 지난번 것을 둔다. */
|
|
15656
|
+
set(databaseId, seen) {
|
|
15657
|
+
const all = this.load();
|
|
15658
|
+
const key = compactNotionId(databaseId);
|
|
15659
|
+
const next = { ...all[key], ...seen };
|
|
15660
|
+
if (JSON.stringify(all[key]) === JSON.stringify(next)) return;
|
|
15661
|
+
all[key] = next;
|
|
15662
|
+
this.stateDb.setMeta(DB_SCHEMAS_META_KEY, JSON.stringify(all));
|
|
15663
|
+
}
|
|
15664
|
+
load() {
|
|
15665
|
+
const raw = this.stateDb.getMeta(DB_SCHEMAS_META_KEY);
|
|
15666
|
+
if (!raw) return {};
|
|
15667
|
+
try {
|
|
15668
|
+
const parsed = JSON.parse(raw);
|
|
15669
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
15670
|
+
} catch {
|
|
15671
|
+
return {};
|
|
15672
|
+
}
|
|
15673
|
+
}
|
|
15674
|
+
};
|
|
15675
|
+
|
|
15676
|
+
// src/sync/sidecar-schema-edit.ts
|
|
15677
|
+
var DEFINABLE_TYPES = /* @__PURE__ */ new Set([
|
|
15678
|
+
"formula",
|
|
15679
|
+
"number",
|
|
15680
|
+
"select",
|
|
15681
|
+
"multi_select"
|
|
15682
|
+
]);
|
|
15683
|
+
var SidecarSchemaEdits = class {
|
|
15684
|
+
constructor(stateDb, vaultFs, notionClient) {
|
|
15685
|
+
this.vaultFs = vaultFs;
|
|
15686
|
+
this.notionClient = notionClient;
|
|
15687
|
+
this.bases = new SidecarBases(stateDb);
|
|
15688
|
+
this.ledger = new SchemaLedger(stateDb);
|
|
15689
|
+
}
|
|
15690
|
+
vaultFs;
|
|
15691
|
+
notionClient;
|
|
15692
|
+
bases;
|
|
15693
|
+
ledger;
|
|
15694
|
+
/** 고친 사이드카를 찾아 보낼 것을 정한다 — Notion 은 고친 것의 DB 만 읽는다. */
|
|
15695
|
+
async plan(inScope = () => true) {
|
|
15696
|
+
const planned = [];
|
|
15697
|
+
const refused = [];
|
|
15698
|
+
const refuse2 = (path, error) => {
|
|
15699
|
+
refused.push({
|
|
15700
|
+
path,
|
|
15701
|
+
operation: "update",
|
|
15702
|
+
error: `\uC0AC\uC774\uB4DC\uCE74\uB97C Notion \uC5D0 \uC62C\uB9AC\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4 \u2014 ${error}`
|
|
15703
|
+
});
|
|
15704
|
+
};
|
|
15705
|
+
for (const [databaseId, base] of this.bases.entries()) {
|
|
15706
|
+
if (!inScope(base.path)) continue;
|
|
15707
|
+
const content = await this.vaultFs.readFile(base.path).catch(() => null);
|
|
15708
|
+
if (content === null) continue;
|
|
15709
|
+
const hash = computeHash(content);
|
|
15710
|
+
if (hash === base.hash) continue;
|
|
15711
|
+
let edited;
|
|
15712
|
+
try {
|
|
15713
|
+
edited = JSON.parse(content);
|
|
15714
|
+
} catch {
|
|
15715
|
+
refuse2(base.path, "JSON \uC73C\uB85C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
15716
|
+
continue;
|
|
15717
|
+
}
|
|
15718
|
+
const sidecar = edited;
|
|
15719
|
+
if (sidecar.generator !== SIDECAR_GENERATOR) continue;
|
|
15720
|
+
if (typeof sidecar.databaseId !== "string" || compactNotionId(sidecar.databaseId) !== databaseId) {
|
|
15721
|
+
refuse2(base.path, "databaseId \uB97C \uBC14\uAFC0 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
15722
|
+
continue;
|
|
15723
|
+
}
|
|
15724
|
+
const properties = parseProperties(sidecar.properties);
|
|
15725
|
+
if (properties === null) {
|
|
15726
|
+
refuse2(base.path, "properties \uB294 name \xB7 type \uC774 \uC788\uB294 \uD56D\uBAA9\uC758 \uBAA9\uB85D\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4");
|
|
15727
|
+
continue;
|
|
15728
|
+
}
|
|
15729
|
+
const diff = diffSidecarProperties(base.properties, properties);
|
|
15730
|
+
if (!diff.ok) {
|
|
15731
|
+
refuse2(base.path, diff.error);
|
|
15732
|
+
continue;
|
|
15733
|
+
}
|
|
15734
|
+
if (Object.keys(diff.payload).length === 0) continue;
|
|
15735
|
+
const meta = await this.notionClient.getDatabaseMeta(databaseId);
|
|
15736
|
+
if (meta.dataSources.length !== 1) {
|
|
15737
|
+
refuse2(
|
|
15738
|
+
base.path,
|
|
15739
|
+
"\uB370\uC774\uD130 \uC18C\uC2A4\uAC00 \uC5EC\uB7FF\uC778 DB \uC758 \uC18D\uC131\uC740 \uC0AC\uC774\uB4DC\uCE74\uB85C \uBC14\uAFB8\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. Notion \uC5D0\uC11C \uBC14\uAFB8\uC138\uC694"
|
|
15740
|
+
);
|
|
15741
|
+
continue;
|
|
15742
|
+
}
|
|
15743
|
+
const source = meta.dataSources[0];
|
|
15744
|
+
const live = source.properties ?? meta.properties;
|
|
15745
|
+
if (!sameSchema(base.properties, live)) {
|
|
15746
|
+
refuse2(
|
|
15747
|
+
base.path,
|
|
15748
|
+
"\uADF8 \uC0AC\uC774 Notion \uC5D0\uC11C\uB3C4 \uC18D\uC131\uC774 \uBC14\uB00C\uC5C8\uC2B5\uB2C8\uB2E4. \uACE0\uCE5C \uB0B4\uC6A9\uC744 \uC62E\uACA8 \uC801\uACE0 \uC774 \uD30C\uC77C\uC744 \uC9C0\uC6B4 \uB4A4 pull \uD558\uBA74 \uC0C8\uB85C \uBC1B\uC2B5\uB2C8\uB2E4"
|
|
15749
|
+
);
|
|
15750
|
+
continue;
|
|
15751
|
+
}
|
|
15752
|
+
planned.push({
|
|
15753
|
+
databaseId,
|
|
15754
|
+
dataSourceId: source.id,
|
|
15755
|
+
path: base.path,
|
|
15756
|
+
hash,
|
|
15757
|
+
properties,
|
|
15758
|
+
payload: diff.payload,
|
|
15759
|
+
edit: { path: base.path, ...diff.edit },
|
|
15760
|
+
seen: this.ledger.get(databaseId) ? null : seenSchemaOf(meta)
|
|
15761
|
+
});
|
|
15762
|
+
}
|
|
15763
|
+
return { planned, refused };
|
|
15764
|
+
}
|
|
15765
|
+
async apply(planned) {
|
|
15766
|
+
const applied = [];
|
|
15767
|
+
const failed = [];
|
|
15768
|
+
for (const edit of planned) {
|
|
15769
|
+
if (edit.seen) this.ledger.set(edit.databaseId, edit.seen);
|
|
15770
|
+
let after;
|
|
15771
|
+
try {
|
|
15772
|
+
after = await this.notionClient.updateDataSourceProperties(edit.dataSourceId, edit.payload);
|
|
15773
|
+
} catch (error) {
|
|
15774
|
+
failed.push({
|
|
15775
|
+
path: edit.path,
|
|
15776
|
+
operation: "update",
|
|
15777
|
+
error: `\uC0AC\uC774\uB4DC\uCE74\uC758 \uC18D\uC131\uC744 Notion \uC5D0 \uC62C\uB9AC\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4: ${error instanceof Error ? error.message : String(error)}`
|
|
15778
|
+
});
|
|
15779
|
+
continue;
|
|
15780
|
+
}
|
|
15781
|
+
this.bases.set(edit.databaseId, {
|
|
15782
|
+
path: edit.path,
|
|
15783
|
+
hash: edit.hash,
|
|
15784
|
+
properties: edit.properties.map((prop) => {
|
|
15785
|
+
const id = prop.id ?? after[prop.name]?.["id"];
|
|
15786
|
+
return typeof id === "string" ? { ...prop, id } : prop;
|
|
15787
|
+
})
|
|
15788
|
+
});
|
|
15789
|
+
applied.push(edit.edit);
|
|
15790
|
+
}
|
|
15791
|
+
return { applied, failed };
|
|
15792
|
+
}
|
|
15793
|
+
};
|
|
15794
|
+
function sidecarPropertiesEdited(content, base) {
|
|
15795
|
+
let parsed;
|
|
15796
|
+
try {
|
|
15797
|
+
parsed = JSON.parse(content);
|
|
15798
|
+
} catch {
|
|
15799
|
+
return true;
|
|
15800
|
+
}
|
|
15801
|
+
const properties = parseProperties(parsed.properties);
|
|
15802
|
+
if (properties === null) return true;
|
|
15803
|
+
const diff = diffSidecarProperties(base.properties, properties);
|
|
15804
|
+
return !diff.ok || Object.keys(diff.payload).length > 0;
|
|
15805
|
+
}
|
|
15806
|
+
function parseProperties(value) {
|
|
15807
|
+
if (!Array.isArray(value)) return null;
|
|
15808
|
+
const out = [];
|
|
15809
|
+
for (const item of value) {
|
|
15810
|
+
const prop = item;
|
|
15811
|
+
if (!prop || typeof prop !== "object" || typeof prop.name !== "string" || prop.name.trim() === "" || typeof prop.type !== "string" || prop.id !== void 0 && typeof prop.id !== "string" || prop.options !== void 0 && !(Array.isArray(prop.options) && prop.options.every((o) => typeof o === "string")) || prop.definition !== void 0 && (typeof prop.definition !== "object" || prop.definition === null || Array.isArray(prop.definition))) {
|
|
15812
|
+
return null;
|
|
15813
|
+
}
|
|
15814
|
+
out.push(prop);
|
|
15815
|
+
}
|
|
15816
|
+
return out;
|
|
15817
|
+
}
|
|
15818
|
+
function diffSidecarProperties(base, edited) {
|
|
15819
|
+
const fail = (error) => ({ ok: false, error });
|
|
15820
|
+
const byId = new Map(base.map((prop) => [prop.id, prop]));
|
|
15821
|
+
const payload = {};
|
|
15822
|
+
const edit = {
|
|
15823
|
+
added: [],
|
|
15824
|
+
removed: [],
|
|
15825
|
+
renamed: [],
|
|
15826
|
+
retyped: [],
|
|
15827
|
+
redefined: []
|
|
15828
|
+
};
|
|
15829
|
+
const droppedOptions = [];
|
|
15830
|
+
const names = /* @__PURE__ */ new Set();
|
|
15831
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
15832
|
+
for (const prop of edited) {
|
|
15833
|
+
if (names.has(prop.name)) return fail(`\uC18D\uC131 \uC774\uB984 \u300C${prop.name}\u300D \uC774 \uB458\uC785\uB2C8\uB2E4`);
|
|
15834
|
+
names.add(prop.name);
|
|
15835
|
+
const old = prop.id === void 0 ? void 0 : byId.get(prop.id);
|
|
15836
|
+
if (prop.id !== void 0 && !old) {
|
|
15837
|
+
return fail(`\u300C${prop.name}\u300D \uC758 id \uB97C \uBAA8\uB985\uB2C8\uB2E4 \u2014 \uC0C8 \uC18D\uC131\uC740 id \uB97C \uC801\uC9C0 \uB9C8\uC138\uC694`);
|
|
15838
|
+
}
|
|
15839
|
+
if (old && seenIds.has(old.id)) return fail(`\u300C${prop.name}\u300D \uC758 id \uAC00 \uB458\uC785\uB2C8\uB2E4`);
|
|
15840
|
+
if (!old) {
|
|
15841
|
+
if (prop.type === "title") return fail("\uC81C\uBAA9 \uC18D\uC131\uC740 \uB354\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
15842
|
+
payload[prop.name] = { [prop.type]: definitionOf(prop, void 0) };
|
|
15843
|
+
edit.added.push(prop.name);
|
|
15844
|
+
continue;
|
|
15845
|
+
}
|
|
15846
|
+
seenIds.add(old.id);
|
|
15847
|
+
const change = {};
|
|
15848
|
+
if (prop.name !== old.name) {
|
|
15849
|
+
change["name"] = prop.name;
|
|
15850
|
+
edit.renamed.push([old.name, prop.name]);
|
|
15851
|
+
}
|
|
15852
|
+
if (prop.type !== old.type) {
|
|
15853
|
+
if (old.type === "title" || prop.type === "title") {
|
|
15854
|
+
return fail("\uC81C\uBAA9 \uC18D\uC131\uC758 \uD0C0\uC785\uC740 \uBC14\uAFC0 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
15855
|
+
}
|
|
15856
|
+
change[prop.type] = definitionOf(prop, void 0);
|
|
15857
|
+
edit.retyped.push([prop.name, prop.type]);
|
|
15858
|
+
} else if (!sameDefinition(prop, old)) {
|
|
15859
|
+
if (!DEFINABLE_TYPES.has(prop.type)) {
|
|
15860
|
+
return fail(
|
|
15861
|
+
`\u300C${prop.name}\u300D(${prop.type}) \uC758 \uC124\uC815\uC740 \uC0AC\uC774\uB4DC\uCE74\uC5D0\uC11C \uBC14\uAFC0 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. Notion \uC5D0\uC11C \uBC14\uAFB8\uC138\uC694`
|
|
15862
|
+
);
|
|
15863
|
+
}
|
|
15864
|
+
change[prop.type] = definitionOf(prop, old);
|
|
15865
|
+
edit.redefined.push(prop.name);
|
|
15866
|
+
const dropped = (old.options ?? []).filter((name) => !(prop.options ?? []).includes(name));
|
|
15867
|
+
if (dropped.length > 0) droppedOptions.push([prop.name, dropped]);
|
|
15868
|
+
}
|
|
15869
|
+
if (Object.keys(change).length > 0) payload[old.id] = change;
|
|
15870
|
+
}
|
|
15871
|
+
for (const old of base) {
|
|
15872
|
+
if (seenIds.has(old.id)) continue;
|
|
15873
|
+
if (old.type === "title") return fail("\uC81C\uBAA9 \uC18D\uC131\uC740 \uC9C0\uC6B8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
15874
|
+
payload[old.id] = null;
|
|
15875
|
+
edit.removed.push(old.name);
|
|
15876
|
+
}
|
|
15877
|
+
const kept = base.filter((p) => p.type !== "title").length;
|
|
15878
|
+
if (kept >= 2 && edit.removed.length === kept && edit.added.length === 0) {
|
|
15879
|
+
return fail("\uC81C\uBAA9 \uB9D0\uACE0 \uBAA8\uB4E0 \uC18D\uC131\uC744 \uC9C0\uC6B0\uB294 \uAC83\uC740 \uBC1B\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 \uC815\uB9D0\uC774\uBA74 Notion \uC5D0\uC11C \uC9C0\uC6B0\uC138\uC694");
|
|
15880
|
+
}
|
|
15881
|
+
return {
|
|
15882
|
+
ok: true,
|
|
15883
|
+
payload,
|
|
15884
|
+
edit: droppedOptions.length > 0 ? { ...edit, droppedOptions } : edit
|
|
15885
|
+
};
|
|
15886
|
+
}
|
|
15887
|
+
function definitionOf(prop, old) {
|
|
15888
|
+
if (!DEFINABLE_TYPES.has(prop.type)) return {};
|
|
15889
|
+
const definition = prop.definition ?? {};
|
|
15890
|
+
if (old && (prop.type === "select" || prop.type === "multi_select") && sameJson(prop.definition, old.definition) && !sameJson(prop.options, old.options)) {
|
|
15891
|
+
const known = old.definition?.["options"] ?? [];
|
|
15892
|
+
return {
|
|
15893
|
+
options: (prop.options ?? []).map((name) => known.find((o) => o.name === name) ?? { name })
|
|
15894
|
+
};
|
|
15895
|
+
}
|
|
15896
|
+
return definition;
|
|
15897
|
+
}
|
|
15898
|
+
function sameDefinition(a, b) {
|
|
15899
|
+
return sameJson(a.definition, b.definition) && sameJson(a.options, b.options);
|
|
15900
|
+
}
|
|
15901
|
+
function sameSchema(base, live) {
|
|
15902
|
+
const liveById = /* @__PURE__ */ new Map();
|
|
15903
|
+
for (const [name, raw] of Object.entries(live)) {
|
|
15904
|
+
const prop = raw;
|
|
15905
|
+
if (typeof prop?.id === "string") liveById.set(prop.id, { name, type: prop.type });
|
|
15906
|
+
}
|
|
15907
|
+
return liveById.size === base.length && base.every((prop) => {
|
|
15908
|
+
const now = liveById.get(prop.id);
|
|
15909
|
+
return now?.name === prop.name && now.type === prop.type;
|
|
15910
|
+
});
|
|
15911
|
+
}
|
|
15912
|
+
function sameJson(a, b) {
|
|
15913
|
+
return JSON.stringify(sortKeys4(a)) === JSON.stringify(sortKeys4(b));
|
|
15914
|
+
}
|
|
15915
|
+
function sortKeys4(value) {
|
|
15916
|
+
if (Array.isArray(value)) return value.map(sortKeys4);
|
|
15917
|
+
if (value !== null && typeof value === "object") {
|
|
15918
|
+
return Object.fromEntries(
|
|
15919
|
+
Object.keys(value).sort().map((key) => [key, sortKeys4(value[key])])
|
|
15920
|
+
);
|
|
15921
|
+
}
|
|
15922
|
+
return value;
|
|
15923
|
+
}
|
|
15924
|
+
|
|
15925
|
+
// src/sync/schema-follow.ts
|
|
15926
|
+
async function rekeyTrackedRows(stateDb, vaultFs, databaseId, pages, changes, soleSource) {
|
|
15927
|
+
const sourceOf = new Map(
|
|
15928
|
+
pages.map((page) => [normalizeNotionId(page.id), sourceKeyOf(page, databaseId)])
|
|
15929
|
+
);
|
|
15930
|
+
const rewritten = [];
|
|
15931
|
+
let failed = 0;
|
|
15932
|
+
const kept = [];
|
|
15933
|
+
for (const record of stateDb.getAll()) {
|
|
15934
|
+
if (record.fileType !== "db-row" || record.notionPageId === null || record.notionParentId === null || !notionIdsEqual(record.notionParentId, databaseId) || (vaultFs.excludesPath?.(record.obsidianPath) ?? false)) {
|
|
15935
|
+
continue;
|
|
15936
|
+
}
|
|
15937
|
+
const source = sourceOf.get(normalizeNotionId(record.notionPageId)) ?? soleSource;
|
|
15938
|
+
const change = source === null ? void 0 : changes.get(source);
|
|
15939
|
+
if (!change || change.renamed.size === 0 && change.removed.size === 0) continue;
|
|
15940
|
+
try {
|
|
15941
|
+
const outcome = await rekeyRow(stateDb, vaultFs, record, change);
|
|
15942
|
+
if (outcome.wrote) rewritten.push(record.id);
|
|
15943
|
+
kept.push(...outcome.kept.map((key) => ({ path: record.obsidianPath, key })));
|
|
15944
|
+
} catch {
|
|
15945
|
+
failed++;
|
|
15946
|
+
}
|
|
15947
|
+
}
|
|
15948
|
+
return { rewritten, kept, failed };
|
|
15949
|
+
}
|
|
15950
|
+
async function rekeyRow(stateDb, vaultFs, record, change) {
|
|
15951
|
+
if (!await vaultFs.exists(record.obsidianPath)) return { wrote: false, kept: [] };
|
|
15952
|
+
const local = await vaultFs.readFile(record.obsidianPath);
|
|
15953
|
+
const snapshot = record.baseSnapshot ? record.baseSnapshot.toString("utf-8") : null;
|
|
15954
|
+
const base = snapshotFrontmatter(record.baseSnapshot);
|
|
15955
|
+
const unmodified = computeHash(local) === record.contentHash;
|
|
15956
|
+
const kept = [];
|
|
15957
|
+
const dropLocal = (key, value) => {
|
|
15958
|
+
if (!change.removed.has(key)) return false;
|
|
15959
|
+
if (unmodified || base !== null && sameFrontmatterValue(plainFrontmatterValue(base[key]), value)) {
|
|
15960
|
+
return true;
|
|
15961
|
+
}
|
|
15962
|
+
kept.push(key);
|
|
15963
|
+
return false;
|
|
15964
|
+
};
|
|
15965
|
+
const nextLocal = rekeyed(local, change, dropLocal);
|
|
15966
|
+
const nextSnapshot = snapshot === null ? null : rekeyed(snapshot, change, (key) => change.removed.has(key));
|
|
15967
|
+
if (nextLocal === null && nextSnapshot === null) return { wrote: false, kept };
|
|
15968
|
+
if (nextLocal !== null) await vaultFs.writeFile(record.obsidianPath, nextLocal);
|
|
15969
|
+
const synced = unmodified ? nextLocal ?? local : nextSnapshot ?? snapshot;
|
|
15970
|
+
const stat2 = unmodified && nextLocal !== null ? await vaultFs.getFileStat(record.obsidianPath) : null;
|
|
15971
|
+
if (synced !== null) {
|
|
15972
|
+
stateDb.transaction(() => {
|
|
15973
|
+
stateDb.updateHash(record.id, computeHash(synced), Buffer.from(synced, "utf-8"));
|
|
15974
|
+
if (stat2) stateDb.updateStatCache(record.id, stat2.mtime, stat2.size);
|
|
15975
|
+
});
|
|
15976
|
+
}
|
|
15977
|
+
return { wrote: nextLocal !== null, kept };
|
|
15978
|
+
}
|
|
15979
|
+
function rekeyed(note, change, dropped) {
|
|
15980
|
+
const lines = rekeyRowFrontmatter(note, change.renamed, dropped);
|
|
15981
|
+
return lines === null ? null : replaceFrontmatterLines(note, lines);
|
|
15982
|
+
}
|
|
15983
|
+
|
|
15984
|
+
// src/sync/discovered-databases.ts
|
|
15985
|
+
var ROOT_DATABASES_FOLDER = "databases";
|
|
15986
|
+
var DISCOVERED_DBS_META_KEY = "discovered_dbs";
|
|
15987
|
+
var INACCESSIBLE_DBS_META_KEY = "inaccessible_dbs";
|
|
15988
|
+
var LINKED_DBS_META_KEY = "linked_dbs";
|
|
15989
|
+
function parseDiscoveredDbs(raw) {
|
|
15990
|
+
if (!raw) return [];
|
|
15991
|
+
try {
|
|
15992
|
+
const parsed = JSON.parse(raw);
|
|
15993
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
15994
|
+
} catch {
|
|
15995
|
+
return [];
|
|
15996
|
+
}
|
|
15997
|
+
}
|
|
15998
|
+
function loadInaccessibleDbIds(stateDb) {
|
|
15999
|
+
const raw = stateDb.getMeta(INACCESSIBLE_DBS_META_KEY);
|
|
16000
|
+
if (!raw) return /* @__PURE__ */ new Set();
|
|
16001
|
+
try {
|
|
16002
|
+
const arr = JSON.parse(raw);
|
|
16003
|
+
return new Set(arr.map((id) => id.replace(/-/g, "")));
|
|
16004
|
+
} catch {
|
|
16005
|
+
return /* @__PURE__ */ new Set();
|
|
16006
|
+
}
|
|
16007
|
+
}
|
|
16008
|
+
function loadLinkedDbMap(stateDb) {
|
|
16009
|
+
const raw = stateDb.getMeta(LINKED_DBS_META_KEY);
|
|
16010
|
+
if (!raw) return /* @__PURE__ */ new Map();
|
|
16011
|
+
try {
|
|
16012
|
+
const obj = JSON.parse(raw);
|
|
16013
|
+
return new Map(Object.entries(obj).map(([k, v]) => [k.replace(/-/g, ""), v.replace(/-/g, "")]));
|
|
16014
|
+
} catch {
|
|
16015
|
+
return /* @__PURE__ */ new Map();
|
|
16016
|
+
}
|
|
16017
|
+
}
|
|
16018
|
+
|
|
16019
|
+
// src/sync/db-folder-moves.ts
|
|
16020
|
+
function rewriteDatabaseFolders(stateDb, moves) {
|
|
16021
|
+
const folders = moves.filter((move) => !isMarkdownPath(move.from));
|
|
16022
|
+
if (folders.length === 0) return;
|
|
16023
|
+
const databases = parseDiscoveredDbs(stateDb.getMeta(DISCOVERED_DBS_META_KEY));
|
|
16024
|
+
let changed = false;
|
|
16025
|
+
for (const db of databases) {
|
|
16026
|
+
const folder = db.localFolder.replace(/\/+$/, "");
|
|
16027
|
+
const to = finalPath(folder, folders);
|
|
16028
|
+
if (to === folder) continue;
|
|
16029
|
+
db.localFolder = to;
|
|
16030
|
+
changed = true;
|
|
16031
|
+
}
|
|
16032
|
+
if (changed) stateDb.setMeta(DISCOVERED_DBS_META_KEY, JSON.stringify(databases));
|
|
16033
|
+
const raw = stateDb.getMeta(DB_BASE_FILES_META_KEY);
|
|
16034
|
+
if (!raw) return;
|
|
16035
|
+
let entries;
|
|
16036
|
+
try {
|
|
16037
|
+
entries = JSON.parse(raw);
|
|
16038
|
+
} catch {
|
|
16039
|
+
return;
|
|
16040
|
+
}
|
|
16041
|
+
if (!entries || typeof entries !== "object") return;
|
|
16042
|
+
let moved = false;
|
|
16043
|
+
for (const entry of Object.values(entries)) {
|
|
16044
|
+
if (typeof entry?.basePath !== "string") continue;
|
|
16045
|
+
const to = finalPath(entry.basePath, folders);
|
|
16046
|
+
if (to === entry.basePath) continue;
|
|
16047
|
+
entry.basePath = to;
|
|
16048
|
+
moved = true;
|
|
16049
|
+
}
|
|
16050
|
+
if (moved) stateDb.setMeta(DB_BASE_FILES_META_KEY, JSON.stringify(entries));
|
|
16051
|
+
}
|
|
16052
|
+
async function retargetBaseFiles(vaultFs, moves) {
|
|
16053
|
+
const folders = moves.filter((move) => !isMarkdownPath(move.from));
|
|
16054
|
+
if (folders.length === 0) return;
|
|
16055
|
+
try {
|
|
16056
|
+
for (const file of await vaultFs.listNonMarkdownFiles()) {
|
|
16057
|
+
if (!file.path.endsWith(BASE_SUFFIX)) continue;
|
|
16058
|
+
const content = await vaultFs.readFile(file.path);
|
|
16059
|
+
if (!isGeneratedDbArtifact(file.path, content)) continue;
|
|
16060
|
+
const next = content.replace(IN_FOLDER_FILTER_RE, (whole, folder) => {
|
|
16061
|
+
const to = finalPath(folder, folders);
|
|
16062
|
+
return to === folder ? whole : inFolderFilter(to);
|
|
16063
|
+
});
|
|
16064
|
+
if (next !== content) await vaultFs.writeFile(file.path, next);
|
|
16065
|
+
}
|
|
16066
|
+
} catch (error) {
|
|
16067
|
+
getLogger().warn(
|
|
16068
|
+
`[Im-Nobsidian] \uC62E\uAE34 \uD3F4\uB354\uB97C \uAC00\uB9AC\uD0A4\uB358 .base \uBCF4\uAE30\uB97C \uACE0\uCE58\uC9C0 \uBABB\uD568: ${error instanceof Error ? error.message : String(error)}`
|
|
16069
|
+
);
|
|
16070
|
+
}
|
|
16071
|
+
}
|
|
16072
|
+
function finalPath(path, moves) {
|
|
16073
|
+
let current = path;
|
|
16074
|
+
for (const move of moves) {
|
|
16075
|
+
if (current === move.from || current.startsWith(`${move.from}/`)) {
|
|
16076
|
+
current = move.to + current.slice(move.from.length);
|
|
16077
|
+
}
|
|
16078
|
+
}
|
|
16079
|
+
return current;
|
|
16080
|
+
}
|
|
16081
|
+
|
|
16082
|
+
// src/sync/note-relocation.ts
|
|
16083
|
+
var NoteRelocator = class {
|
|
16084
|
+
constructor(stateDb, vaultFs, configuredDatabases = []) {
|
|
16085
|
+
this.stateDb = stateDb;
|
|
16086
|
+
this.vaultFs = vaultFs;
|
|
16087
|
+
this.configuredDatabases = configuredDatabases;
|
|
16088
|
+
}
|
|
16089
|
+
stateDb;
|
|
16090
|
+
vaultFs;
|
|
16091
|
+
configuredDatabases;
|
|
16092
|
+
/**
|
|
16093
|
+
* 함께 옮길 것 — 폴더로 추적하는 페이지는 그 폴더, 폴더 노트(`X/X.md`)는 든 폴더, 노트는 그 파일과 옆의 자식
|
|
16094
|
+
* 폴더(`X/`, 있으면).
|
|
16095
|
+
*/
|
|
16096
|
+
unitsOf(record) {
|
|
16097
|
+
if (isFolderRecord(record)) return [record.obsidianPath];
|
|
16098
|
+
if (record.fileType !== "file") return [parentFolderOf(record.obsidianPath)];
|
|
16099
|
+
const children = stripMarkdownExtension(record.obsidianPath);
|
|
16100
|
+
const hasChildren = this.stateDb.getAll().some((r) => within(r.obsidianPath, children));
|
|
16101
|
+
return hasChildren ? [record.obsidianPath, children] : [record.obsidianPath];
|
|
16102
|
+
}
|
|
16103
|
+
/** 옮기지 못할 까닭 — 없으면 null. */
|
|
16104
|
+
async blocked(move) {
|
|
16105
|
+
if (within(move.to, move.from)) return "\uC81C \uC548\uC73C\uB85C \uC62E\uAE38 \uC218 \uC5C6\uB2E4";
|
|
16106
|
+
if (!await this.vaultFs.exists(move.from)) return "\uBCFC\uD2B8\uC5D0 \uB178\uD2B8\uAC00 \uC5C6\uB2E4";
|
|
16107
|
+
if (await this.taken(move.to)) return `\uC62E\uAE38 \uC790\uB9AC(${move.to})\uC5D0 \uAC19\uC740 \uC774\uB984\uC774 \uC788\uB2E4`;
|
|
16108
|
+
const isFolder = !isMarkdownPath(move.from);
|
|
16109
|
+
if (this.vaultFs.excludesPath?.(move.to, isFolder ? "folder" : "file")) {
|
|
16110
|
+
return `\uC62E\uAE38 \uC790\uB9AC(${move.to})\uAC00 \uB3D9\uAE30\uD654\uC5D0\uC11C \uBE60\uC9C4\uB2E4(paths \xB7 .im-nobsidian-ignore)`;
|
|
16111
|
+
}
|
|
16112
|
+
if (isFolder && this.holdsConfiguredDatabase(move.from)) {
|
|
16113
|
+
return "\uD3F4\uB354 \uC548\uC5D0 \uC124\uC815 \uD30C\uC77C\uB85C \uC815\uD55C DB \uD3F4\uB354(notion.databases)\uAC00 \uC788\uB2E4 \u2014 \uC124\uC815\uC758 localFolder \uB97C \uACE0\uCE5C \uB4A4 Obsidian \uC5D0\uC11C \uC62E\uAE30\uBA74 \uB2E4\uC74C push \uAC00 Notion \uC744 \uB9DE\uCD98\uB2E4";
|
|
16114
|
+
}
|
|
16115
|
+
if (isFolder && (this.vaultFs.holdsOtherConnection?.(move.from) ?? false)) {
|
|
16116
|
+
return "\uD3F4\uB354 \uC548\uC5D0 \uB2E4\uB978 \uC5F0\uACB0 \uD3F4\uB354\uAC00 \uC788\uB2E4 \u2014 Obsidian \uC5D0\uC11C \uC62E\uAE30\uBA74 \uB2E4\uC74C push \uAC00 Notion \uC744 \uB9DE\uCD98\uB2E4";
|
|
16117
|
+
}
|
|
16118
|
+
return null;
|
|
16119
|
+
}
|
|
16120
|
+
/** 이 경로에 파일 · 폴더가 있거나 추적하는 것이 있다. */
|
|
16121
|
+
async taken(path) {
|
|
16122
|
+
return await this.vaultFs.exists(path) || this.stateDb.getAll().some((r) => within(r.obsidianPath, path));
|
|
16123
|
+
}
|
|
16124
|
+
/**
|
|
16125
|
+
* 차례로 옮기고 상태 DB 를 새 경로로 적는다. 하나라도 실패하면 옮긴 것을 되돌리고 던진다 — 상태 DB 는 옛 경로 그대로다.
|
|
16126
|
+
* @param alsoInTransaction 경로를 옮겨 적는 트랜잭션 안에서 함께 할 것.
|
|
16127
|
+
*/
|
|
16128
|
+
async relocate(moves, alsoInTransaction) {
|
|
16129
|
+
const done = [];
|
|
16130
|
+
try {
|
|
16131
|
+
for (const move of moves) {
|
|
16132
|
+
await this.vaultFs.moveFile(move.from, move.to);
|
|
16133
|
+
done.push(move);
|
|
16134
|
+
}
|
|
16135
|
+
} catch (error) {
|
|
16136
|
+
for (const move of done.reverse())
|
|
16137
|
+
await this.vaultFs.moveFile(move.to, move.from).catch(() => {
|
|
16138
|
+
});
|
|
16139
|
+
throw error;
|
|
16140
|
+
}
|
|
16141
|
+
this.stateDb.transaction(() => {
|
|
16142
|
+
for (const move of moves) this.rewritePaths(move);
|
|
16143
|
+
rewriteDatabaseFolders(this.stateDb, moves);
|
|
16144
|
+
alsoInTransaction?.();
|
|
16145
|
+
});
|
|
16146
|
+
await retargetBaseFiles(this.vaultFs, moves);
|
|
16147
|
+
}
|
|
16148
|
+
holdsConfiguredDatabase(folder) {
|
|
16149
|
+
return this.configuredDatabases.some(
|
|
16150
|
+
(db) => within(db.localFolder.replace(/\/+$/, ""), folder)
|
|
16151
|
+
);
|
|
16152
|
+
}
|
|
16153
|
+
/** 옮긴 것 아래의 레코드 · 위키링크 · 보존 마커 · 첨부 기록을 새 경로로 옮겨 적는다. */
|
|
16154
|
+
rewritePaths(move) {
|
|
16155
|
+
for (const record of this.stateDb.getAll()) {
|
|
16156
|
+
if (!within(record.obsidianPath, move.from)) continue;
|
|
16157
|
+
const from = record.obsidianPath;
|
|
16158
|
+
const to = move.to + from.slice(move.from.length);
|
|
15449
16159
|
this.stateDb.updatePath(record.id, to);
|
|
15450
16160
|
const entry = record.notionPageId ? this.stateDb.resolvePageId(record.notionPageId) : null;
|
|
15451
16161
|
if (entry && entry.obsidianPath === from) {
|
|
@@ -15896,6 +16606,8 @@ var DatabaseSyncer = class {
|
|
|
15896
16606
|
this.observation = observation;
|
|
15897
16607
|
this.noteRowBody = noteRowBody;
|
|
15898
16608
|
this.baseFileInfo = new DbBaseFiles(stateDb);
|
|
16609
|
+
this.sidecarBases = new SidecarBases(stateDb);
|
|
16610
|
+
this.schemaLedger = new SchemaLedger(stateDb);
|
|
15899
16611
|
this.relocator = new NoteRelocator(stateDb, vaultFs, config.notion.databases ?? []);
|
|
15900
16612
|
this.propertyMapper = PropertyMapper.fromConfig(config);
|
|
15901
16613
|
this.propertyMapper.setWikilinkResolver({
|
|
@@ -15935,11 +16647,21 @@ var DatabaseSyncer = class {
|
|
|
15935
16647
|
};
|
|
15936
16648
|
/** 볼트에서 사라져 되살린 행의 경로 — pull 마감이 가져가 화면에 알린다(D8). */
|
|
15937
16649
|
restoredRows = [];
|
|
16650
|
+
/** Notion 에서 바뀐 속성을 행 노트에 옮긴 것 — pull 마감이 가져가 화면에 알린다(ADR-045). */
|
|
16651
|
+
schemaFollowed = [];
|
|
16652
|
+
/** pull 이 마지막으로 쓴 사이드카 — 볼트에서 고친 것은 덮어쓰지 않는다(ADR-045). */
|
|
16653
|
+
sidecarBases;
|
|
16654
|
+
/** 이번 pull 이 고쳐 둔 것이라 덮어쓰지 않은 사이드카 경로. */
|
|
16655
|
+
heldSidecars = /* @__PURE__ */ new Set();
|
|
16656
|
+
/** 이번 pull 이 볼트에서 고친 것이라 덮어쓰지 않은 `.base` 경로(ADR-046). */
|
|
16657
|
+
heldBases = /* @__PURE__ */ new Set();
|
|
15938
16658
|
/**
|
|
15939
16659
|
* 실제로 기록한 .base 경로/DB 제목 — placeholder 임베드 재작성(F22)의 SSOT. 이번 pull 이 조회하지
|
|
15940
16660
|
* 않은 DB 의 것도 남아 있다({@link DbBaseFiles}).
|
|
15941
16661
|
*/
|
|
15942
16662
|
baseFileInfo;
|
|
16663
|
+
/** DB 마다 지난 pull 이 본 속성 — Notion 에서 바꾼 속성 이름을 행 노트에 옮기는 근거(ADR-045). */
|
|
16664
|
+
schemaLedger;
|
|
15943
16665
|
get decorations() {
|
|
15944
16666
|
return this.decorationsCache ??= new PageDecorations(this.notionClient, this.imageHandler);
|
|
15945
16667
|
}
|
|
@@ -16005,12 +16727,14 @@ var DatabaseSyncer = class {
|
|
|
16005
16727
|
const schema = await this.notionClient.getDatabaseSchema(dbConfig.databaseId, meta);
|
|
16006
16728
|
this.propertyMapper.loadSchema(schema);
|
|
16007
16729
|
await this.vaultFs.ensureFolder(dbConfig.localFolder);
|
|
16730
|
+
const followed = wholeDbInScope ? await this.followSchema(dbConfig, meta, pages) : { changes: /* @__PURE__ */ new Map(), rekeyed: /* @__PURE__ */ new Set() };
|
|
16731
|
+
const refreshedKeys = (page) => followed.changes.get(sourceKeyOf(page, dbConfig.databaseId))?.refreshed;
|
|
16008
16732
|
if (wholeDbInScope) {
|
|
16009
16733
|
const viewsConfig = await this.pullDatabaseViews(dbConfig);
|
|
16010
16734
|
await this.generateBaseFile(dbConfig, viewsConfig, meta, void 0, this.rangeEndsOf(pages));
|
|
16011
16735
|
}
|
|
16012
16736
|
let created = 0;
|
|
16013
|
-
let updated =
|
|
16737
|
+
let updated = followed.rekeyed.size;
|
|
16014
16738
|
let restored = 0;
|
|
16015
16739
|
const conflicts = [];
|
|
16016
16740
|
const failed = [];
|
|
@@ -16025,7 +16749,14 @@ var DatabaseSyncer = class {
|
|
|
16025
16749
|
for (const page of pages) {
|
|
16026
16750
|
try {
|
|
16027
16751
|
const record = this.stateDb.getByNotionId(page.id);
|
|
16028
|
-
const action = await this.rowAction(
|
|
16752
|
+
const action = await this.rowAction(
|
|
16753
|
+
dbConfig,
|
|
16754
|
+
page,
|
|
16755
|
+
record,
|
|
16756
|
+
wholeDbInScope,
|
|
16757
|
+
scope,
|
|
16758
|
+
refreshedKeys(page)
|
|
16759
|
+
);
|
|
16029
16760
|
if (action !== "skip") pending.push({ page, action });
|
|
16030
16761
|
else if (record && (wholeDbInScope || inAnyPathScope(record.obsidianPath, scope))) {
|
|
16031
16762
|
unchanged.push(page);
|
|
@@ -16044,7 +16775,7 @@ var DatabaseSyncer = class {
|
|
|
16044
16775
|
try {
|
|
16045
16776
|
const record = this.stateDb.getByNotionId(page.id);
|
|
16046
16777
|
const outcome = await withDeadline(
|
|
16047
|
-
() => action === "reform" && record ? this.
|
|
16778
|
+
() => action === "reform" && record ? this.rewriteStaleKeys(page, record, refreshedKeys(page)) : this.pullDatabasePage(page, dbConfig),
|
|
16048
16779
|
this.config.advanced.itemTimeoutMs,
|
|
16049
16780
|
`pull ${dbConfig.localFolder}/${page.id}`
|
|
16050
16781
|
);
|
|
@@ -16055,7 +16786,7 @@ var DatabaseSyncer = class {
|
|
|
16055
16786
|
if (action === "restore") {
|
|
16056
16787
|
restored++;
|
|
16057
16788
|
this.restoredRows.push(outcome.path);
|
|
16058
|
-
} else updated++;
|
|
16789
|
+
} else if (!followed.rekeyed.has(record.id)) updated++;
|
|
16059
16790
|
writtenPaths.push(outcome.path);
|
|
16060
16791
|
} else if (outcome.action === "conflict") {
|
|
16061
16792
|
conflicts.push(outcome.conflict);
|
|
@@ -16218,32 +16949,35 @@ var DatabaseSyncer = class {
|
|
|
16218
16949
|
* (틀린) 전제로 db-row 를 제외해 두었기에 양쪽 어디에도 복원 경로가 없는 상태였다.
|
|
16219
16950
|
* deleteSync 가 켜져 있으면 되살리지 않는다 — 지운 것은 Notion 에서도 지우라는 뜻이고, sync 는
|
|
16220
16951
|
* pull 을 먼저 돌려 되살리면 뒤이은 push 가 지울 것을 잃는다(detectMissingLocalFiles 와 같다).
|
|
16221
|
-
* · 예전 버전이 적은 날짜 모양이 남은
|
|
16222
|
-
* F-08).
|
|
16952
|
+
* · 예전 버전이 적은 날짜 모양이 남은 행, Notion 이 새로 만들거나 타입을 바꾼 속성의 값이 다른 행은 그 줄만
|
|
16953
|
+
* 고쳐 쓴다({@link staleKeysToRewrite}, F-08 · ADR-045).
|
|
16954
|
+
*
|
|
16955
|
+
* @param refreshed 이 행의 data source 에서 새로 생기거나 타입이 바뀐 속성의 키(ADR-045).
|
|
16223
16956
|
*/
|
|
16224
|
-
async rowAction(dbConfig, page, record, wholeDbInScope, scope) {
|
|
16957
|
+
async rowAction(dbConfig, page, record, wholeDbInScope, scope, refreshed) {
|
|
16225
16958
|
if (!wholeDbInScope && !(record && inAnyPathScope(record.obsidianPath, scope))) return "skip";
|
|
16226
16959
|
if (record && this.vaultFs.excludesPath?.(record.obsidianPath)) return "skip";
|
|
16227
16960
|
if (!record) return "pull";
|
|
16228
16961
|
const misplaced = !isDirectDbRowPath(dbConfig.localFolder, record.obsidianPath);
|
|
16229
16962
|
const changed = misplaced || this.remoteVerdict(record, page) !== "unchanged";
|
|
16230
|
-
if (!changed && await this.
|
|
16963
|
+
if (!changed && await this.staleKeysToRewrite(record, page, refreshed)) return "reform";
|
|
16231
16964
|
if (this.config.sync.deleteSync || await this.vaultFs.exists(record.obsidianPath)) {
|
|
16232
16965
|
return changed ? "pull" : "skip";
|
|
16233
16966
|
}
|
|
16234
16967
|
return "restore";
|
|
16235
16968
|
}
|
|
16236
16969
|
/**
|
|
16237
|
-
* 원격이 그대로인 행이지만 예전 버전이 적은 날짜 모양(`…Z` · `…+09:00` ·
|
|
16238
|
-
*
|
|
16239
|
-
*
|
|
16970
|
+
* 원격이 그대로인 행이지만 그 줄만 고쳐 쓸 키가 있는가 — 예전 버전이 적은 날짜 모양(`…Z` · `…+09:00` ·
|
|
16971
|
+
* `{start, end}`, F-08, {@link PropertyMapper.outdatedDateKeys}), 또는 Notion 이 새로 만들거나 타입을 바꾼
|
|
16972
|
+
* 속성의 값이 적힌 것과 다른 키(ADR-045 — 수식 열을 더하면 행마다 값이 생기지만 행의 수정 시각은 그대로다).
|
|
16973
|
+
* 고쳐 쓰면 사본도 새 값이 되어 다음 pull 은 건너뛴다.
|
|
16240
16974
|
*
|
|
16241
16975
|
* 지난 동기화 뒤 로컬을 고친 행은 고르지 않는다 — 로컬 편집을 지키느라 쓰지 않으니 pull 마다 고르기만
|
|
16242
16976
|
* 한다. 그 편집을 올린 뒤의 pull 이 고른다. 로컬 파일을 읽지 못하면 고르지 않는다.
|
|
16243
16977
|
*/
|
|
16244
|
-
async
|
|
16978
|
+
async staleKeysToRewrite(record, page, refreshed) {
|
|
16245
16979
|
const base = snapshotFrontmatter(record.baseSnapshot);
|
|
16246
|
-
if (base === null || this.
|
|
16980
|
+
if (base === null || this.staleKeys(base, rowProperties(page), refreshed).length === 0) {
|
|
16247
16981
|
return false;
|
|
16248
16982
|
}
|
|
16249
16983
|
try {
|
|
@@ -16252,25 +16986,37 @@ var DatabaseSyncer = class {
|
|
|
16252
16986
|
return false;
|
|
16253
16987
|
}
|
|
16254
16988
|
}
|
|
16989
|
+
/** 적힌 frontmatter 에서 받은 값으로 고쳐 쓸 키 — {@link staleKeysToRewrite}. */
|
|
16990
|
+
staleKeys(written, raw, refreshed) {
|
|
16991
|
+
const keys = new Set(this.propertyMapper.outdatedDateKeys(written, raw));
|
|
16992
|
+
if (refreshed && refreshed.size > 0) {
|
|
16993
|
+
const fresh = this.propertyMapper.fromNotionProperties(raw);
|
|
16994
|
+
for (const key of refreshed) {
|
|
16995
|
+
if (raw[key]?.type === "files") continue;
|
|
16996
|
+
if (!sameFrontmatterValue(written[key] ?? null, fresh[key] ?? null)) keys.add(key);
|
|
16997
|
+
}
|
|
16998
|
+
}
|
|
16999
|
+
return [...keys];
|
|
17000
|
+
}
|
|
16255
17001
|
/**
|
|
16256
|
-
*
|
|
16257
|
-
*
|
|
16258
|
-
*
|
|
17002
|
+
* 원격이 그대로인 행에서 {@link staleKeys} 의 키만 받은 값으로 고쳐 쓴다 — 본문 · 첨부는 다시 받지 않는다. 본문은
|
|
17003
|
+
* 한 글자도 바꾸지 않고, 다른 키의 줄은 그대로 둔다(키 단위로 나눠 읽을 수 없는 frontmatter 는 값만 지킨다 —
|
|
17004
|
+
* {@link mergeRowFrontmatter}).
|
|
16259
17005
|
*
|
|
16260
17006
|
* 예전에는 행을 통째로 다시 받았다. 행마다 본문을 읽어 요청이 늘고, 다시 그릴 때마다 달라지는 본문(빈
|
|
16261
17007
|
* 블록 · 첨부 이름)까지 바뀌었다 — 실볼트 사본에서 다시 받은 149행 중 99행.
|
|
16262
17008
|
*
|
|
16263
17009
|
* 고른 뒤 로컬을 고쳤으면 쓰지 않는다(`skipped`) — 이어지는 push 가 올린다.
|
|
16264
17010
|
*/
|
|
16265
|
-
async
|
|
17011
|
+
async rewriteStaleKeys(page, record, refreshed) {
|
|
16266
17012
|
const path = record.obsidianPath;
|
|
16267
17013
|
const local = await this.vaultFs.readFile(path);
|
|
16268
17014
|
if (computeHash(local) !== record.contentHash) return { action: "skipped", path };
|
|
16269
17015
|
const raw = rowProperties(page);
|
|
16270
|
-
const keys = this.
|
|
17016
|
+
const keys = this.staleKeys(splitFrontmatter(local).data, raw, refreshed);
|
|
16271
17017
|
const fresh = this.propertyMapper.fromNotionProperties(raw);
|
|
16272
17018
|
const pulled = Object.fromEntries(
|
|
16273
|
-
keys.filter((key) => Object.hasOwn(fresh, key)).map((key) => [key, fresh[key]])
|
|
17019
|
+
keys.filter((key) => Object.hasOwn(fresh, key) && !isEmptyFrontmatterValue(fresh[key])).map((key) => [key, fresh[key]])
|
|
16274
17020
|
);
|
|
16275
17021
|
const lines = keys.length > 0 ? mergeRowFrontmatter(local, pulled, new Set(keys)) : null;
|
|
16276
17022
|
const content = lines === null ? null : replaceFrontmatterLines(local, lines);
|
|
@@ -16283,6 +17029,86 @@ var DatabaseSyncer = class {
|
|
|
16283
17029
|
});
|
|
16284
17030
|
return { action: "written", path };
|
|
16285
17031
|
}
|
|
17032
|
+
/**
|
|
17033
|
+
* Notion 에서 바뀐 속성을 지난 pull 이 본 스키마와 id 로 견줘 행 노트에 옮긴다(ADR-045) — 이름을 바꾼 속성은 키
|
|
17034
|
+
* 이름을, 지운 속성은 키를. 새로 생기거나 타입이 바뀐 속성은 돌려준다 — 그대로인 행도 그 값을 고쳐 쓴다.
|
|
17035
|
+
*
|
|
17036
|
+
* 처음 보는 DB 는 견줄 것이 없어 적기만 한다. 행 하나라도 옮기지 못하면 본 스키마를 적지 않는다 — 다음 pull 이 다시
|
|
17037
|
+
* 옮긴다(이미 옮긴 행은 바꿀 것이 없다).
|
|
17038
|
+
*
|
|
17039
|
+
* @returns 바뀐 속성(data source → 바뀐 것, 바뀐 소스만)과 키를 바꾼 행의 레코드 id.
|
|
17040
|
+
*/
|
|
17041
|
+
async followSchema(dbConfig, meta, pages) {
|
|
17042
|
+
const changes = /* @__PURE__ */ new Map();
|
|
17043
|
+
const seen = seenSchemaOf(meta);
|
|
17044
|
+
if (seen === null) return { changes, rekeyed: /* @__PURE__ */ new Set() };
|
|
17045
|
+
const before = this.schemaLedger.get(dbConfig.databaseId);
|
|
17046
|
+
for (const [source, after] of Object.entries(seen)) {
|
|
17047
|
+
const prev = before?.[source];
|
|
17048
|
+
if (!prev) continue;
|
|
17049
|
+
const change = diffSourceSchema(prev, after);
|
|
17050
|
+
if (!isEmptySchemaChange(change)) changes.set(source, change);
|
|
17051
|
+
}
|
|
17052
|
+
let rekeyed2 = /* @__PURE__ */ new Set();
|
|
17053
|
+
if (changes.size > 0) {
|
|
17054
|
+
const sources = Object.keys(seen);
|
|
17055
|
+
const soleSource = meta.dataSources.length <= 1 && sources.length === 1 ? sources[0] : null;
|
|
17056
|
+
const outcome = await rekeyTrackedRows(
|
|
17057
|
+
this.stateDb,
|
|
17058
|
+
this.vaultFs,
|
|
17059
|
+
dbConfig.databaseId,
|
|
17060
|
+
pages,
|
|
17061
|
+
changes,
|
|
17062
|
+
soleSource
|
|
17063
|
+
);
|
|
17064
|
+
rekeyed2 = new Set(outcome.rewritten);
|
|
17065
|
+
this.noteSchemaFollowed(dbConfig, changes, outcome);
|
|
17066
|
+
if (outcome.failed > 0) {
|
|
17067
|
+
getLogger().warn(
|
|
17068
|
+
`[DB Sync] ${dbConfig.localFolder}: \uBC14\uB010 \uC18D\uC131\uC744 \uD589 \uB178\uD2B8 ${outcome.failed}\uAC1C\uC5D0 \uC62E\uAE30\uC9C0 \uBABB\uD568 \u2014 \uB2E4\uC74C pull \uC774 \uB2E4\uC2DC \uD569\uB2C8\uB2E4`
|
|
17069
|
+
);
|
|
17070
|
+
return { changes, rekeyed: rekeyed2 };
|
|
17071
|
+
}
|
|
17072
|
+
}
|
|
17073
|
+
this.schemaLedger.set(dbConfig.databaseId, seen);
|
|
17074
|
+
return { changes, rekeyed: rekeyed2 };
|
|
17075
|
+
}
|
|
17076
|
+
/** 옮긴 것을 pull 마감이 알리게 적는다 — 이름 바꿈 · 지움이 있었던 DB 만. 날짜 범위 끝 키는 따로 세지 않는다. */
|
|
17077
|
+
noteSchemaFollowed(dbConfig, changes, outcome) {
|
|
17078
|
+
const derived = (key, keys) => key.endsWith(RANGE_END_SUFFIX) && keys.has(key.slice(0, -RANGE_END_SUFFIX.length));
|
|
17079
|
+
const renamed = [...changes.values()].flatMap(
|
|
17080
|
+
(c) => [...c.renamed].filter(([from]) => !derived(from, c.renamed))
|
|
17081
|
+
);
|
|
17082
|
+
const removed = [...changes.values()].flatMap(
|
|
17083
|
+
(c) => [...c.removed].filter((key) => !derived(key, c.removed))
|
|
17084
|
+
);
|
|
17085
|
+
if (renamed.length === 0 && removed.length === 0) return;
|
|
17086
|
+
this.schemaFollowed.push({
|
|
17087
|
+
folder: dbConfig.localFolder,
|
|
17088
|
+
renamed,
|
|
17089
|
+
removed,
|
|
17090
|
+
rows: outcome.rewritten.length,
|
|
17091
|
+
kept: outcome.kept
|
|
17092
|
+
});
|
|
17093
|
+
}
|
|
17094
|
+
/** 이번 pull 이 행 노트에 옮긴 Notion 속성 변경 — 가져가면 비운다(ADR-045). */
|
|
17095
|
+
/** 고쳐 둔 것이라 덮어쓰지 않은 사이드카 — 가져가면 비운다. */
|
|
17096
|
+
takeHeldSidecars() {
|
|
17097
|
+
const held = [...this.heldSidecars];
|
|
17098
|
+
this.heldSidecars = /* @__PURE__ */ new Set();
|
|
17099
|
+
return held;
|
|
17100
|
+
}
|
|
17101
|
+
/** 고쳐 둔 것이라 덮어쓰지 않은 `.base` — 가져가면 비운다. */
|
|
17102
|
+
takeHeldBases() {
|
|
17103
|
+
const held = [...this.heldBases];
|
|
17104
|
+
this.heldBases = /* @__PURE__ */ new Set();
|
|
17105
|
+
return held;
|
|
17106
|
+
}
|
|
17107
|
+
takeSchemaFollowed() {
|
|
17108
|
+
const followed = this.schemaFollowed;
|
|
17109
|
+
this.schemaFollowed = [];
|
|
17110
|
+
return followed;
|
|
17111
|
+
}
|
|
16286
17112
|
/** 이 DB 의 추적 행 가운데 이번 조회에 없던 것 — 지워졌는지는 아직 모른다. */
|
|
16287
17113
|
unlistedRows(dbConfig, pages, paths) {
|
|
16288
17114
|
const listed = new Set(pages.map((page) => normalizeNotionId(page.id)));
|
|
@@ -16394,7 +17220,7 @@ var DatabaseSyncer = class {
|
|
|
16394
17220
|
lastSynced: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16395
17221
|
views: [{ id: "default", name: "Table", type: "table" }]
|
|
16396
17222
|
};
|
|
16397
|
-
const
|
|
17223
|
+
const generated = this.baseFileGenerator.generateWithViews({
|
|
16398
17224
|
databaseId: dbConfig.databaseId,
|
|
16399
17225
|
databaseName: dbName,
|
|
16400
17226
|
schema: schemaFull,
|
|
@@ -16402,12 +17228,21 @@ var DatabaseSyncer = class {
|
|
|
16402
17228
|
folderPath: rowsFolder ?? dbConfig.localFolder,
|
|
16403
17229
|
rangeEnds
|
|
16404
17230
|
});
|
|
17231
|
+
const baseContent = generated.content;
|
|
16405
17232
|
const safeName = sanitizeFileName(dbName);
|
|
16406
17233
|
const basePath = `${dbConfig.localFolder}/${safeName}${BASE_SUFFIX}`;
|
|
16407
17234
|
const previous = this.baseFileInfo.get(dbConfig.databaseId);
|
|
16408
17235
|
const existing = await this.vaultFs.readFile(basePath).catch(() => null);
|
|
16409
|
-
let
|
|
16410
|
-
|
|
17236
|
+
let record = {
|
|
17237
|
+
basePath,
|
|
17238
|
+
title: dbName,
|
|
17239
|
+
hash: computeHash(baseContent),
|
|
17240
|
+
// Notion 보기를 받지 못해 기본 표 하나를 쓴 파일은 올릴 보기가 없다.
|
|
17241
|
+
...viewsConfig ? recordBaseViews(baseContent, generated.views, viewsConfig.views) : null
|
|
17242
|
+
};
|
|
17243
|
+
const ours = existing === null || isGeneratedDbArtifact(basePath, existing) || previous?.basePath === basePath && previous.views !== void 0;
|
|
17244
|
+
if (!ours) {
|
|
17245
|
+
record = { basePath, title: dbName, hash: record.hash };
|
|
16411
17246
|
if (!this.keptUserBases.has(basePath)) {
|
|
16412
17247
|
this.keptUserBases.add(basePath);
|
|
16413
17248
|
getLogger().info(
|
|
@@ -16415,12 +17250,12 @@ var DatabaseSyncer = class {
|
|
|
16415
17250
|
);
|
|
16416
17251
|
}
|
|
16417
17252
|
} else if (existing !== null && existing !== baseContent && editedSince(previous, basePath, existing)) {
|
|
16418
|
-
|
|
17253
|
+
record = { ...previous, title: dbName };
|
|
16419
17254
|
this.warnEditedBase(basePath);
|
|
16420
17255
|
} else if (existing !== baseContent) {
|
|
16421
17256
|
await this.vaultFs.writeFile(basePath, baseContent);
|
|
16422
17257
|
}
|
|
16423
|
-
this.baseFileInfo.set(dbConfig.databaseId,
|
|
17258
|
+
this.baseFileInfo.set(dbConfig.databaseId, record);
|
|
16424
17259
|
getLogger().debug(`[DB Sync] .base \uD30C\uC77C \uC0DD\uC131: ${basePath}`);
|
|
16425
17260
|
await this.generateSidecar(
|
|
16426
17261
|
dbConfig,
|
|
@@ -16465,10 +17300,11 @@ var DatabaseSyncer = class {
|
|
|
16465
17300
|
}
|
|
16466
17301
|
/** 볼트에서 고친 `.base` 를 덮어쓰거나 지우지 않았다고 한 번 알린다. */
|
|
16467
17302
|
warnEditedBase(path) {
|
|
17303
|
+
this.heldBases.add(path);
|
|
16468
17304
|
if (this.keptUserBases.has(path)) return;
|
|
16469
17305
|
this.keptUserBases.add(path);
|
|
16470
17306
|
getLogger().warn(
|
|
16471
|
-
`[DB Sync] \uBCFC\uD2B8\uC5D0\uC11C \uACE0\uCE5C .base \uB77C Notion \uBCF4\uAE30\uB85C \uB36E\uC5B4\uC4F0\uC9C0 \uC54A\uC74C: ${path} \u2014 Notion \uC758 \uBCF4\uAE30\uB97C \uB2E4\uC2DC \uBC1B\uC73C\uB824\uBA74 \uC774 \uD30C\uC77C\uC744 \uC9C0\uC6B0\uC138\uC694`
|
|
17307
|
+
`[DB Sync] \uBCFC\uD2B8\uC5D0\uC11C \uACE0\uCE5C .base \uB77C Notion \uBCF4\uAE30\uB85C \uB36E\uC5B4\uC4F0\uC9C0 \uC54A\uC74C: ${path} \u2014 push \uD558\uBA74 \uACE0\uCE5C \uBCF4\uAE30 \uC774\uB984 \xB7 \uC5F4 \xB7 \uC815\uB82C \xB7 \uD544\uD130\uB97C Notion \uC5D0 \uC62C\uB9BD\uB2C8\uB2E4. Notion \uC758 \uBCF4\uAE30\uB97C \uB2E4\uC2DC \uBC1B\uC73C\uB824\uBA74 \uC774 \uD30C\uC77C\uC744 \uC9C0\uC6B0\uC138\uC694`
|
|
16472
17308
|
);
|
|
16473
17309
|
}
|
|
16474
17310
|
/**
|
|
@@ -16487,6 +17323,7 @@ var DatabaseSyncer = class {
|
|
|
16487
17323
|
for (const path of stale) {
|
|
16488
17324
|
const content = await this.vaultFs.readFile(path).catch(() => null);
|
|
16489
17325
|
if (content === null || !isGeneratedDbArtifact(path, content)) continue;
|
|
17326
|
+
if (this.heldSidecars.has(path)) continue;
|
|
16490
17327
|
if (editedSince(previous, path, content)) {
|
|
16491
17328
|
this.warnEditedBase(path);
|
|
16492
17329
|
continue;
|
|
@@ -16513,11 +17350,26 @@ var DatabaseSyncer = class {
|
|
|
16513
17350
|
isInline,
|
|
16514
17351
|
appearance
|
|
16515
17352
|
});
|
|
16516
|
-
const sidecarPath = `${dbConfig.localFolder}/${safeName}${SIDECAR_SUFFIX}`;
|
|
16517
|
-
const serialized = this.sidecarGenerator.serialize(sidecar);
|
|
16518
|
-
const existing = await this.vaultFs.readFile(sidecarPath).catch(() => null);
|
|
17353
|
+
const sidecarPath = `${dbConfig.localFolder}/${safeName}${SIDECAR_SUFFIX}`;
|
|
17354
|
+
const serialized = this.sidecarGenerator.serialize(sidecar);
|
|
17355
|
+
const existing = await this.vaultFs.readFile(sidecarPath).catch(() => null);
|
|
17356
|
+
if (existing !== null && existing !== serialized && !isGeneratedDbArtifact(sidecarPath, existing)) {
|
|
17357
|
+
return;
|
|
17358
|
+
}
|
|
17359
|
+
const previous = this.sidecarBases.get(dbConfig.databaseId);
|
|
17360
|
+
if (previous) {
|
|
17361
|
+
const written = previous.path === sidecarPath ? existing : await this.vaultFs.readFile(previous.path).catch(() => null);
|
|
17362
|
+
if (written !== null && written !== serialized && computeHash(written) !== previous.hash && isGeneratedDbArtifact(previous.path, written) && sidecarPropertiesEdited(written, previous)) {
|
|
17363
|
+
this.heldSidecars.add(previous.path);
|
|
17364
|
+
return;
|
|
17365
|
+
}
|
|
17366
|
+
}
|
|
17367
|
+
this.sidecarBases.set(dbConfig.databaseId, {
|
|
17368
|
+
path: sidecarPath,
|
|
17369
|
+
hash: computeHash(serialized),
|
|
17370
|
+
properties: sidecar.properties
|
|
17371
|
+
});
|
|
16519
17372
|
if (existing === serialized) return;
|
|
16520
|
-
if (existing !== null && !isGeneratedDbArtifact(sidecarPath, existing)) return;
|
|
16521
17373
|
await this.vaultFs.writeFile(sidecarPath, serialized);
|
|
16522
17374
|
if (sidecar.degraded.length > 0) {
|
|
16523
17375
|
const unrep = sidecar.degraded.filter((d) => d.kind === "view-unrepresentable").length;
|
|
@@ -17000,19 +17852,19 @@ function threeWayMerge(base, local, remote) {
|
|
|
17000
17852
|
}
|
|
17001
17853
|
const regionStart = first.baseStart;
|
|
17002
17854
|
let regionEnd = first.baseEnd;
|
|
17003
|
-
const
|
|
17855
|
+
const group2 = [first];
|
|
17004
17856
|
i++;
|
|
17005
17857
|
while (i < hunks.length) {
|
|
17006
17858
|
const next = hunks[i];
|
|
17007
17859
|
const overlaps = next.baseStart < regionEnd || regionEnd === regionStart && next.baseStart === regionStart;
|
|
17008
17860
|
if (!overlaps) break;
|
|
17009
17861
|
regionEnd = Math.max(regionEnd, next.baseEnd);
|
|
17010
|
-
|
|
17862
|
+
group2.push(next);
|
|
17011
17863
|
i++;
|
|
17012
17864
|
}
|
|
17013
17865
|
const baseRegion = baseLines.slice(regionStart, regionEnd);
|
|
17014
|
-
const localRegion = applyHunks(baseLines,
|
|
17015
|
-
const remoteRegion = applyHunks(baseLines,
|
|
17866
|
+
const localRegion = applyHunks(baseLines, group2, 0, regionStart, regionEnd);
|
|
17867
|
+
const remoteRegion = applyHunks(baseLines, group2, 2, regionStart, regionEnd);
|
|
17016
17868
|
if (arraysEqual(localRegion, baseRegion)) {
|
|
17017
17869
|
merged.push(...remoteRegion);
|
|
17018
17870
|
} else if (arraysEqual(remoteRegion, baseRegion)) {
|
|
@@ -17063,10 +17915,10 @@ function computeHunks(base, modified, side) {
|
|
|
17063
17915
|
emitGap(base.length, modified.length);
|
|
17064
17916
|
return hunks;
|
|
17065
17917
|
}
|
|
17066
|
-
function applyHunks(baseLines,
|
|
17918
|
+
function applyHunks(baseLines, group2, side, regionStart, regionEnd) {
|
|
17067
17919
|
const out = [];
|
|
17068
17920
|
let b = regionStart;
|
|
17069
|
-
for (const hunk of
|
|
17921
|
+
for (const hunk of group2) {
|
|
17070
17922
|
if (hunk.side !== side) continue;
|
|
17071
17923
|
if (hunk.baseStart > b) out.push(...baseLines.slice(b, hunk.baseStart));
|
|
17072
17924
|
out.push(...hunk.content);
|
|
@@ -17421,165 +18273,1071 @@ var ScopedVaultFS = class {
|
|
|
17421
18273
|
const local = this.fromBase(item.path);
|
|
17422
18274
|
if (local !== null) out.push({ ...item, path: local });
|
|
17423
18275
|
}
|
|
17424
|
-
return out;
|
|
18276
|
+
return out;
|
|
18277
|
+
}
|
|
18278
|
+
};
|
|
18279
|
+
|
|
18280
|
+
// src/sync/pull-watermark.ts
|
|
18281
|
+
var INCREMENTAL_SAFETY_WINDOW_MS = 15 * 6e4;
|
|
18282
|
+
function incrementalSearchSince(watermark) {
|
|
18283
|
+
const ms = Date.parse(watermark);
|
|
18284
|
+
return Number.isFinite(ms) ? new Date(ms - INCREMENTAL_SAFETY_WINDOW_MS).toISOString() : watermark;
|
|
18285
|
+
}
|
|
18286
|
+
function nextPullWatermark(startedAt, unapplied) {
|
|
18287
|
+
let earliest = Date.parse(startedAt);
|
|
18288
|
+
for (const change of unapplied) {
|
|
18289
|
+
if (change.type === "deleted") continue;
|
|
18290
|
+
const edited = Date.parse(change.lastEdited);
|
|
18291
|
+
if (Number.isFinite(edited) && edited < earliest) earliest = edited;
|
|
18292
|
+
}
|
|
18293
|
+
return new Date(earliest).toISOString();
|
|
18294
|
+
}
|
|
18295
|
+
|
|
18296
|
+
// src/sync/remote-scan.ts
|
|
18297
|
+
var LAST_FULL_PULL_META_KEY = "last_full_pull_at";
|
|
18298
|
+
var LAST_PULL_META_KEY = "last_pull_at";
|
|
18299
|
+
var PENDING_DATABASES_META_KEY = "db_pull_pending";
|
|
18300
|
+
function chooseRemoteScan(state, options) {
|
|
18301
|
+
if (options.force) return { kind: "full", reason: "forced" };
|
|
18302
|
+
if (state.databaseMode) return { kind: "full", reason: "database-mode" };
|
|
18303
|
+
if (!state.lastPullAt || state.trackedRecords === 0) return { kind: "full", reason: "first" };
|
|
18304
|
+
if (!Number.isFinite(state.fullReconcileIntervalSec) || state.fullReconcileIntervalSec <= 0) {
|
|
18305
|
+
return { kind: "full", reason: "every-pull" };
|
|
18306
|
+
}
|
|
18307
|
+
const nextFullAt = nextFullReconcileAt(state.lastFullPullAt, state.fullReconcileIntervalSec);
|
|
18308
|
+
if (nextFullAt === null || Date.parse(nextFullAt) <= options.now) {
|
|
18309
|
+
if (!options.deferDue) return { kind: "full", reason: "due" };
|
|
18310
|
+
return {
|
|
18311
|
+
kind: "incremental",
|
|
18312
|
+
since: state.lastPullAt,
|
|
18313
|
+
lastFullAt: state.lastFullPullAt,
|
|
18314
|
+
nextFullAt: null
|
|
18315
|
+
};
|
|
18316
|
+
}
|
|
18317
|
+
return {
|
|
18318
|
+
kind: "incremental",
|
|
18319
|
+
since: state.lastPullAt,
|
|
18320
|
+
lastFullAt: state.lastFullPullAt,
|
|
18321
|
+
nextFullAt
|
|
18322
|
+
};
|
|
18323
|
+
}
|
|
18324
|
+
function nextFullReconcileAt(lastFullPullAt, intervalSec) {
|
|
18325
|
+
const last = lastFullPullAt === null ? NaN : Date.parse(lastFullPullAt);
|
|
18326
|
+
return Number.isFinite(last) ? new Date(last + intervalSec * 1e3).toISOString() : null;
|
|
18327
|
+
}
|
|
18328
|
+
function remoteScanInfo(scan, lastFullPullAt, deleteSync, deletionsChecked = false) {
|
|
18329
|
+
return scan.kind === "full" ? {
|
|
18330
|
+
kind: "full",
|
|
18331
|
+
reason: scan.reason,
|
|
18332
|
+
lastFullAt: lastFullPullAt,
|
|
18333
|
+
nextFullAt: null,
|
|
18334
|
+
deletionsDeferred: false
|
|
18335
|
+
} : {
|
|
18336
|
+
kind: "incremental",
|
|
18337
|
+
lastFullAt: scan.lastFullAt,
|
|
18338
|
+
nextFullAt: scan.nextFullAt,
|
|
18339
|
+
deletionsDeferred: deleteSync && !deletionsChecked
|
|
18340
|
+
};
|
|
18341
|
+
}
|
|
18342
|
+
var ALL_DATABASES = { kind: "all" };
|
|
18343
|
+
function selectsDatabase(selection, databaseId) {
|
|
18344
|
+
return selection.kind === "all" || selection.ids.has(compactNotionId(databaseId));
|
|
18345
|
+
}
|
|
18346
|
+
function parsePendingDatabases(raw) {
|
|
18347
|
+
if (!raw) return /* @__PURE__ */ new Set();
|
|
18348
|
+
try {
|
|
18349
|
+
const parsed = JSON.parse(raw);
|
|
18350
|
+
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
|
|
18351
|
+
return new Set(
|
|
18352
|
+
parsed.filter((id) => typeof id === "string").map((id) => compactNotionId(id))
|
|
18353
|
+
);
|
|
18354
|
+
} catch {
|
|
18355
|
+
return /* @__PURE__ */ new Set();
|
|
18356
|
+
}
|
|
18357
|
+
}
|
|
18358
|
+
function serializePendingDatabases(ids) {
|
|
18359
|
+
return JSON.stringify([...ids].sort());
|
|
18360
|
+
}
|
|
18361
|
+
function addPendingDatabases(stateDb, databaseIds) {
|
|
18362
|
+
const pending = parsePendingDatabases(stateDb.getMeta(PENDING_DATABASES_META_KEY));
|
|
18363
|
+
const before = pending.size;
|
|
18364
|
+
for (const id of databaseIds) pending.add(compactNotionId(id));
|
|
18365
|
+
if (pending.size !== before) {
|
|
18366
|
+
stateDb.setMeta(PENDING_DATABASES_META_KEY, serializePendingDatabases(pending));
|
|
18367
|
+
}
|
|
18368
|
+
}
|
|
18369
|
+
var DatabasePullLedger = class {
|
|
18370
|
+
constructor(selection, pending) {
|
|
18371
|
+
this.selection = selection;
|
|
18372
|
+
this.pending = pending;
|
|
18373
|
+
}
|
|
18374
|
+
selection;
|
|
18375
|
+
pending;
|
|
18376
|
+
settled = /* @__PURE__ */ new Set();
|
|
18377
|
+
unfinished = /* @__PURE__ */ new Set();
|
|
18378
|
+
skippedCount = 0;
|
|
18379
|
+
/** 이제 동기화하는 DB — 알면 대기를 이것으로 좁힌다. */
|
|
18380
|
+
syncing = null;
|
|
18381
|
+
/** 이 DB 를 조회하나 — 새로 발견한 DB 는 부른 쪽이 따로 조회한다(받은 적이 없다). */
|
|
18382
|
+
selects(databaseId) {
|
|
18383
|
+
return selectsDatabase(this.selection, databaseId);
|
|
18384
|
+
}
|
|
18385
|
+
/** 바뀐 것이 보이지 않아 조회하지 않았다. */
|
|
18386
|
+
skip() {
|
|
18387
|
+
this.skippedCount++;
|
|
18388
|
+
}
|
|
18389
|
+
/** 받았거나 · 이제 없어서 뺐다 — 다시 조회할 것이 없다. */
|
|
18390
|
+
settle(databaseId) {
|
|
18391
|
+
this.settled.add(compactNotionId(databaseId));
|
|
18392
|
+
}
|
|
18393
|
+
/** 조회하다 실패했거나 · 받지 못한 행이 있거나 · 취소로 닿지 못했다 — 다음 pull 이 다시 조회한다. */
|
|
18394
|
+
retry(databaseId) {
|
|
18395
|
+
this.unfinished.add(compactNotionId(databaseId));
|
|
18396
|
+
}
|
|
18397
|
+
get skipped() {
|
|
18398
|
+
return this.skippedCount;
|
|
18399
|
+
}
|
|
18400
|
+
/**
|
|
18401
|
+
* 이제 동기화하는 DB 를 알린다 — 대기를 이것으로 좁힌다. 설정에서 뺐거나 접근 불가로 뺀 DB 를 영영
|
|
18402
|
+
* 들고 있지 않는다. 알리지 않으면(발견이 멈춤 등) 좁히지 않는다 — 모르는 것을 버리지 않는다.
|
|
18403
|
+
*/
|
|
18404
|
+
keepOnly(databaseIds) {
|
|
18405
|
+
this.syncing = new Set([...databaseIds].map((id) => compactNotionId(id)));
|
|
18406
|
+
}
|
|
18407
|
+
/** 다음 pull 의 대기 — 지난 대기에서 받은 것을 빼고 이번에 받지 못한 것을 더한다. */
|
|
18408
|
+
nextPending() {
|
|
18409
|
+
const next = new Set([...this.pending].filter((id) => !this.settled.has(id)));
|
|
18410
|
+
for (const id of this.unfinished) next.add(id);
|
|
18411
|
+
const syncing = this.syncing;
|
|
18412
|
+
return syncing ? new Set([...next].filter((id) => syncing.has(id))) : next;
|
|
18413
|
+
}
|
|
18414
|
+
};
|
|
18415
|
+
|
|
18416
|
+
// src/sync/schema-push.ts
|
|
18417
|
+
var NOT_PROPERTIES = /* @__PURE__ */ new Set(["title", ...DECORATION_KEYS]);
|
|
18418
|
+
var VaultSchemaRenames = class {
|
|
18419
|
+
constructor(stateDb, vaultFs, notionClient) {
|
|
18420
|
+
this.stateDb = stateDb;
|
|
18421
|
+
this.vaultFs = vaultFs;
|
|
18422
|
+
this.notionClient = notionClient;
|
|
18423
|
+
this.ledger = new SchemaLedger(stateDb);
|
|
18424
|
+
}
|
|
18425
|
+
stateDb;
|
|
18426
|
+
vaultFs;
|
|
18427
|
+
notionClient;
|
|
18428
|
+
ledger;
|
|
18429
|
+
/** 이번 push 의 바뀐 행에서 이름 바꾸기를 찾는다 — Notion 은 그런 DB 만 읽고, 쓰지 않는다. */
|
|
18430
|
+
async plan(changes) {
|
|
18431
|
+
const byDatabase = /* @__PURE__ */ new Map();
|
|
18432
|
+
for (const change of changes) {
|
|
18433
|
+
if (change.type !== "modified") continue;
|
|
18434
|
+
const row = await this.changedRow(change.path);
|
|
18435
|
+
if (!row) continue;
|
|
18436
|
+
const key = compactNotionId(row.record.notionParentId);
|
|
18437
|
+
byDatabase.set(key, [...byDatabase.get(key) ?? [], row]);
|
|
18438
|
+
}
|
|
18439
|
+
const planned = [];
|
|
18440
|
+
const refused = [];
|
|
18441
|
+
for (const rows of byDatabase.values()) {
|
|
18442
|
+
const databaseId = rows[0].record.notionParentId;
|
|
18443
|
+
const pairs = await this.localRenames(databaseId, rows);
|
|
18444
|
+
if (pairs.length === 0) continue;
|
|
18445
|
+
const meta = await this.notionClient.getDatabaseMeta(databaseId);
|
|
18446
|
+
for (const [from, to] of pairs) {
|
|
18447
|
+
const carrying = rows.filter((row) => Object.hasOwn(row.current, to));
|
|
18448
|
+
const folder = folderOf4(carrying[0].record.obsidianPath);
|
|
18449
|
+
const owners = meta.dataSources.filter(
|
|
18450
|
+
(ds) => ds.properties?.[from] !== void 0
|
|
18451
|
+
);
|
|
18452
|
+
if (owners.length === 0) continue;
|
|
18453
|
+
const owner = owners[0];
|
|
18454
|
+
const ownProps = owner.properties;
|
|
18455
|
+
if (Object.hasOwn(ownProps, to)) continue;
|
|
18456
|
+
const propertyId = ownProps[from]?.id;
|
|
18457
|
+
if (owners.length > 1 || typeof propertyId !== "string") {
|
|
18458
|
+
const error = `\uC18D\uC131 \uC774\uB984 \u300C${from}\u300D \u2192 \u300C${to}\u300D \uB97C Notion \uC5D0 \uC62E\uAE38 \uC218 \uC5C6\uC5B4 \uC62C\uB9AC\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4 \u2014 \uC774 DB \uC758 \uB370\uC774\uD130 \uC18C\uC2A4 \uC5EC\uB7FF\uC5D0 \u300C${from}\u300D \uC774 \uC788\uC2B5\uB2C8\uB2E4. Notion \uC5D0\uC11C \uBC14\uAFB8\uC138\uC694`;
|
|
18459
|
+
refused.push(
|
|
18460
|
+
...carrying.map((row) => ({
|
|
18461
|
+
path: row.record.obsidianPath,
|
|
18462
|
+
operation: "update",
|
|
18463
|
+
error
|
|
18464
|
+
}))
|
|
18465
|
+
);
|
|
18466
|
+
continue;
|
|
18467
|
+
}
|
|
18468
|
+
planned.push({
|
|
18469
|
+
databaseId,
|
|
18470
|
+
dataSourceId: owner.id,
|
|
18471
|
+
propertyId,
|
|
18472
|
+
folder,
|
|
18473
|
+
from,
|
|
18474
|
+
to,
|
|
18475
|
+
rows: carrying.map((row) => row.record)
|
|
18476
|
+
});
|
|
18477
|
+
}
|
|
18478
|
+
}
|
|
18479
|
+
return { planned, refused };
|
|
18480
|
+
}
|
|
18481
|
+
/**
|
|
18482
|
+
* 열 이름을 바꾸고, 그 행들의 동기화 사본도 새 키로 — 사본이 옛 키를 들고 있으면 행 push 가 옛 열을 비운다. 이름
|
|
18483
|
+
* 바꾸기 말고 바뀐 것이 없는 행은 동기화를 마친 것으로 적는다.
|
|
18484
|
+
*/
|
|
18485
|
+
async apply(planned) {
|
|
18486
|
+
const applied = [];
|
|
18487
|
+
const settled = /* @__PURE__ */ new Set();
|
|
18488
|
+
const failed = [];
|
|
18489
|
+
for (const rename2 of planned) {
|
|
18490
|
+
try {
|
|
18491
|
+
const after = await this.notionClient.updateDataSourceProperties(rename2.dataSourceId, {
|
|
18492
|
+
[rename2.propertyId]: { name: rename2.to }
|
|
18493
|
+
});
|
|
18494
|
+
if (after[rename2.to]?.id !== rename2.propertyId) {
|
|
18495
|
+
getLogger().warn(
|
|
18496
|
+
`[Im-Nobsidian] \uC18D\uC131 \uC774\uB984 \u300C${rename2.from}\u300D \u2192 \u300C${rename2.to}\u300D: Notion \uC774 \uB2E4\uB978 \uC774\uB984\uC744 \uBD99\uC600\uC2B5\uB2C8\uB2E4 \u2014 Notion \uC5D0\uC11C \uD655\uC778\uD558\uC138\uC694`
|
|
18497
|
+
);
|
|
18498
|
+
}
|
|
18499
|
+
} catch (error) {
|
|
18500
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
18501
|
+
for (const record of rename2.rows) {
|
|
18502
|
+
failed.push({
|
|
18503
|
+
path: record.obsidianPath,
|
|
18504
|
+
operation: "update",
|
|
18505
|
+
error: `\uC18D\uC131 \uC774\uB984 \u300C${rename2.from}\u300D \u2192 \u300C${rename2.to}\u300D \uB97C Notion \uC5D0\uC11C \uBC14\uAFB8\uC9C0 \uBABB\uD574 \uC62C\uB9AC\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4: ${reason}`
|
|
18506
|
+
});
|
|
18507
|
+
}
|
|
18508
|
+
continue;
|
|
18509
|
+
}
|
|
18510
|
+
applied.push({ folder: rename2.folder, from: rename2.from, to: rename2.to });
|
|
18511
|
+
this.noteRenamed(rename2);
|
|
18512
|
+
for (const record of rename2.rows) {
|
|
18513
|
+
if (await this.rekeySnapshot(record, rename2)) settled.add(record.obsidianPath);
|
|
18514
|
+
}
|
|
18515
|
+
}
|
|
18516
|
+
return { applied, settled, failed };
|
|
18517
|
+
}
|
|
18518
|
+
/** 바뀐 행 노트 — 지난 동기화 사본이 있는 DB 행만. */
|
|
18519
|
+
async changedRow(path) {
|
|
18520
|
+
const record = this.stateDb.getByPath(path);
|
|
18521
|
+
if (record?.fileType !== "db-row" || !record.notionParentId) return null;
|
|
18522
|
+
const base = snapshotFrontmatter(record.baseSnapshot);
|
|
18523
|
+
if (base === null) return null;
|
|
18524
|
+
try {
|
|
18525
|
+
const current = splitFrontmatter(await this.vaultFs.readFile(path)).data;
|
|
18526
|
+
return { record, base, current };
|
|
18527
|
+
} catch {
|
|
18528
|
+
return null;
|
|
18529
|
+
}
|
|
18530
|
+
}
|
|
18531
|
+
/** 바뀐 행이 말하는 이름 바꾸기 — 옛 키 → 새 키. 한 키를 두 갈래로 읽을 수 있으면 뺀다. */
|
|
18532
|
+
async localRenames(databaseId, rows) {
|
|
18533
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
18534
|
+
for (const { base, current } of rows) {
|
|
18535
|
+
for (const from of Object.keys(base)) {
|
|
18536
|
+
if (NOT_PROPERTIES.has(from) || Object.hasOwn(current, from)) continue;
|
|
18537
|
+
const value = plainFrontmatterValue(base[from]);
|
|
18538
|
+
if (isEmptyFrontmatterValue(value)) continue;
|
|
18539
|
+
for (const to of Object.keys(current)) {
|
|
18540
|
+
if (NOT_PROPERTIES.has(to) || Object.hasOwn(base, to)) continue;
|
|
18541
|
+
if (!sameFrontmatterValue(value, plainFrontmatterValue(current[to]))) continue;
|
|
18542
|
+
candidates.set(from, /* @__PURE__ */ new Set([...candidates.get(from) ?? [], to]));
|
|
18543
|
+
}
|
|
18544
|
+
}
|
|
18545
|
+
}
|
|
18546
|
+
const pairs = [];
|
|
18547
|
+
const targets = /* @__PURE__ */ new Map();
|
|
18548
|
+
for (const [from, tos] of candidates) {
|
|
18549
|
+
const consistent = [...tos].filter(
|
|
18550
|
+
(to) => rows.every(
|
|
18551
|
+
({ base, current }) => !Object.hasOwn(current, from) && (isEmptyFrontmatterValue(plainFrontmatterValue(base[from])) || Object.hasOwn(current, to))
|
|
18552
|
+
)
|
|
18553
|
+
);
|
|
18554
|
+
if (consistent.length !== 1) continue;
|
|
18555
|
+
pairs.push([from, consistent[0]]);
|
|
18556
|
+
targets.set(consistent[0], (targets.get(consistent[0]) ?? 0) + 1);
|
|
18557
|
+
}
|
|
18558
|
+
const unique = pairs.filter(([, to]) => targets.get(to) === 1);
|
|
18559
|
+
if (unique.length === 0) return [];
|
|
18560
|
+
const changed = new Set(rows.map((row) => row.record.id));
|
|
18561
|
+
const others = this.stateDb.getAll().filter(
|
|
18562
|
+
(record) => record.fileType === "db-row" && record.notionParentId !== null && notionIdsEqual(record.notionParentId, databaseId) && !changed.has(record.id)
|
|
18563
|
+
);
|
|
18564
|
+
const keysOf = await Promise.all(others.map((record) => this.keysOf(record)));
|
|
18565
|
+
return unique.filter(
|
|
18566
|
+
([from, to]) => keysOf.every((keys) => keys !== null && !keys.has(from) && !keys.has(to))
|
|
18567
|
+
);
|
|
18568
|
+
}
|
|
18569
|
+
/** 행 노트에 지금 적힌 키 — 읽지 못하면 null(그 DB 의 이름 바꾸기는 보지 않는다). */
|
|
18570
|
+
async keysOf(record) {
|
|
18571
|
+
try {
|
|
18572
|
+
const data = splitFrontmatter(await this.vaultFs.readFile(record.obsidianPath)).data;
|
|
18573
|
+
return new Set(Object.keys(data).filter((key) => !isEmptyFrontmatterValue(data[key])));
|
|
18574
|
+
} catch {
|
|
18575
|
+
return null;
|
|
18576
|
+
}
|
|
18577
|
+
}
|
|
18578
|
+
/** 그 행의 사본을 새 키로 — 노트가 사본과 같아졌으면 true(동기화를 마쳤다). */
|
|
18579
|
+
async rekeySnapshot(record, rename2) {
|
|
18580
|
+
const snapshot = record.baseSnapshot?.toString("utf-8");
|
|
18581
|
+
if (snapshot === void 0) return false;
|
|
18582
|
+
const lines = rekeyRowFrontmatter(snapshot, /* @__PURE__ */ new Map([[rename2.from, rename2.to]]), () => false);
|
|
18583
|
+
const next = lines === null ? null : replaceFrontmatterLines(snapshot, lines);
|
|
18584
|
+
if (next === null) return false;
|
|
18585
|
+
let local = null;
|
|
18586
|
+
try {
|
|
18587
|
+
local = await this.vaultFs.readFile(record.obsidianPath);
|
|
18588
|
+
} catch {
|
|
18589
|
+
}
|
|
18590
|
+
const synced = local !== null && sameRow(local, next);
|
|
18591
|
+
const content = synced ? local : next;
|
|
18592
|
+
this.stateDb.transaction(() => {
|
|
18593
|
+
this.stateDb.updateHash(record.id, computeHash(content), Buffer.from(content, "utf-8"));
|
|
18594
|
+
});
|
|
18595
|
+
return synced;
|
|
18596
|
+
}
|
|
18597
|
+
/** 지난 pull 이 본 스키마에도 새 이름을 적는다 — 다음 pull 이 이 이름 바꾸기를 Notion 의 것으로 다시 옮기지 않게. */
|
|
18598
|
+
noteRenamed(rename2) {
|
|
18599
|
+
const seen = this.ledger.get(rename2.databaseId);
|
|
18600
|
+
const source = compactNotionId(rename2.dataSourceId);
|
|
18601
|
+
const known = seen?.[source]?.[rename2.propertyId];
|
|
18602
|
+
if (!seen || !known) return;
|
|
18603
|
+
this.ledger.set(rename2.databaseId, {
|
|
18604
|
+
[source]: { ...seen[source], [rename2.propertyId]: { ...known, name: rename2.to } }
|
|
18605
|
+
});
|
|
18606
|
+
}
|
|
18607
|
+
};
|
|
18608
|
+
function sameRow(a, b) {
|
|
18609
|
+
const left = splitFrontmatter(a);
|
|
18610
|
+
const right = splitFrontmatter(b);
|
|
18611
|
+
const keys = Object.keys(left.data);
|
|
18612
|
+
return left.content === right.content && keys.length === Object.keys(right.data).length && keys.every((key) => sameFrontmatterValue(left.data[key], right.data[key]));
|
|
18613
|
+
}
|
|
18614
|
+
function folderOf4(path) {
|
|
18615
|
+
const at = path.lastIndexOf("/");
|
|
18616
|
+
return at === -1 ? "" : path.slice(0, at);
|
|
18617
|
+
}
|
|
18618
|
+
|
|
18619
|
+
// src/view/bases-filter-to-notion.ts
|
|
18620
|
+
var MAX_FILTER_DEPTH = 2;
|
|
18621
|
+
var TEXT_TYPES = /* @__PURE__ */ new Set([
|
|
18622
|
+
"title",
|
|
18623
|
+
"rich_text",
|
|
18624
|
+
"url",
|
|
18625
|
+
"email",
|
|
18626
|
+
"phone_number"
|
|
18627
|
+
]);
|
|
18628
|
+
var EMPTY_TYPES = /* @__PURE__ */ new Set([
|
|
18629
|
+
...TEXT_TYPES,
|
|
18630
|
+
"number",
|
|
18631
|
+
"select",
|
|
18632
|
+
"status",
|
|
18633
|
+
"multi_select",
|
|
18634
|
+
"date"
|
|
18635
|
+
]);
|
|
18636
|
+
var NUMBER_OPS = {
|
|
18637
|
+
"==": "equals",
|
|
18638
|
+
"!=": "does_not_equal",
|
|
18639
|
+
">": "greater_than",
|
|
18640
|
+
"<": "less_than",
|
|
18641
|
+
">=": "greater_than_or_equal_to",
|
|
18642
|
+
"<=": "less_than_or_equal_to"
|
|
18643
|
+
};
|
|
18644
|
+
var DATE_OPS = {
|
|
18645
|
+
"==": "equals",
|
|
18646
|
+
"<": "before",
|
|
18647
|
+
">": "after",
|
|
18648
|
+
"<=": "on_or_before",
|
|
18649
|
+
">=": "on_or_after"
|
|
18650
|
+
};
|
|
18651
|
+
var RELATIVE_VALUE_OF = new Map(
|
|
18652
|
+
Object.entries(RELATIVE_DATE_VALUES).map(([value, expr]) => [squash(expr), value])
|
|
18653
|
+
);
|
|
18654
|
+
var Refusal = class extends Error {
|
|
18655
|
+
};
|
|
18656
|
+
function refuse(message) {
|
|
18657
|
+
throw new Refusal(message);
|
|
18658
|
+
}
|
|
18659
|
+
function basesFilterToNotion(node, schema) {
|
|
18660
|
+
try {
|
|
18661
|
+
const filter = settle([toFilter(node, schema)])[0];
|
|
18662
|
+
if (depthOf(filter) > MAX_FILTER_DEPTH) {
|
|
18663
|
+
refuse("Notion \uD544\uD130\uB294 and \xB7 or \uBB36\uC74C\uC744 \uB450 \uACB9\uAE4C\uC9C0\uB9CC \uBC1B\uC2B5\uB2C8\uB2E4");
|
|
18664
|
+
}
|
|
18665
|
+
return { ok: true, filter };
|
|
18666
|
+
} catch (error) {
|
|
18667
|
+
if (error instanceof Refusal) return { ok: false, error: error.message };
|
|
18668
|
+
throw error;
|
|
18669
|
+
}
|
|
18670
|
+
}
|
|
18671
|
+
function toFilter(node, schema) {
|
|
18672
|
+
if (typeof node === "string") return compile(parseExpression(node), schema);
|
|
18673
|
+
if (!node || typeof node !== "object" || Array.isArray(node)) {
|
|
18674
|
+
refuse("\uD544\uD130\uB294 \uC2DD \uD558\uB098\uC774\uAC70\uB098 and \xB7 or \xB7 not \uBB36\uC74C\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4");
|
|
18675
|
+
}
|
|
18676
|
+
const group2 = node;
|
|
18677
|
+
const keys = Object.keys(group2);
|
|
18678
|
+
if (keys.length !== 1 || !["and", "or", "not"].includes(keys[0])) {
|
|
18679
|
+
refuse(`\uD544\uD130 \uBB36\uC74C\uC740 and \xB7 or \xB7 not \uC911 \uD558\uB098\uC5EC\uC57C \uD569\uB2C8\uB2E4(${keys.join(", ")})`);
|
|
18680
|
+
}
|
|
18681
|
+
const op = keys[0];
|
|
18682
|
+
const children = group2[op];
|
|
18683
|
+
if (!Array.isArray(children) || children.length === 0) refuse(`${op} \uBB36\uC74C\uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4`);
|
|
18684
|
+
const asts = children.map(
|
|
18685
|
+
(child) => typeof child === "string" ? parseExpression(child) : { kind: "yaml", node: child }
|
|
18686
|
+
);
|
|
18687
|
+
const ast = op === "not" ? { kind: "and", items: asts.map((item) => ({ kind: "not", item })) } : { kind: op, items: asts };
|
|
18688
|
+
return compile(ast, schema);
|
|
18689
|
+
}
|
|
18690
|
+
var OPERATORS = [
|
|
18691
|
+
"&&",
|
|
18692
|
+
"||",
|
|
18693
|
+
"==",
|
|
18694
|
+
"!=",
|
|
18695
|
+
">=",
|
|
18696
|
+
"<=",
|
|
18697
|
+
">",
|
|
18698
|
+
"<",
|
|
18699
|
+
"!",
|
|
18700
|
+
"+",
|
|
18701
|
+
"-",
|
|
18702
|
+
"(",
|
|
18703
|
+
")",
|
|
18704
|
+
"[",
|
|
18705
|
+
"]",
|
|
18706
|
+
".",
|
|
18707
|
+
","
|
|
18708
|
+
];
|
|
18709
|
+
function tokenize(text) {
|
|
18710
|
+
const tokens = [];
|
|
18711
|
+
let i = 0;
|
|
18712
|
+
while (i < text.length) {
|
|
18713
|
+
const c = text[i];
|
|
18714
|
+
if (/\s/.test(c)) {
|
|
18715
|
+
i++;
|
|
18716
|
+
continue;
|
|
18717
|
+
}
|
|
18718
|
+
if (c === '"' || c === "'") {
|
|
18719
|
+
let value = "";
|
|
18720
|
+
i++;
|
|
18721
|
+
while (i < text.length && text[i] !== c) {
|
|
18722
|
+
if (text[i] === "\\" && i + 1 < text.length) i++;
|
|
18723
|
+
value += text[i];
|
|
18724
|
+
i++;
|
|
18725
|
+
}
|
|
18726
|
+
if (i >= text.length) refuse(`\uB530\uC634\uD45C\uAC00 \uB2EB\uD788\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4: ${text}`);
|
|
18727
|
+
i++;
|
|
18728
|
+
tokens.push({ t: "string", v: value });
|
|
18729
|
+
continue;
|
|
18730
|
+
}
|
|
18731
|
+
const number = /^\d+(?:\.\d+)?/.exec(text.slice(i));
|
|
18732
|
+
if (number) {
|
|
18733
|
+
tokens.push({ t: "number", v: Number(number[0]) });
|
|
18734
|
+
i += number[0].length;
|
|
18735
|
+
continue;
|
|
18736
|
+
}
|
|
18737
|
+
const op = OPERATORS.find((o) => text.startsWith(o, i));
|
|
18738
|
+
if (op) {
|
|
18739
|
+
tokens.push({ t: "op", v: op });
|
|
18740
|
+
i += op.length;
|
|
18741
|
+
continue;
|
|
18742
|
+
}
|
|
18743
|
+
const ident = /^[\p{L}\p{N}_]+/u.exec(text.slice(i));
|
|
18744
|
+
if (ident) {
|
|
18745
|
+
tokens.push({ t: "ident", v: ident[0] });
|
|
18746
|
+
i += ident[0].length;
|
|
18747
|
+
continue;
|
|
18748
|
+
}
|
|
18749
|
+
refuse(`\uD544\uD130 \uC2DD\uC744 \uC77D\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4: ${text}`);
|
|
17425
18750
|
}
|
|
17426
|
-
|
|
17427
|
-
|
|
17428
|
-
// src/sync/pull-watermark.ts
|
|
17429
|
-
var INCREMENTAL_SAFETY_WINDOW_MS = 15 * 6e4;
|
|
17430
|
-
function incrementalSearchSince(watermark) {
|
|
17431
|
-
const ms = Date.parse(watermark);
|
|
17432
|
-
return Number.isFinite(ms) ? new Date(ms - INCREMENTAL_SAFETY_WINDOW_MS).toISOString() : watermark;
|
|
18751
|
+
return tokens;
|
|
17433
18752
|
}
|
|
17434
|
-
function
|
|
17435
|
-
|
|
17436
|
-
|
|
17437
|
-
|
|
17438
|
-
const
|
|
17439
|
-
|
|
17440
|
-
}
|
|
17441
|
-
|
|
18753
|
+
function parseExpression(text) {
|
|
18754
|
+
const tokens = tokenize(text);
|
|
18755
|
+
let pos = 0;
|
|
18756
|
+
const peek = (v) => {
|
|
18757
|
+
const token2 = tokens[pos];
|
|
18758
|
+
return v === void 0 || token2?.t === "op" && token2.v === v ? token2 : void 0;
|
|
18759
|
+
};
|
|
18760
|
+
const take = (v) => {
|
|
18761
|
+
if (!peek(v)) return false;
|
|
18762
|
+
pos++;
|
|
18763
|
+
return true;
|
|
18764
|
+
};
|
|
18765
|
+
const expect = (v) => {
|
|
18766
|
+
if (!take(v)) refuse(`\uD544\uD130 \uC2DD\uC5D0\uC11C '${v}' \uAC00 \uC640\uC57C \uD569\uB2C8\uB2E4: ${text}`);
|
|
18767
|
+
};
|
|
18768
|
+
const orExpr = () => {
|
|
18769
|
+
const items = [andExpr()];
|
|
18770
|
+
while (take("||")) items.push(andExpr());
|
|
18771
|
+
return items.length === 1 ? items[0] : { kind: "or", items };
|
|
18772
|
+
};
|
|
18773
|
+
const andExpr = () => {
|
|
18774
|
+
const items = [unary()];
|
|
18775
|
+
while (take("&&")) items.push(unary());
|
|
18776
|
+
return items.length === 1 ? items[0] : { kind: "and", items };
|
|
18777
|
+
};
|
|
18778
|
+
const unary = () => {
|
|
18779
|
+
if (take("!")) return { kind: "not", item: unary() };
|
|
18780
|
+
if (peek("(")) {
|
|
18781
|
+
pos++;
|
|
18782
|
+
const inner = orExpr();
|
|
18783
|
+
expect(")");
|
|
18784
|
+
return inner;
|
|
18785
|
+
}
|
|
18786
|
+
return comparison();
|
|
18787
|
+
};
|
|
18788
|
+
const comparison = () => {
|
|
18789
|
+
const left = operand();
|
|
18790
|
+
const op = tokens[pos];
|
|
18791
|
+
if (op?.t === "op" && ["==", "!=", ">", "<", ">=", "<="].includes(op.v)) {
|
|
18792
|
+
if (left.call) refuse(`\uD568\uC218 \uACB0\uACFC\uB97C \uACAC\uC8FC\uB294 \uC2DD\uC740 \uC62E\uAE30\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${text}`);
|
|
18793
|
+
pos++;
|
|
18794
|
+
return { kind: "compare", op: op.v, left: left.term, right: operand().term };
|
|
18795
|
+
}
|
|
18796
|
+
if (left.call) return left.call;
|
|
18797
|
+
refuse(`\uBE44\uAD50\uB098 \uD568\uC218 \uD638\uCD9C\uC774 \uC544\uB2CC \uC2DD\uC740 \uC62E\uAE30\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${text}`);
|
|
18798
|
+
};
|
|
18799
|
+
const operand = () => {
|
|
18800
|
+
const token2 = tokens[pos];
|
|
18801
|
+
if (!token2) refuse(`\uD544\uD130 \uC2DD\uC774 \uB35C \uB05D\uB0AC\uC2B5\uB2C8\uB2E4: ${text}`);
|
|
18802
|
+
if (token2.t === "string") {
|
|
18803
|
+
pos++;
|
|
18804
|
+
return { term: { kind: "string", value: token2.v } };
|
|
18805
|
+
}
|
|
18806
|
+
if (token2.t === "number") {
|
|
18807
|
+
pos++;
|
|
18808
|
+
return { term: { kind: "number", value: token2.v } };
|
|
18809
|
+
}
|
|
18810
|
+
if (token2.t === "ident" && (token2.v === "true" || token2.v === "false")) {
|
|
18811
|
+
pos++;
|
|
18812
|
+
return { term: { kind: "boolean", value: token2.v === "true" } };
|
|
18813
|
+
}
|
|
18814
|
+
if (token2.t === "ident" && (token2.v === "today" || token2.v === "date") && tokens[pos + 1]?.v === "(") {
|
|
18815
|
+
return { term: dateTerm() };
|
|
18816
|
+
}
|
|
18817
|
+
if (token2.t === "ident") return reference();
|
|
18818
|
+
refuse(`\uD544\uD130 \uC2DD\uC744 \uC77D\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4: ${text}`);
|
|
18819
|
+
};
|
|
18820
|
+
const dateTerm = () => {
|
|
18821
|
+
const start = pos;
|
|
18822
|
+
const fn = tokens[pos].v;
|
|
18823
|
+
pos += 2;
|
|
18824
|
+
let literal;
|
|
18825
|
+
if (fn === "date") {
|
|
18826
|
+
const arg = tokens[pos];
|
|
18827
|
+
if (arg?.t !== "string") refuse(`date() \uC5D0\uB294 \uB0A0\uC9DC \uBB38\uC790\uC5F4\uC744 \uB123\uC5B4\uC57C \uD569\uB2C8\uB2E4: ${text}`);
|
|
18828
|
+
literal = arg.v;
|
|
18829
|
+
pos++;
|
|
18830
|
+
}
|
|
18831
|
+
expect(")");
|
|
18832
|
+
if (fn === "today" && (peek("+") || peek("-")) && tokens[pos + 1]?.t === "string") pos += 2;
|
|
18833
|
+
const raw = tokens.slice(start, pos).map((t) => t.t === "string" ? JSON.stringify(t.v) : String(t.v)).join("");
|
|
18834
|
+
return { kind: "date", text: raw, ...literal !== void 0 ? { literal } : {} };
|
|
18835
|
+
};
|
|
18836
|
+
const reference = () => {
|
|
18837
|
+
const head = tokens[pos++].v;
|
|
18838
|
+
let ref;
|
|
18839
|
+
if ((head === "note" || head === "file") && (peek(".") || peek("["))) {
|
|
18840
|
+
let name;
|
|
18841
|
+
if (take("[")) {
|
|
18842
|
+
const key = tokens[pos++];
|
|
18843
|
+
if (key?.t !== "string") refuse(`${head}[\u2026] \uC5D0\uB294 \uBB38\uC790\uC5F4 \uC774\uB984\uC744 \uB123\uC5B4\uC57C \uD569\uB2C8\uB2E4: ${text}`);
|
|
18844
|
+
name = key.v;
|
|
18845
|
+
expect("]");
|
|
18846
|
+
} else {
|
|
18847
|
+
pos++;
|
|
18848
|
+
const key = tokens[pos++];
|
|
18849
|
+
if (key?.t !== "ident") refuse(`${head}. \uB4A4\uC5D0 \uC774\uB984\uC774 \uC640\uC57C \uD569\uB2C8\uB2E4: ${text}`);
|
|
18850
|
+
name = key.v;
|
|
18851
|
+
}
|
|
18852
|
+
ref = { scope: head, name, dateOnly: false };
|
|
18853
|
+
} else if (head === "formula" || head === "this") {
|
|
18854
|
+
refuse(`${head} \uB294 Notion \uC18D\uC131\uC774 \uC544\uB2C8\uB77C \uC62E\uAE30\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${text}`);
|
|
18855
|
+
} else {
|
|
18856
|
+
ref = { scope: "note", name: head, dateOnly: false };
|
|
18857
|
+
}
|
|
18858
|
+
while (take(".")) {
|
|
18859
|
+
const method = tokens[pos++];
|
|
18860
|
+
if (method?.t !== "ident") refuse(`'.' \uB4A4\uC5D0 \uD568\uC218 \uC774\uB984\uC774 \uC640\uC57C \uD569\uB2C8\uB2E4: ${text}`);
|
|
18861
|
+
expect("(");
|
|
18862
|
+
const args = [];
|
|
18863
|
+
if (!take(")")) {
|
|
18864
|
+
do
|
|
18865
|
+
args.push(operand().term);
|
|
18866
|
+
while (take(","));
|
|
18867
|
+
expect(")");
|
|
18868
|
+
}
|
|
18869
|
+
if (method.v === "date" && args.length === 0 && !ref.dateOnly) {
|
|
18870
|
+
ref = { ...ref, dateOnly: true };
|
|
18871
|
+
continue;
|
|
18872
|
+
}
|
|
18873
|
+
if (peek(".")) refuse(`\uD568\uC218\uB97C \uC774\uC5B4 \uBD80\uB978 \uC2DD\uC740 \uC62E\uAE30\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${text}`);
|
|
18874
|
+
return {
|
|
18875
|
+
term: { kind: "ref", ref },
|
|
18876
|
+
call: { kind: "call", target: { kind: "ref", ref }, name: method.v, args }
|
|
18877
|
+
};
|
|
18878
|
+
}
|
|
18879
|
+
return { term: { kind: "ref", ref } };
|
|
18880
|
+
};
|
|
18881
|
+
const ast = orExpr();
|
|
18882
|
+
if (pos !== tokens.length) refuse(`\uD544\uD130 \uC2DD \uB05D\uC5D0 \uC77D\uC9C0 \uBABB\uD55C \uAC83\uC774 \uC788\uC2B5\uB2C8\uB2E4: ${text}`);
|
|
18883
|
+
return ast;
|
|
18884
|
+
}
|
|
18885
|
+
function compile(ast, schema) {
|
|
18886
|
+
switch (ast.kind) {
|
|
18887
|
+
case "yaml":
|
|
18888
|
+
return toFilter(ast.node, schema);
|
|
18889
|
+
case "and":
|
|
18890
|
+
case "or":
|
|
18891
|
+
return group(
|
|
18892
|
+
ast.kind,
|
|
18893
|
+
ast.items.map((item) => compile(item, schema))
|
|
18894
|
+
);
|
|
18895
|
+
case "not":
|
|
18896
|
+
return negate(compile(ast.item, schema));
|
|
18897
|
+
case "compare":
|
|
18898
|
+
return compare(ast, schema);
|
|
18899
|
+
case "call":
|
|
18900
|
+
return call(ast, schema);
|
|
18901
|
+
}
|
|
18902
|
+
}
|
|
18903
|
+
function group(op, items) {
|
|
18904
|
+
const merged = settle(op === "and" ? mergeRelativeRanges(items) : items);
|
|
18905
|
+
return merged.length === 1 ? merged[0] : { [op]: merged };
|
|
18906
|
+
}
|
|
18907
|
+
function settle(items) {
|
|
18908
|
+
return items.map((item) => {
|
|
18909
|
+
if (!isPendingRange(item)) return item;
|
|
18910
|
+
const { pending, ...leaf2 } = item;
|
|
18911
|
+
if (Object.keys(leaf2).length === 0) {
|
|
18912
|
+
refuse(`\uC0C1\uB300 \uAE30\uAC04 '${pending.text}' \uC740 \uBC18\uB300\uCABD \uB05D(today())\uACFC and \uB85C \uC9DD\uC744 \uC774\uB904\uC57C \uC62E\uAE41\uB2C8\uB2E4`);
|
|
18913
|
+
}
|
|
18914
|
+
return leaf2;
|
|
18915
|
+
});
|
|
17442
18916
|
}
|
|
17443
|
-
|
|
17444
|
-
|
|
17445
|
-
|
|
17446
|
-
|
|
17447
|
-
|
|
17448
|
-
|
|
17449
|
-
|
|
17450
|
-
|
|
17451
|
-
if (
|
|
17452
|
-
|
|
17453
|
-
|
|
18917
|
+
function compare(ast, schema) {
|
|
18918
|
+
if (ast.left.kind !== "ref") refuse("\uBE44\uAD50\uC758 \uC67C\uCABD\uC740 \uC18D\uC131\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4");
|
|
18919
|
+
const target = targetOf(ast.left.ref, schema);
|
|
18920
|
+
const value = ast.right;
|
|
18921
|
+
const leaf2 = (op, v) => ({
|
|
18922
|
+
...target.head,
|
|
18923
|
+
[target.type]: { [op]: v }
|
|
18924
|
+
});
|
|
18925
|
+
if (TEXT_TYPES.has(target.type) || target.type === "select" || target.type === "status") {
|
|
18926
|
+
if (value.kind !== "string") refuse(`${target.label}: \uBB38\uC790\uC5F4\uACFC\uB9CC \uACAC\uC904 \uC218 \uC788\uC2B5\uB2C8\uB2E4`);
|
|
18927
|
+
if (ast.op === "==") return leaf2("equals", value.value);
|
|
18928
|
+
if (ast.op === "!=") return leaf2("does_not_equal", value.value);
|
|
18929
|
+
refuse(`${target.label}: '${ast.op}' \uB85C\uB294 \uACAC\uC8FC\uC9C0 \uBABB\uD569\uB2C8\uB2E4`);
|
|
18930
|
+
}
|
|
18931
|
+
if (target.type === "number") {
|
|
18932
|
+
if (value.kind !== "number") refuse(`${target.label}: \uC22B\uC790\uC640\uB9CC \uACAC\uC904 \uC218 \uC788\uC2B5\uB2C8\uB2E4`);
|
|
18933
|
+
return leaf2(NUMBER_OPS[ast.op], value.value);
|
|
18934
|
+
}
|
|
18935
|
+
if (target.type === "checkbox") {
|
|
18936
|
+
if (value.kind !== "boolean") refuse(`${target.label}: true \xB7 false \uC640\uB9CC \uACAC\uC904 \uC218 \uC788\uC2B5\uB2C8\uB2E4`);
|
|
18937
|
+
if (ast.op === "==") return leaf2("equals", value.value);
|
|
18938
|
+
if (ast.op === "!=") return leaf2("does_not_equal", value.value);
|
|
18939
|
+
refuse(`${target.label}: '${ast.op}' \uB85C\uB294 \uACAC\uC8FC\uC9C0 \uBABB\uD569\uB2C8\uB2E4`);
|
|
18940
|
+
}
|
|
18941
|
+
if (target.type === "date" || target.type === "created_time" || target.type === "last_edited_time") {
|
|
18942
|
+
const op = DATE_OPS[ast.op];
|
|
18943
|
+
if (!op) refuse(`${target.label}: \uB0A0\uC9DC\uB294 '${ast.op}' \uB85C \uACAC\uC8FC\uC9C0 \uBABB\uD569\uB2C8\uB2E4`);
|
|
18944
|
+
return dateLeaf(target, op, ast.op, value);
|
|
18945
|
+
}
|
|
18946
|
+
refuse(`${target.label}: ${target.type} \uC18D\uC131\uC758 \uD544\uD130\uB294 \uC62E\uAE30\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4`);
|
|
18947
|
+
}
|
|
18948
|
+
function dateLeaf(target, op, symbol, value) {
|
|
18949
|
+
const leaf2 = (v) => ({ ...target.head, [target.type]: { [op]: v } });
|
|
18950
|
+
if (value.kind === "string") {
|
|
18951
|
+
if (!/^\d{4}-\d{2}-\d{2}/.test(value.value))
|
|
18952
|
+
refuse(`${target.label}: \uB0A0\uC9DC\uAC00 \uC544\uB2CC \uAC12 '${value.value}'`);
|
|
18953
|
+
return leaf2(value.value);
|
|
18954
|
+
}
|
|
18955
|
+
if (value.kind !== "date") refuse(`${target.label}: \uB0A0\uC9DC\uC640\uB9CC \uACAC\uC904 \uC218 \uC788\uC2B5\uB2C8\uB2E4`);
|
|
18956
|
+
if (value.literal !== void 0) {
|
|
18957
|
+
if (!/^\d{4}-\d{2}-\d{2}/.test(value.literal))
|
|
18958
|
+
refuse(`${target.label}: \uB0A0\uC9DC\uAC00 \uC544\uB2CC \uAC12 '${value.literal}'`);
|
|
18959
|
+
return leaf2(value.literal);
|
|
18960
|
+
}
|
|
18961
|
+
const pending = { target, symbol, text: value.text };
|
|
18962
|
+
const relative2 = RELATIVE_VALUE_OF.get(value.text);
|
|
18963
|
+
return relative2 === void 0 ? { pending } : { ...leaf2(relative2), pending };
|
|
18964
|
+
}
|
|
18965
|
+
function call(ast, schema) {
|
|
18966
|
+
if (ast.target.kind !== "ref") refuse("\uD568\uC218\uB294 \uC18D\uC131\uC5D0 \uBD99\uC5EC\uC57C \uD569\uB2C8\uB2E4");
|
|
18967
|
+
const target = targetOf(ast.target.ref, schema);
|
|
18968
|
+
const leaf2 = (op, v) => ({
|
|
18969
|
+
...target.head,
|
|
18970
|
+
[target.type]: { [op]: v }
|
|
18971
|
+
});
|
|
18972
|
+
const strings = ast.args.map((arg) => {
|
|
18973
|
+
if (arg.kind !== "string") refuse(`${target.label}: ${ast.name}() \uC5D0\uB294 \uBB38\uC790\uC5F4\uC744 \uB123\uC5B4\uC57C \uD569\uB2C8\uB2E4`);
|
|
18974
|
+
return arg.value;
|
|
18975
|
+
});
|
|
18976
|
+
if (ast.name === "isEmpty" && strings.length === 0) {
|
|
18977
|
+
if (!EMPTY_TYPES.has(target.type)) refuse(`${target.label}: \uBE48 \uAC12 \uC870\uAC74\uC740 \uC62E\uAE30\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4`);
|
|
18978
|
+
return leaf2("is_empty", true);
|
|
18979
|
+
}
|
|
18980
|
+
if (TEXT_TYPES.has(target.type) && strings.length === 1) {
|
|
18981
|
+
if (ast.name === "contains") return leaf2("contains", strings[0]);
|
|
18982
|
+
if (ast.name === "startsWith") return leaf2("starts_with", strings[0]);
|
|
18983
|
+
if (ast.name === "endsWith") return leaf2("ends_with", strings[0]);
|
|
18984
|
+
}
|
|
18985
|
+
if (target.type === "multi_select" && strings.length > 0) {
|
|
18986
|
+
if (ast.name === "containsAny" || ast.name === "contains" && strings.length === 1) {
|
|
18987
|
+
const parts = strings.map((s) => leaf2("contains", s));
|
|
18988
|
+
return parts.length === 1 ? parts[0] : { or: parts };
|
|
18989
|
+
}
|
|
18990
|
+
}
|
|
18991
|
+
refuse(`${target.label}: ${ast.name}() \uC740 \uC62E\uAE30\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4`);
|
|
18992
|
+
}
|
|
18993
|
+
function negate(filter) {
|
|
18994
|
+
if (isPendingRange(filter)) refuse("\uC0C1\uB300 \uAE30\uAC04 \uC870\uAC74\uC758 \uBD80\uC815\uC740 \uC62E\uAE30\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4");
|
|
18995
|
+
if (Array.isArray(filter["and"])) return { or: filter["and"].map(negate) };
|
|
18996
|
+
if (Array.isArray(filter["or"])) return { and: filter["or"].map(negate) };
|
|
18997
|
+
const flips = {
|
|
18998
|
+
equals: "does_not_equal",
|
|
18999
|
+
does_not_equal: "equals",
|
|
19000
|
+
contains: "does_not_contain",
|
|
19001
|
+
does_not_contain: "contains",
|
|
19002
|
+
is_empty: "is_not_empty",
|
|
19003
|
+
is_not_empty: "is_empty"
|
|
19004
|
+
};
|
|
19005
|
+
for (const [key, cond] of Object.entries(filter)) {
|
|
19006
|
+
if (key === "property" || key === "timestamp" || !cond || typeof cond !== "object") continue;
|
|
19007
|
+
if (key === "date" || key === "created_time" || key === "last_edited_time" || key === "number") {
|
|
19008
|
+
if (!("is_empty" in cond) && !("is_not_empty" in cond)) {
|
|
19009
|
+
refuse("\uB0A0\uC9DC \xB7 \uC22B\uC790 \uBE44\uAD50\uC758 \uBD80\uC815\uC740 \uC62E\uAE30\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 \uBC18\uB300 \uBE44\uAD50\uB85C \uBC14\uAFD4 \uC4F0\uC138\uC694");
|
|
19010
|
+
}
|
|
19011
|
+
}
|
|
19012
|
+
const [op, value] = Object.entries(cond)[0] ?? [];
|
|
19013
|
+
const flipped = op === void 0 ? void 0 : flips[op];
|
|
19014
|
+
if (!flipped) refuse(`'${String(op)}' \uC870\uAC74\uC758 \uBD80\uC815\uC740 \uC62E\uAE30\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4`);
|
|
19015
|
+
return { ...filter, [key]: { [flipped]: flipped.startsWith("is_") ? true : value } };
|
|
17454
19016
|
}
|
|
17455
|
-
|
|
17456
|
-
|
|
17457
|
-
|
|
17458
|
-
|
|
17459
|
-
|
|
17460
|
-
|
|
17461
|
-
|
|
17462
|
-
|
|
17463
|
-
|
|
19017
|
+
refuse("\uBD80\uC815\uD560 \uC870\uAC74\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4");
|
|
19018
|
+
}
|
|
19019
|
+
function targetOf(ref, schema) {
|
|
19020
|
+
if (ref.scope === "file") {
|
|
19021
|
+
const label = `file.${ref.name}`;
|
|
19022
|
+
const timestamp = notionTimestampOf(label);
|
|
19023
|
+
if (timestamp) return { head: { timestamp }, type: timestamp, label };
|
|
19024
|
+
const title = Object.values(schema).find((p) => p.type === "title");
|
|
19025
|
+
if (label === BASES_TITLE && title) {
|
|
19026
|
+
return { head: { property: decodePropertyId(title.id) }, type: "title", label };
|
|
19027
|
+
}
|
|
19028
|
+
refuse(`${label} \uC740 Notion \uC18D\uC131\uC774 \uC544\uB2C8\uB77C \uC62E\uAE30\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4`);
|
|
17464
19029
|
}
|
|
19030
|
+
const prop = Object.hasOwn(schema, ref.name) ? schema[ref.name] : void 0;
|
|
19031
|
+
if (!prop) refuse(`'${ref.name}' \uC740 \uC774 DB \uC758 \uC18D\uC131\uC774 \uC544\uB2D9\uB2C8\uB2E4`);
|
|
17465
19032
|
return {
|
|
17466
|
-
|
|
17467
|
-
|
|
17468
|
-
|
|
17469
|
-
nextFullAt
|
|
17470
|
-
};
|
|
17471
|
-
}
|
|
17472
|
-
function nextFullReconcileAt(lastFullPullAt, intervalSec) {
|
|
17473
|
-
const last = lastFullPullAt === null ? NaN : Date.parse(lastFullPullAt);
|
|
17474
|
-
return Number.isFinite(last) ? new Date(last + intervalSec * 1e3).toISOString() : null;
|
|
17475
|
-
}
|
|
17476
|
-
function remoteScanInfo(scan, lastFullPullAt, deleteSync, deletionsChecked = false) {
|
|
17477
|
-
return scan.kind === "full" ? {
|
|
17478
|
-
kind: "full",
|
|
17479
|
-
reason: scan.reason,
|
|
17480
|
-
lastFullAt: lastFullPullAt,
|
|
17481
|
-
nextFullAt: null,
|
|
17482
|
-
deletionsDeferred: false
|
|
17483
|
-
} : {
|
|
17484
|
-
kind: "incremental",
|
|
17485
|
-
lastFullAt: scan.lastFullAt,
|
|
17486
|
-
nextFullAt: scan.nextFullAt,
|
|
17487
|
-
deletionsDeferred: deleteSync && !deletionsChecked
|
|
19033
|
+
head: { property: decodePropertyId(prop.id) },
|
|
19034
|
+
type: prop.type,
|
|
19035
|
+
label: `\uC18D\uC131 '${ref.name}'`
|
|
17488
19036
|
};
|
|
17489
19037
|
}
|
|
17490
|
-
|
|
17491
|
-
|
|
17492
|
-
return selection.kind === "all" || selection.ids.has(compactNotionId(databaseId));
|
|
19038
|
+
function isPendingRange(filter) {
|
|
19039
|
+
return typeof filter["pending"] === "object" && filter["pending"] !== null;
|
|
17493
19040
|
}
|
|
17494
|
-
function
|
|
17495
|
-
|
|
17496
|
-
|
|
17497
|
-
const
|
|
17498
|
-
|
|
17499
|
-
|
|
17500
|
-
|
|
17501
|
-
|
|
17502
|
-
|
|
17503
|
-
|
|
19041
|
+
function mergeRelativeRanges(items) {
|
|
19042
|
+
const out = [...items];
|
|
19043
|
+
for (const [range, { toward, span }] of Object.entries(RELATIVE_DATE_RANGES)) {
|
|
19044
|
+
const [lowText, highText] = toward === "past" ? [squash(`today() - "${span}"`), "today()"] : ["today()", squash(`today() + "${span}"`)];
|
|
19045
|
+
for (let i = 0; i < out.length; i++) {
|
|
19046
|
+
const low = out[i];
|
|
19047
|
+
if (!isPendingRange(low) || low.pending.symbol !== ">=" || low.pending.text !== lowText)
|
|
19048
|
+
continue;
|
|
19049
|
+
const j = out.findIndex(
|
|
19050
|
+
(high, k) => k !== i && isPendingRange(high) && high.pending.symbol === "<=" && high.pending.text === highText && JSON.stringify(high.pending.target.head) === JSON.stringify(low.pending.target.head)
|
|
19051
|
+
);
|
|
19052
|
+
if (j < 0) continue;
|
|
19053
|
+
const { target } = low.pending;
|
|
19054
|
+
out[i] = { ...target.head, [target.type]: { [range]: {} } };
|
|
19055
|
+
out.splice(j, 1);
|
|
19056
|
+
}
|
|
17504
19057
|
}
|
|
19058
|
+
return out;
|
|
17505
19059
|
}
|
|
17506
|
-
function
|
|
17507
|
-
return
|
|
19060
|
+
function squash(text) {
|
|
19061
|
+
return text.replace(/\s+/g, "");
|
|
17508
19062
|
}
|
|
17509
|
-
function
|
|
17510
|
-
const
|
|
17511
|
-
|
|
17512
|
-
|
|
17513
|
-
if (pending.size !== before) {
|
|
17514
|
-
stateDb.setMeta(PENDING_DATABASES_META_KEY, serializePendingDatabases(pending));
|
|
17515
|
-
}
|
|
19063
|
+
function depthOf(filter) {
|
|
19064
|
+
const children = filter["and"] ?? filter["or"];
|
|
19065
|
+
if (!Array.isArray(children)) return 0;
|
|
19066
|
+
return 1 + Math.max(0, ...children.map(depthOf));
|
|
17516
19067
|
}
|
|
17517
|
-
|
|
17518
|
-
|
|
17519
|
-
|
|
17520
|
-
|
|
17521
|
-
|
|
17522
|
-
|
|
17523
|
-
|
|
17524
|
-
|
|
17525
|
-
|
|
17526
|
-
|
|
17527
|
-
|
|
17528
|
-
|
|
17529
|
-
|
|
17530
|
-
|
|
17531
|
-
|
|
17532
|
-
|
|
17533
|
-
|
|
17534
|
-
|
|
17535
|
-
this.skippedCount++;
|
|
19068
|
+
|
|
19069
|
+
// src/sync/base-view-push.ts
|
|
19070
|
+
var PROPERTY_REQUEST_KEYS = [
|
|
19071
|
+
"property_id",
|
|
19072
|
+
"visible",
|
|
19073
|
+
"width",
|
|
19074
|
+
"wrap",
|
|
19075
|
+
"status_show_as",
|
|
19076
|
+
"card_property_width_mode",
|
|
19077
|
+
"date_format",
|
|
19078
|
+
"time_format"
|
|
19079
|
+
];
|
|
19080
|
+
var REQUIRED_CONFIGURATION_KEYS = ["group_by", "date_property_id"];
|
|
19081
|
+
var BaseViewEdits = class {
|
|
19082
|
+
constructor(stateDb, vaultFs, notionClient) {
|
|
19083
|
+
this.vaultFs = vaultFs;
|
|
19084
|
+
this.notionClient = notionClient;
|
|
19085
|
+
this.bases = new DbBaseFiles(stateDb);
|
|
17536
19086
|
}
|
|
17537
|
-
|
|
17538
|
-
|
|
17539
|
-
|
|
19087
|
+
vaultFs;
|
|
19088
|
+
notionClient;
|
|
19089
|
+
bases;
|
|
19090
|
+
/** 고친 `.base` 를 찾아 보낼 것을 정한다 — Notion 은 고친 보기만 읽는다. */
|
|
19091
|
+
async plan(inScope = () => true) {
|
|
19092
|
+
const planned = [];
|
|
19093
|
+
const refused = [];
|
|
19094
|
+
const refuse2 = (path, error) => {
|
|
19095
|
+
refused.push({
|
|
19096
|
+
path,
|
|
19097
|
+
operation: "update",
|
|
19098
|
+
error: `.base \uC758 \uBCF4\uAE30\uB97C Notion \uC5D0 \uC62C\uB9AC\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4 \u2014 ${error}`
|
|
19099
|
+
});
|
|
19100
|
+
};
|
|
19101
|
+
for (const [databaseId, base] of this.bases.all()) {
|
|
19102
|
+
if (!base.views || !inScope(base.basePath)) continue;
|
|
19103
|
+
const content = await this.vaultFs.readFile(base.basePath).catch(() => null);
|
|
19104
|
+
if (content === null) continue;
|
|
19105
|
+
const hash = computeHash(content);
|
|
19106
|
+
if (hash === base.hash) continue;
|
|
19107
|
+
const parsed = readBaseFile(content);
|
|
19108
|
+
if (!parsed) {
|
|
19109
|
+
refuse2(base.basePath, "YAML \uB85C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
19110
|
+
continue;
|
|
19111
|
+
}
|
|
19112
|
+
const matched = matchViews(base.views, parsed.views);
|
|
19113
|
+
const edits = parsed.views.flatMap((view, i) => {
|
|
19114
|
+
const record = matched[i];
|
|
19115
|
+
if (!record) return [];
|
|
19116
|
+
const edited = pushableOf(view);
|
|
19117
|
+
const changed = changedFields(record, edited);
|
|
19118
|
+
return changed.length > 0 ? [{ record, edited, changed }] : [];
|
|
19119
|
+
});
|
|
19120
|
+
if (edits.length === 0) continue;
|
|
19121
|
+
const meta = await this.notionClient.getDatabaseMeta(databaseId);
|
|
19122
|
+
if (meta.dataSources.length > 1) {
|
|
19123
|
+
refuse2(base.basePath, "\uB370\uC774\uD130 \uC18C\uC2A4\uAC00 \uC5EC\uB7FF\uC778 DB \uC758 \uBCF4\uAE30\uB294 Notion \uC5D0\uC11C \uBC14\uAFB8\uC138\uC694");
|
|
19124
|
+
continue;
|
|
19125
|
+
}
|
|
19126
|
+
const schema = await this.notionClient.getDatabaseSchemaFull(databaseId, meta);
|
|
19127
|
+
const views = [];
|
|
19128
|
+
let allPlanned = true;
|
|
19129
|
+
for (const { record, edited, changed } of edits) {
|
|
19130
|
+
const label = `\uBCF4\uAE30 '${record.name}'`;
|
|
19131
|
+
let live;
|
|
19132
|
+
try {
|
|
19133
|
+
live = await this.notionClient.getView(record.viewId);
|
|
19134
|
+
} catch (error) {
|
|
19135
|
+
if (!isNotionObjectNotFound(error)) throw error;
|
|
19136
|
+
live = null;
|
|
19137
|
+
}
|
|
19138
|
+
if (!live) {
|
|
19139
|
+
refuse2(base.basePath, `${label}: Notion \uC5D0\uC11C \uC9C0\uC6B4 \uBCF4\uAE30\uC785\uB2C8\uB2E4`);
|
|
19140
|
+
allPlanned = false;
|
|
19141
|
+
continue;
|
|
19142
|
+
}
|
|
19143
|
+
if (viewFingerprint(live) !== record.notion) {
|
|
19144
|
+
refuse2(
|
|
19145
|
+
base.basePath,
|
|
19146
|
+
`${label}: \uADF8 \uC0AC\uC774 Notion \uC5D0\uC11C\uB3C4 \uC774 \uBCF4\uAE30\uB97C \uACE0\uCCE4\uC2B5\uB2C8\uB2E4. \uACE0\uCE5C \uB0B4\uC6A9\uC744 \uC62E\uACA8 \uC801\uACE0 \uC774 \uD30C\uC77C\uC744 \uC9C0\uC6B4 \uB4A4 pull \uD558\uBA74 \uC0C8\uB85C \uBC1B\uC2B5\uB2C8\uB2E4`
|
|
19147
|
+
);
|
|
19148
|
+
allPlanned = false;
|
|
19149
|
+
continue;
|
|
19150
|
+
}
|
|
19151
|
+
const built = buildRequest(record, edited, changed, live, schema);
|
|
19152
|
+
if (!built.ok) {
|
|
19153
|
+
refuse2(base.basePath, `${label}: ${built.error}`);
|
|
19154
|
+
allPlanned = false;
|
|
19155
|
+
continue;
|
|
19156
|
+
}
|
|
19157
|
+
views.push({
|
|
19158
|
+
viewId: record.viewId,
|
|
19159
|
+
request: built.request,
|
|
19160
|
+
edit: {
|
|
19161
|
+
path: base.basePath,
|
|
19162
|
+
view: edited.name,
|
|
19163
|
+
...edited.name !== record.name ? { from: record.name } : {},
|
|
19164
|
+
changed
|
|
19165
|
+
},
|
|
19166
|
+
record: {
|
|
19167
|
+
viewId: record.viewId,
|
|
19168
|
+
type: record.type,
|
|
19169
|
+
...edited,
|
|
19170
|
+
...record.lossyFilter && !changed.includes("filter") ? { lossyFilter: true } : {},
|
|
19171
|
+
notion: record.notion
|
|
19172
|
+
}
|
|
19173
|
+
});
|
|
19174
|
+
}
|
|
19175
|
+
if (views.length === 0) continue;
|
|
19176
|
+
planned.push({
|
|
19177
|
+
databaseId,
|
|
19178
|
+
path: base.basePath,
|
|
19179
|
+
base,
|
|
19180
|
+
hash,
|
|
19181
|
+
complete: allPlanned && matched.filter(Boolean).length === base.views.length && restSignature(parsed, matched) === base.rest,
|
|
19182
|
+
views
|
|
19183
|
+
});
|
|
19184
|
+
}
|
|
19185
|
+
return { planned, refused };
|
|
17540
19186
|
}
|
|
17541
|
-
|
|
17542
|
-
|
|
17543
|
-
|
|
19187
|
+
async apply(planned) {
|
|
19188
|
+
const applied = [];
|
|
19189
|
+
const failed = [];
|
|
19190
|
+
for (const file of planned) {
|
|
19191
|
+
const records = new Map(file.base.views.map((record) => [record.viewId, record]));
|
|
19192
|
+
let allApplied = true;
|
|
19193
|
+
for (const view of file.views) {
|
|
19194
|
+
try {
|
|
19195
|
+
await this.notionClient.updateView(view.viewId, view.request);
|
|
19196
|
+
} catch (error) {
|
|
19197
|
+
allApplied = false;
|
|
19198
|
+
failed.push({
|
|
19199
|
+
path: file.path,
|
|
19200
|
+
operation: "update",
|
|
19201
|
+
error: `\uBCF4\uAE30 '${view.edit.view}' \uB97C Notion \uC5D0 \uC62C\uB9AC\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4: ${error instanceof Error ? error.message : String(error)}`
|
|
19202
|
+
});
|
|
19203
|
+
continue;
|
|
19204
|
+
}
|
|
19205
|
+
applied.push(view.edit);
|
|
19206
|
+
const after = await this.notionClient.getView(view.viewId).catch(() => null);
|
|
19207
|
+
records.set(view.viewId, {
|
|
19208
|
+
...view.record,
|
|
19209
|
+
notion: after ? viewFingerprint(after) : view.record.notion
|
|
19210
|
+
});
|
|
19211
|
+
}
|
|
19212
|
+
this.bases.set(file.databaseId, {
|
|
19213
|
+
...file.base,
|
|
19214
|
+
views: file.base.views.map((record) => records.get(record.viewId) ?? record),
|
|
19215
|
+
// 고친 것을 모두 올렸으면 이 파일은 이제 pull 이 쓴 것과 같은 뜻이다 — 다음 pull 이 Notion 의 모습으로 다시 쓴다.
|
|
19216
|
+
// 남은 것이 있으면 계속 붙잡아 둔다.
|
|
19217
|
+
...file.complete && allApplied ? { hash: file.hash } : {}
|
|
19218
|
+
});
|
|
19219
|
+
}
|
|
19220
|
+
return { applied, failed };
|
|
17544
19221
|
}
|
|
17545
|
-
|
|
17546
|
-
|
|
19222
|
+
};
|
|
19223
|
+
function buildRequest(record, edited, changed, live, schema) {
|
|
19224
|
+
const request = {};
|
|
19225
|
+
try {
|
|
19226
|
+
if (changed.includes("name")) {
|
|
19227
|
+
if (!edited.name.trim()) throw new Error("\uBCF4\uAE30 \uC774\uB984\uC774 \uBE44\uC5C8\uC2B5\uB2C8\uB2E4");
|
|
19228
|
+
request.name = edited.name;
|
|
19229
|
+
}
|
|
19230
|
+
if (changed.includes("columns")) request.configuration = columnsOf(edited, live, schema);
|
|
19231
|
+
if (changed.includes("sort")) {
|
|
19232
|
+
request.sorts = edited.sort ? edited.sort.map((s) => ({
|
|
19233
|
+
property: propertyIdOf(s.property, schema, "\uC815\uB82C"),
|
|
19234
|
+
direction: s.direction === "DESC" ? "descending" : "ascending"
|
|
19235
|
+
})) : null;
|
|
19236
|
+
}
|
|
19237
|
+
if (changed.includes("filter")) {
|
|
19238
|
+
if (record.lossyFilter) {
|
|
19239
|
+
throw new Error(
|
|
19240
|
+
"Notion \uD544\uD130\uC5D0 \uBCFC\uD2B8\uB85C \uC62E\uAE30\uC9C0 \uBABB\uD55C \uC870\uAC74\uC774 \uC788\uC5B4 \uBCFC\uD2B8\uC758 \uD544\uD130\uB85C \uB36E\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uD544\uD130\uB294 Notion \uC5D0\uC11C \uACE0\uCE58\uC138\uC694"
|
|
19241
|
+
);
|
|
19242
|
+
}
|
|
19243
|
+
if (edited.filters === void 0) {
|
|
19244
|
+
request.filter = null;
|
|
19245
|
+
} else {
|
|
19246
|
+
const converted = basesFilterToNotion(edited.filters, schema);
|
|
19247
|
+
if (!converted.ok) throw new Error(`\uD544\uD130 \u2014 ${converted.error}`);
|
|
19248
|
+
request.filter = converted.filter;
|
|
19249
|
+
}
|
|
19250
|
+
}
|
|
19251
|
+
} catch (error) {
|
|
19252
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
17547
19253
|
}
|
|
17548
|
-
|
|
17549
|
-
|
|
17550
|
-
|
|
17551
|
-
|
|
17552
|
-
|
|
17553
|
-
|
|
19254
|
+
return { ok: true, request };
|
|
19255
|
+
}
|
|
19256
|
+
function columnsOf(edited, live, schema) {
|
|
19257
|
+
if (!edited.order) throw new Error("\uC5F4 \uBAA9\uB85D(order)\uC744 \uC9C0\uC6B4 \uBCF4\uAE30\uB294 \uC62C\uB9AC\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4");
|
|
19258
|
+
const configuration = live.configuration ?? {};
|
|
19259
|
+
const liveEntries = Array.isArray(configuration["properties"]) ? configuration["properties"] : [];
|
|
19260
|
+
const entryOf = (id) => {
|
|
19261
|
+
const found2 = liveEntries.find(
|
|
19262
|
+
(entry) => decodePropertyId(String(entry["property_id"])) === id
|
|
19263
|
+
);
|
|
19264
|
+
const out = { property_id: id };
|
|
19265
|
+
for (const key of PROPERTY_REQUEST_KEYS) {
|
|
19266
|
+
if (found2 && key !== "property_id" && found2[key] !== void 0 && found2[key] !== null) {
|
|
19267
|
+
out[key] = found2[key];
|
|
19268
|
+
}
|
|
19269
|
+
}
|
|
19270
|
+
return out;
|
|
19271
|
+
};
|
|
19272
|
+
const title = Object.values(schema).find((prop) => prop.type === "title");
|
|
19273
|
+
const shown = [];
|
|
19274
|
+
for (const column of edited.order) {
|
|
19275
|
+
if (column === BASES_TITLE) continue;
|
|
19276
|
+
if (isRangeEnd(column, schema)) continue;
|
|
19277
|
+
const id = propertyIdOf(column, schema, "\uC5F4");
|
|
19278
|
+
if (title && id === decodePropertyId(title.id)) continue;
|
|
19279
|
+
if (!shown.includes(id)) shown.push(id);
|
|
19280
|
+
}
|
|
19281
|
+
const titleId = title ? decodePropertyId(title.id) : void 0;
|
|
19282
|
+
const properties = [
|
|
19283
|
+
...titleId ? [entryOf(titleId)] : [],
|
|
19284
|
+
...shown.map((id) => ({ ...entryOf(id), visible: true })),
|
|
19285
|
+
...liveEntries.map((entry) => decodePropertyId(String(entry["property_id"]))).filter((id) => id !== titleId && !shown.includes(id)).map((id) => ({ ...entryOf(id), visible: false }))
|
|
19286
|
+
];
|
|
19287
|
+
return {
|
|
19288
|
+
type: configuration["type"] ?? live.type,
|
|
19289
|
+
...Object.fromEntries(
|
|
19290
|
+
REQUIRED_CONFIGURATION_KEYS.filter((key) => configuration[key] !== void 0).map((key) => [
|
|
19291
|
+
key,
|
|
19292
|
+
configuration[key]
|
|
19293
|
+
])
|
|
19294
|
+
),
|
|
19295
|
+
properties
|
|
19296
|
+
};
|
|
19297
|
+
}
|
|
19298
|
+
function propertyIdOf(column, schema, where) {
|
|
19299
|
+
if (column === BASES_TITLE) {
|
|
19300
|
+
const title = Object.values(schema).find((prop2) => prop2.type === "title");
|
|
19301
|
+
if (title) return decodePropertyId(title.id);
|
|
17554
19302
|
}
|
|
17555
|
-
|
|
17556
|
-
|
|
17557
|
-
|
|
17558
|
-
|
|
17559
|
-
const syncing = this.syncing;
|
|
17560
|
-
return syncing ? new Set([...next].filter((id) => syncing.has(id))) : next;
|
|
19303
|
+
if (notionTimestampOf(column)) {
|
|
19304
|
+
throw new Error(
|
|
19305
|
+
`${where} '${column}' \u2014 Notion \uBCF4\uAE30\uB294 \uB9CC\uB4E0 \xB7 \uACE0\uCE5C \uC2DC\uAC01 \uC815\uB82C\uC744 API \uB85C \uBC1B\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. Notion \uC5D0\uC11C \uBC14\uAFB8\uC138\uC694`
|
|
19306
|
+
);
|
|
17561
19307
|
}
|
|
17562
|
-
|
|
19308
|
+
const prop = Object.hasOwn(schema, column) ? schema[column] : void 0;
|
|
19309
|
+
if (!prop) throw new Error(`${where} '${column}' \uC740 \uC774 DB \uC758 Notion \uC18D\uC131\uC774 \uC544\uB2D9\uB2C8\uB2E4`);
|
|
19310
|
+
return decodePropertyId(prop.id);
|
|
19311
|
+
}
|
|
19312
|
+
function isRangeEnd(column, schema) {
|
|
19313
|
+
if (!column.endsWith(RANGE_END_SUFFIX) || Object.hasOwn(schema, column)) return false;
|
|
19314
|
+
const start = column.slice(0, -RANGE_END_SUFFIX.length);
|
|
19315
|
+
return Object.hasOwn(schema, start) && schema[start].type === "date";
|
|
19316
|
+
}
|
|
17563
19317
|
|
|
17564
19318
|
// src/sync/row-schema-cache.ts
|
|
17565
19319
|
var RowSchemaCache = class {
|
|
17566
|
-
constructor(loadSchema, wikilinkResolver, createMapper = () => new PropertyMapper(), loadPeople) {
|
|
19320
|
+
constructor(loadSchema, wikilinkResolver, createMapper = () => new PropertyMapper(), loadPeople, loadRowSource = async () => null) {
|
|
17567
19321
|
this.loadSchema = loadSchema;
|
|
17568
19322
|
this.wikilinkResolver = wikilinkResolver;
|
|
17569
19323
|
this.createMapper = createMapper;
|
|
17570
19324
|
this.loadPeople = loadPeople;
|
|
19325
|
+
this.loadRowSource = loadRowSource;
|
|
17571
19326
|
}
|
|
17572
19327
|
loadSchema;
|
|
17573
19328
|
wikilinkResolver;
|
|
17574
19329
|
createMapper;
|
|
17575
19330
|
loadPeople;
|
|
19331
|
+
loadRowSource;
|
|
17576
19332
|
mappers = /* @__PURE__ */ new Map();
|
|
19333
|
+
rowSources = /* @__PURE__ */ new Map();
|
|
17577
19334
|
people = null;
|
|
17578
|
-
|
|
17579
|
-
|
|
19335
|
+
/** @param dataSourceId 행이 사는 data source — 주면 그 소스의 스키마로 옮긴다. */
|
|
19336
|
+
mapperFor(databaseId, dataSourceId) {
|
|
19337
|
+
const key = `${compactNotionId(databaseId)}:${dataSourceId ? compactNotionId(dataSourceId) : ""}`;
|
|
17580
19338
|
const cached = this.mappers.get(key);
|
|
17581
19339
|
if (cached) return cached;
|
|
17582
|
-
const created = this.loadSchema(databaseId).then(async (schema) => {
|
|
19340
|
+
const created = this.loadSchema(databaseId, dataSourceId ?? void 0).then(async (schema) => {
|
|
17583
19341
|
const mapper = this.createMapper();
|
|
17584
19342
|
mapper.setWikilinkResolver(this.wikilinkResolver);
|
|
17585
19343
|
mapper.loadSchema(schema);
|
|
@@ -17592,8 +19350,24 @@ var RowSchemaCache = class {
|
|
|
17592
19350
|
});
|
|
17593
19351
|
return created;
|
|
17594
19352
|
}
|
|
19353
|
+
/**
|
|
19354
|
+
* 새 행을 둘 data source — 데이터 소스가 여럿인 DB 는 첫 소스. 그런 DB 에 DB 를 부모로 행을 만들면 Notion 이 400 으로
|
|
19355
|
+
* 거절한다(2026-10-29 실측: «Databases with multiple data sources are not supported in this API version»).
|
|
19356
|
+
*/
|
|
19357
|
+
rowSourceFor(databaseId) {
|
|
19358
|
+
const key = compactNotionId(databaseId);
|
|
19359
|
+
const cached = this.rowSources.get(key);
|
|
19360
|
+
if (cached) return cached;
|
|
19361
|
+
const loaded = this.loadRowSource(databaseId);
|
|
19362
|
+
this.rowSources.set(key, loaded);
|
|
19363
|
+
loaded.catch(() => {
|
|
19364
|
+
if (this.rowSources.get(key) === loaded) this.rowSources.delete(key);
|
|
19365
|
+
});
|
|
19366
|
+
return loaded;
|
|
19367
|
+
}
|
|
17595
19368
|
clear() {
|
|
17596
19369
|
this.mappers.clear();
|
|
19370
|
+
this.rowSources.clear();
|
|
17597
19371
|
this.people = null;
|
|
17598
19372
|
}
|
|
17599
19373
|
/** 사용자 목록을 못 읽으면 이름은 풀지 않는다 — id 로 적힌 사람만 보낸다(예전 동작). */
|
|
@@ -18141,7 +19915,7 @@ var DatabaseDiscovery = class {
|
|
|
18141
19915
|
failed.push({
|
|
18142
19916
|
path: pathOf(parentPageId),
|
|
18143
19917
|
operation: "create",
|
|
18144
|
-
error: `DB ${
|
|
19918
|
+
error: `DB ${shortNotionId(dbId)} \uB97C \uC77D\uC9C0 \uBABB\uD568 \u2014 \uB2E4\uC74C pull \uC774 \uB2E4\uC2DC \uBCF8\uB2E4: ${error}`
|
|
18145
19919
|
});
|
|
18146
19920
|
};
|
|
18147
19921
|
const register = async (dbId, parentPageId) => {
|
|
@@ -18191,10 +19965,11 @@ var DatabaseDiscovery = class {
|
|
|
18191
19965
|
nextRetry.parents.push(...retry.parents);
|
|
18192
19966
|
nextRetry.dbs.push(...retry.dbs);
|
|
18193
19967
|
} else {
|
|
19968
|
+
const root = this.config.notion.rootPageId;
|
|
18194
19969
|
const scanIds = scanAll ? [
|
|
18195
|
-
|
|
19970
|
+
root,
|
|
18196
19971
|
...this.stateDb.getAll().filter((r) => r.notionPageId).map((r) => r.notionPageId)
|
|
18197
|
-
].filter((id) => !this._bodyReadPages.has(normalizeNotionId(id))) : retry.parents;
|
|
19972
|
+
].filter((id) => !this._bodyReadPages.has(normalizeNotionId(id))) : options.rescanRoot && !retry.parents.some((id) => notionIdsEqual(id, root)) ? [root, ...retry.parents] : retry.parents;
|
|
18198
19973
|
if (scanIds.length > 0) {
|
|
18199
19974
|
const { found: found2, unread } = await this.discoverChildDatabases(scanIds);
|
|
18200
19975
|
for (const { dbId, parentPageId } of found2) {
|
|
@@ -19456,7 +21231,7 @@ function rewriteTarget(target, source, context, kind) {
|
|
|
19456
21231
|
const shown = keepExtension ? destination : destination.slice(0, -".md".length);
|
|
19457
21232
|
let next;
|
|
19458
21233
|
if (resolved.relative) {
|
|
19459
|
-
next = relativePath(
|
|
21234
|
+
next = relativePath(folderOf5(source.to), shown);
|
|
19460
21235
|
if (!linkpath.startsWith("./") && next.startsWith("./")) next = next.slice(2);
|
|
19461
21236
|
} else if (style === "name") {
|
|
19462
21237
|
const name = baseName3(shown);
|
|
@@ -19478,14 +21253,14 @@ function resolveLinkpath(linkpath, sourcePath, files) {
|
|
|
19478
21253
|
const matches = byName(files, baseName3(candidate)).filter((file) => file.endsWith(suffix));
|
|
19479
21254
|
if (matches.length === 1) return found(matches[0], linkpath);
|
|
19480
21255
|
if (matches.length > 1) {
|
|
19481
|
-
const local = `${folderPrefix(
|
|
21256
|
+
const local = `${folderPrefix(folderOf5(sourcePath))}${candidate}`;
|
|
19482
21257
|
return matches.includes(local) ? found(local, linkpath) : null;
|
|
19483
21258
|
}
|
|
19484
21259
|
}
|
|
19485
21260
|
return null;
|
|
19486
21261
|
}
|
|
19487
21262
|
function resolveRelative(linkpath, sourcePath, files) {
|
|
19488
|
-
const joined = normalizeSegments(`${folderPrefix(
|
|
21263
|
+
const joined = normalizeSegments(`${folderPrefix(folderOf5(sourcePath))}${linkpath}`);
|
|
19489
21264
|
if (joined === null) return null;
|
|
19490
21265
|
for (const candidate of withExtension(joined)) {
|
|
19491
21266
|
if (files.has(candidate)) return { ...found(candidate, linkpath), relative: true };
|
|
@@ -19544,7 +21319,7 @@ function relativePath(from, to) {
|
|
|
19544
21319
|
function encodeMarkdownTarget(next, original) {
|
|
19545
21320
|
return original.includes("%") || next.includes(" ") ? next.replace(/ /g, "%20") : next;
|
|
19546
21321
|
}
|
|
19547
|
-
function
|
|
21322
|
+
function folderOf5(path) {
|
|
19548
21323
|
const slash = path.lastIndexOf("/");
|
|
19549
21324
|
return slash === -1 ? "" : path.slice(0, slash);
|
|
19550
21325
|
}
|
|
@@ -20495,15 +22270,15 @@ var DatabaseCreator = class {
|
|
|
20495
22270
|
*/
|
|
20496
22271
|
async addStatus(dataSourceId, status, folder) {
|
|
20497
22272
|
const groupOf = /* @__PURE__ */ new Map();
|
|
20498
|
-
for (const
|
|
20499
|
-
for (const option of
|
|
22273
|
+
for (const group2 of status.groups ?? []) {
|
|
22274
|
+
for (const option of group2.options) groupOf.set(option, group2.name);
|
|
20500
22275
|
}
|
|
20501
22276
|
const unknown = [];
|
|
20502
22277
|
const options = status.options.map((option) => {
|
|
20503
|
-
const
|
|
20504
|
-
if (
|
|
20505
|
-
if (NOTION_STATUS_GROUPS.includes(
|
|
20506
|
-
unknown.push(`${option.name}(${
|
|
22278
|
+
const group2 = groupOf.get(option.name);
|
|
22279
|
+
if (group2 === void 0) return option;
|
|
22280
|
+
if (NOTION_STATUS_GROUPS.includes(group2)) return { ...option, group: group2 };
|
|
22281
|
+
unknown.push(`${option.name}(${group2})`);
|
|
20507
22282
|
return option;
|
|
20508
22283
|
});
|
|
20509
22284
|
await this.notionClient.updateDataSourceProperties(dataSourceId, {
|
|
@@ -21573,7 +23348,8 @@ var PagePusher = class {
|
|
|
21573
23348
|
);
|
|
21574
23349
|
}
|
|
21575
23350
|
const title = noteTitle(current.data, path);
|
|
21576
|
-
const
|
|
23351
|
+
const source = await this.rowSchemas.rowSourceFor(databaseId);
|
|
23352
|
+
const mapper = await this.rowSchemas.mapperFor(databaseId, source);
|
|
21577
23353
|
const changes = diffRowProperties(null, current.data);
|
|
21578
23354
|
const { properties, skipped } = mapper.toNotionPropertyChanges(changes, title, {
|
|
21579
23355
|
current: current.data,
|
|
@@ -21629,7 +23405,8 @@ var PagePusher = class {
|
|
|
21629
23405
|
title,
|
|
21630
23406
|
conversionResult.content,
|
|
21631
23407
|
properties,
|
|
21632
|
-
{ path, frontmatter: current.data }
|
|
23408
|
+
{ path, frontmatter: current.data },
|
|
23409
|
+
source
|
|
21633
23410
|
);
|
|
21634
23411
|
const { page } = created;
|
|
21635
23412
|
const mapping = this.stateDb.upsert({
|
|
@@ -21761,8 +23538,8 @@ var PagePusher = class {
|
|
|
21761
23538
|
`frontmatter \uB97C \uC77D\uC9C0 \uBABB\uD574 \uD589\uC744 \uBCF4\uB0B4\uC9C0 \uC54A\uC74C (${path}): ${error instanceof Error ? error.message : String(error)}`
|
|
21762
23539
|
);
|
|
21763
23540
|
}
|
|
21764
|
-
const mapper = await this.rowSchemas.mapperFor(databaseId);
|
|
21765
23541
|
const remote = await this.livePage(record);
|
|
23542
|
+
const mapper = await this.rowSchemas.mapperFor(databaseId, dataSourceIdOf(remote));
|
|
21766
23543
|
const drift = options.overwriteRemote ? "none" : await this.drift.remoteDrift(record, remote);
|
|
21767
23544
|
const against = options.overwriteRemote ? this.remoteRowState(mapper, remote) : this.baseRowState(record);
|
|
21768
23545
|
const title = noteTitle(current.data, path);
|
|
@@ -21937,7 +23714,8 @@ var PagePusher = class {
|
|
|
21937
23714
|
* 버린 맨 앞 `# H1` 을 호출측이 매핑을 적은 뒤 되살린다({@link restoreCreatedHeading}). 블록으로
|
|
21938
23715
|
* 보냈으면 null 이다 — 블록은 보낸 그대로 생긴다.
|
|
21939
23716
|
*/
|
|
21940
|
-
async pushCreatePage(parentId, parentType, title, markdownContent, properties, note) {
|
|
23717
|
+
async pushCreatePage(parentId, parentType, title, markdownContent, properties, note, dataSourceId) {
|
|
23718
|
+
const parentSource = dataSourceId ? { dataSourceId } : {};
|
|
21941
23719
|
const decorate2 = async (page2) => await this.decorations.push(page2.id, note.path, note.frontmatter, {}) ?? page2;
|
|
21942
23720
|
if (this.config.conversion.preferMarkdownApi !== false) {
|
|
21943
23721
|
const deferred = deferSyncedReferences(obsidianToNotionEnhanced(markdownContent));
|
|
@@ -21952,7 +23730,8 @@ var PagePusher = class {
|
|
|
21952
23730
|
parentType,
|
|
21953
23731
|
title,
|
|
21954
23732
|
markdown,
|
|
21955
|
-
properties
|
|
23733
|
+
properties,
|
|
23734
|
+
...parentSource
|
|
21956
23735
|
})
|
|
21957
23736
|
);
|
|
21958
23737
|
return { page: page2, markdown, syncedRefs: deferred.refs, syncedRefText: deferred.removed };
|
|
@@ -21963,7 +23742,8 @@ var PagePusher = class {
|
|
|
21963
23742
|
parentId,
|
|
21964
23743
|
parentType,
|
|
21965
23744
|
title,
|
|
21966
|
-
properties
|
|
23745
|
+
properties,
|
|
23746
|
+
...parentSource
|
|
21967
23747
|
})
|
|
21968
23748
|
);
|
|
21969
23749
|
if (blocks.length > 0) {
|
|
@@ -23246,11 +25026,21 @@ var RemoteDetector = class _RemoteDetector {
|
|
|
23246
25026
|
_moved = /* @__PURE__ */ new Map();
|
|
23247
25027
|
// 증분 감지가 search 로 살아 있다고 본 페이지(압축 id) — 삭제 확인이 다시 묻지 않는다.
|
|
23248
25028
|
_seenAlive = /* @__PURE__ */ new Set();
|
|
25029
|
+
// 이번 감지에서 루트의 자식이 바뀌었을 수 있나 — {@link rootChanged}.
|
|
25030
|
+
_rootChanged = false;
|
|
23249
25031
|
// 이번 감지가 삭제를 물은 뒤의 커서 — pull 이 마칠 때 적는다({@link saveDeletionSweep}). undefined 면 묻지 않았다.
|
|
23250
25032
|
_sweepCursor;
|
|
23251
25033
|
// 이번 감지가 추적 중인 것을 모두 물었나 — 그러면 증분이어도 미룬 삭제가 없다.
|
|
23252
25034
|
_sweptAll = false;
|
|
23253
25035
|
/** 이번 감지에서 자식 페이지를 가진 것으로 본 페이지인가 — 폴더노트 판정의 1차 근거. */
|
|
25036
|
+
/**
|
|
25037
|
+
* 루트 바로 아래에 새 DB 가 생겼을 수 있나 — 증분 search 에 루트가 들었거나 전체 대조다. 루트는 받는 노트가 아니라
|
|
25038
|
+
* 본문을 읽지 않아, 루트 바로 아래 새 DB 는 `pull --force` 만 찾았다(2026-10-29 실측 — DB 를 만들면 루트의 수정
|
|
25039
|
+
* 시각이 바뀐다). 이때 DB 발견이 루트의 자식을 한 번 훑는다.
|
|
25040
|
+
*/
|
|
25041
|
+
rootChanged() {
|
|
25042
|
+
return this._rootChanged;
|
|
25043
|
+
}
|
|
23254
25044
|
hasChildPages(pageId) {
|
|
23255
25045
|
return this._childParentIds.has(normalizeNotionId(pageId));
|
|
23256
25046
|
}
|
|
@@ -23355,6 +25145,7 @@ var RemoteDetector = class _RemoteDetector {
|
|
|
23355
25145
|
*/
|
|
23356
25146
|
async detectRemote(scan, options) {
|
|
23357
25147
|
this._seenAlive.clear();
|
|
25148
|
+
this._rootChanged = scan.kind === "full";
|
|
23358
25149
|
this._sweepCursor = void 0;
|
|
23359
25150
|
this._sweptAll = false;
|
|
23360
25151
|
const detection = scan.kind === "full" ? { changes: await this.detectRemoteChanges(options.signal), databaseIds: null } : await this.detectRemoteChangesIncremental(scan.since, options);
|
|
@@ -23597,7 +25388,10 @@ var RemoteDetector = class _RemoteDetector {
|
|
|
23597
25388
|
databaseIds.add(compactNotionId(page.parentDatabaseId));
|
|
23598
25389
|
continue;
|
|
23599
25390
|
}
|
|
23600
|
-
if (notionIdsEqual(page.id, this.config.notion.rootPageId))
|
|
25391
|
+
if (notionIdsEqual(page.id, this.config.notion.rootPageId)) {
|
|
25392
|
+
this._rootChanged = true;
|
|
25393
|
+
continue;
|
|
25394
|
+
}
|
|
23601
25395
|
throwIfAborted(options.signal);
|
|
23602
25396
|
try {
|
|
23603
25397
|
const parentId = await extractParentId(this.notionClient, page);
|
|
@@ -23850,7 +25644,7 @@ function draftFolderDatabase(input) {
|
|
|
23850
25644
|
let index = 0;
|
|
23851
25645
|
for (const [name, list] of values) {
|
|
23852
25646
|
if (NON_PROPERTY_KEYS.has(name)) continue;
|
|
23853
|
-
if (
|
|
25647
|
+
if (isRangeEnd2(name, values)) continue;
|
|
23854
25648
|
if (name === DEFAULT_TITLE_PROPERTY) {
|
|
23855
25649
|
skipped.push({ what: name, reason: "\uC81C\uBAA9 \uC18D\uC131\uACFC \uC774\uB984\uC774 \uAC19\uB2E4 \u2014 \uC774\uB984\uC744 \uBC14\uAFB8\uBA74 \uC18D\uC131\uC774 \uB41C\uB2E4" });
|
|
23856
25650
|
continue;
|
|
@@ -23967,7 +25761,7 @@ function collectValues(notes) {
|
|
|
23967
25761
|
}
|
|
23968
25762
|
return out;
|
|
23969
25763
|
}
|
|
23970
|
-
function
|
|
25764
|
+
function isRangeEnd2(name, values) {
|
|
23971
25765
|
if (!name.endsWith(RANGE_END_SUFFIX)) return false;
|
|
23972
25766
|
const start = values.get(name.slice(0, -RANGE_END_SUFFIX.length));
|
|
23973
25767
|
return start !== void 0 && start.length > 0 && start.every(isFrontmatterDate);
|
|
@@ -24026,7 +25820,7 @@ ${text}
|
|
|
24026
25820
|
return [];
|
|
24027
25821
|
}
|
|
24028
25822
|
const idOf2 = /* @__PURE__ */ new Map();
|
|
24029
|
-
for (const p of properties) idOf2.set(p.type === "title" ?
|
|
25823
|
+
for (const p of properties) idOf2.set(p.type === "title" ? BASES_TITLE : p.name, p.id);
|
|
24030
25824
|
const columnId = (raw) => {
|
|
24031
25825
|
if (typeof raw !== "string") return void 0;
|
|
24032
25826
|
return idOf2.get(raw) ?? idOf2.get(raw.replace(/^note\./, ""));
|
|
@@ -24152,11 +25946,26 @@ var SyncOrchestrator = class {
|
|
|
24152
25946
|
propertyMapper.setWikilinkResolver(wikilinkResolver);
|
|
24153
25947
|
this.notionClient.setWikilinkResolver(wikilinkResolver);
|
|
24154
25948
|
this.rowSchemas = new RowSchemaCache(
|
|
24155
|
-
(databaseId) =>
|
|
25949
|
+
async (databaseId, dataSourceId) => {
|
|
25950
|
+
if (!dataSourceId) return notionClient.getDatabaseSchema(databaseId);
|
|
25951
|
+
const meta = await notionClient.getDatabaseMeta(databaseId);
|
|
25952
|
+
const own = meta.dataSources.length > 1 ? meta.dataSources.find((ds) => notionIdsEqual(ds.id, dataSourceId))?.properties : void 0;
|
|
25953
|
+
return notionClient.getDatabaseSchema(
|
|
25954
|
+
databaseId,
|
|
25955
|
+
own ? { ...meta, properties: own } : meta
|
|
25956
|
+
);
|
|
25957
|
+
},
|
|
24156
25958
|
wikilinkResolver,
|
|
24157
25959
|
() => PropertyMapper.fromConfig(config),
|
|
24158
|
-
async () => peopleResolverOf(await notionClient.listUsers())
|
|
25960
|
+
async () => peopleResolverOf(await notionClient.listUsers()),
|
|
25961
|
+
async (databaseId) => {
|
|
25962
|
+
const sources = (await notionClient.getDatabaseMeta(databaseId)).dataSources;
|
|
25963
|
+
return sources.length > 1 ? sources[0].id : null;
|
|
25964
|
+
}
|
|
24159
25965
|
);
|
|
25966
|
+
this.vaultRenames = new VaultSchemaRenames(stateDb, vaultFs, notionClient);
|
|
25967
|
+
this.sidecarEdits = new SidecarSchemaEdits(stateDb, vaultFs, notionClient);
|
|
25968
|
+
this.viewEdits = new BaseViewEdits(stateDb, vaultFs, notionClient);
|
|
24160
25969
|
this.drift = new RemoteDriftChecker(
|
|
24161
25970
|
config,
|
|
24162
25971
|
stateDb,
|
|
@@ -24254,6 +26063,12 @@ var SyncOrchestrator = class {
|
|
|
24254
26063
|
databaseSyncer;
|
|
24255
26064
|
/** DB 행 push 용 — DB 마다 스키마를 읽은 매퍼. 실행마다 비운다(S-01). */
|
|
24256
26065
|
rowSchemas;
|
|
26066
|
+
/** 볼트에서 바꾼 DB 속성 이름을 Notion 의 열 이름으로(ADR-045). */
|
|
26067
|
+
vaultRenames;
|
|
26068
|
+
/** 볼트에서 고친 사이드카의 속성을 Notion DB 의 열로(ADR-045). */
|
|
26069
|
+
sidecarEdits;
|
|
26070
|
+
/** 볼트에서 고친 `.base` 의 보기를 Notion 보기로(ADR-046). */
|
|
26071
|
+
viewEdits;
|
|
24257
26072
|
// 이번 실행이 원격을 보는 기준(N-05) — push · pull · status 가 시작할 때 정한다
|
|
24258
26073
|
// ({@link RunObservation.begin}). 원격 판정과 관측 기록이 같은 값을 쓴다.
|
|
24259
26074
|
observation;
|
|
@@ -24335,20 +26150,31 @@ var SyncOrchestrator = class {
|
|
|
24335
26150
|
const excludeSet = options?.excludePaths ? new Set(options.excludePaths) : null;
|
|
24336
26151
|
const filtered = (options?.paths ? eligible.filter((c) => inAnyPathScope(c.path, options.paths)) : eligible).filter((c) => !excludeSet || !excludeSet.has(c.path));
|
|
24337
26152
|
const folderMoves = this.planner.pendingFolderMoves(plan).filter((move) => inAnyPathScope(move.to, options?.paths));
|
|
26153
|
+
const inScope = (path) => inAnyPathScope(path, options?.paths) && !excludeSet?.has(path);
|
|
26154
|
+
const sidecarPlan = await this.sidecarEdits.plan(inScope);
|
|
26155
|
+
const plannedEdits = sidecarPlan.planned.map((edit) => edit.edit);
|
|
26156
|
+
const viewPlan = await this.viewEdits.plan(inScope);
|
|
26157
|
+
const plannedViews = viewPlan.planned.flatMap((file) => file.views.map((view2) => view2.edit));
|
|
24338
26158
|
if (filtered.length === 0 && folderMoves.length === 0) {
|
|
24339
26159
|
if (!options?.dryRun) await this.completeDatabaseSchemas();
|
|
26160
|
+
const edited2 = options?.dryRun ? { applied: plannedEdits, failed: [] } : await this.sidecarEdits.apply(sidecarPlan.planned);
|
|
26161
|
+
const viewed2 = options?.dryRun ? { applied: plannedViews, failed: [] } : await this.viewEdits.apply(viewPlan.planned);
|
|
24340
26162
|
return {
|
|
24341
26163
|
created: 0,
|
|
24342
26164
|
updated: 0,
|
|
24343
26165
|
deleted: 0,
|
|
24344
26166
|
moved: 0,
|
|
24345
|
-
failed: [],
|
|
24346
|
-
duration: Date.now() - startTime
|
|
26167
|
+
failed: [...sidecarPlan.refused, ...edited2.failed, ...viewPlan.refused, ...viewed2.failed],
|
|
26168
|
+
duration: Date.now() - startTime,
|
|
26169
|
+
...edited2.applied.length > 0 ? { schemaEdits: edited2.applied } : {},
|
|
26170
|
+
...viewed2.applied.length > 0 ? { viewEdits: viewed2.applied } : {}
|
|
24347
26171
|
};
|
|
24348
26172
|
}
|
|
24349
26173
|
const refusedCreates = this.placement.refusedCreates(filtered, view.lookup);
|
|
24350
26174
|
const refusedMoves = this.placement.refusedMoves(filtered, view);
|
|
24351
26175
|
const refusedFailures = [
|
|
26176
|
+
...sidecarPlan.refused,
|
|
26177
|
+
...viewPlan.refused,
|
|
24352
26178
|
...[...refusedCreates].map(([path, error]) => ({
|
|
24353
26179
|
path,
|
|
24354
26180
|
operation: "create",
|
|
@@ -24362,9 +26188,14 @@ var SyncOrchestrator = class {
|
|
|
24362
26188
|
if (error) refusedFailures.push({ path: move.to, operation: "move", error });
|
|
24363
26189
|
else movableFolders.push(move);
|
|
24364
26190
|
}
|
|
24365
|
-
const
|
|
26191
|
+
const placeable2 = filtered.filter(
|
|
24366
26192
|
(c) => !refusedCreates.has(c.path) && !refusedMoves.has(c.path)
|
|
24367
26193
|
);
|
|
26194
|
+
const renames = await this.vaultRenames.plan(placeable2);
|
|
26195
|
+
refusedFailures.push(...renames.refused);
|
|
26196
|
+
const renameRefused = new Set(renames.refused.map((f) => f.path));
|
|
26197
|
+
const applicable = placeable2.filter((c) => !renameRefused.has(c.path));
|
|
26198
|
+
const plannedRenames = renames.planned.map(({ folder, from, to }) => ({ folder, from, to }));
|
|
24368
26199
|
if (options?.dryRun) {
|
|
24369
26200
|
const planned = this.config.sync.deleteSync ? applicable : applicable.filter((c) => c.type !== "deleted");
|
|
24370
26201
|
const items = [
|
|
@@ -24389,16 +26220,30 @@ var SyncOrchestrator = class {
|
|
|
24389
26220
|
failed: refusedFailures,
|
|
24390
26221
|
duration: Date.now() - startTime,
|
|
24391
26222
|
...unsentDeletions2.length > 0 ? { unsentDeletions: unsentDeletions2 } : {},
|
|
24392
|
-
...readOnlySkipped.length > 0 ? { readOnlySkipped } : {}
|
|
26223
|
+
...readOnlySkipped.length > 0 ? { readOnlySkipped } : {},
|
|
26224
|
+
...plannedRenames.length > 0 ? { renamedProperties: plannedRenames } : {},
|
|
26225
|
+
...plannedEdits.length > 0 ? { schemaEdits: plannedEdits } : {},
|
|
26226
|
+
...plannedViews.length > 0 ? { viewEdits: plannedViews } : {}
|
|
24393
26227
|
};
|
|
24394
26228
|
}
|
|
24395
26229
|
this.stateDb.setMeta(PUSH_IN_PROGRESS_META_KEY, "true");
|
|
24396
26230
|
await this.observation.resolveBotUserId();
|
|
24397
26231
|
const counts = { created: 0, updated: 0, deleted: 0, moved: 0 };
|
|
24398
26232
|
const failed = [...refusedFailures];
|
|
26233
|
+
const edited = await this.sidecarEdits.apply(sidecarPlan.planned);
|
|
26234
|
+
failed.push(...edited.failed);
|
|
26235
|
+
const viewed = await this.viewEdits.apply(viewPlan.planned);
|
|
26236
|
+
failed.push(...viewed.failed);
|
|
26237
|
+
const renamed = await this.vaultRenames.apply(renames.planned);
|
|
26238
|
+
failed.push(...renamed.failed);
|
|
26239
|
+
if (renamed.applied.length > 0 || edited.applied.length > 0) this.rowSchemas.clear();
|
|
26240
|
+
const renameFailed = new Set(renamed.failed.map((f) => f.path));
|
|
26241
|
+
const pushing = applicable.filter(
|
|
26242
|
+
(c) => !renamed.settled.has(c.path) && !renameFailed.has(c.path)
|
|
26243
|
+
);
|
|
24399
26244
|
const unsentDeletions = [];
|
|
24400
26245
|
let completed = 0;
|
|
24401
|
-
const total = movableFolders.length +
|
|
26246
|
+
const total = movableFolders.length + pushing.length;
|
|
24402
26247
|
counts.moved += await this.pusher.pushFolderMoves(
|
|
24403
26248
|
movableFolders,
|
|
24404
26249
|
failed,
|
|
@@ -24446,14 +26291,10 @@ var SyncOrchestrator = class {
|
|
|
24446
26291
|
retryQueue.push(change);
|
|
24447
26292
|
}
|
|
24448
26293
|
};
|
|
24449
|
-
const pushedEarly = await this.placement.prepareFolders(
|
|
24450
|
-
applicable,
|
|
24451
|
-
firstAttempt,
|
|
24452
|
-
!options?.paths
|
|
24453
|
-
);
|
|
26294
|
+
const pushedEarly = await this.placement.prepareFolders(pushing, firstAttempt, !options?.paths);
|
|
24454
26295
|
await this.completeDatabaseSchemas();
|
|
24455
26296
|
await runPool(
|
|
24456
|
-
|
|
26297
|
+
pushing.filter((c) => !pushedEarly.has(c.path)),
|
|
24457
26298
|
firstAttempt,
|
|
24458
26299
|
{ concurrency: this.config.advanced.concurrency, signal: options?.signal }
|
|
24459
26300
|
);
|
|
@@ -24582,7 +26423,10 @@ var SyncOrchestrator = class {
|
|
|
24582
26423
|
failed,
|
|
24583
26424
|
duration: Date.now() - startTime,
|
|
24584
26425
|
readOnlySkipped: this.pusher.takeReadOnlySkips(),
|
|
24585
|
-
...unsentDeletions.length > 0 ? { unsentDeletions } : {}
|
|
26426
|
+
...unsentDeletions.length > 0 ? { unsentDeletions } : {},
|
|
26427
|
+
...renamed.applied.length > 0 ? { renamedProperties: renamed.applied } : {},
|
|
26428
|
+
...edited.applied.length > 0 ? { schemaEdits: edited.applied } : {},
|
|
26429
|
+
...viewed.applied.length > 0 ? { viewEdits: viewed.applied } : {}
|
|
24586
26430
|
};
|
|
24587
26431
|
}
|
|
24588
26432
|
/**
|
|
@@ -24667,6 +26511,9 @@ var SyncOrchestrator = class {
|
|
|
24667
26511
|
this.puller.beginPull();
|
|
24668
26512
|
this.rowSchemas.clear();
|
|
24669
26513
|
this.databaseSyncer.takeRestoredRows();
|
|
26514
|
+
this.databaseSyncer.takeSchemaFollowed();
|
|
26515
|
+
this.databaseSyncer.takeHeldSidecars();
|
|
26516
|
+
this.databaseSyncer.takeHeldBases();
|
|
24670
26517
|
const emptyResult = {
|
|
24671
26518
|
created: 0,
|
|
24672
26519
|
updated: 0,
|
|
@@ -24746,6 +26593,9 @@ var SyncOrchestrator = class {
|
|
|
24746
26593
|
await this.puller.removeVacatedFolders();
|
|
24747
26594
|
const renamedRows = this.databaseSyncer.takeRenamedRows();
|
|
24748
26595
|
restoredPaths.push(...this.databaseSyncer.takeRestoredRows());
|
|
26596
|
+
const schemaFollowed = this.databaseSyncer.takeSchemaFollowed();
|
|
26597
|
+
const heldSidecars = this.databaseSyncer.takeHeldSidecars();
|
|
26598
|
+
const heldBases = this.databaseSyncer.takeHeldBases();
|
|
24749
26599
|
const relinked = await this.linkUpdater.apply([...relocated.applied, ...renamedRows.applied]);
|
|
24750
26600
|
const linkCount = writtenPaths.length > 0 ? await this.resolveNotionLinks(writtenPaths) : 0;
|
|
24751
26601
|
if (!options?.paths) {
|
|
@@ -24771,6 +26621,9 @@ var SyncOrchestrator = class {
|
|
|
24771
26621
|
deleted,
|
|
24772
26622
|
restored,
|
|
24773
26623
|
...restoredPaths.length > 0 ? { restoredPaths } : {},
|
|
26624
|
+
...schemaFollowed.length > 0 ? { schemaFollowed } : {},
|
|
26625
|
+
...heldSidecars.length > 0 ? { heldSidecars } : {},
|
|
26626
|
+
...heldBases.length > 0 ? { heldBases } : {},
|
|
24774
26627
|
conflicts,
|
|
24775
26628
|
writtenPaths,
|
|
24776
26629
|
failed,
|
|
@@ -24885,6 +26738,7 @@ var SyncOrchestrator = class {
|
|
|
24885
26738
|
conflicts,
|
|
24886
26739
|
{
|
|
24887
26740
|
forceRediscovery: options?.force === true,
|
|
26741
|
+
rescanRoot: this.detector.rootChanged(),
|
|
24888
26742
|
paths: options?.paths,
|
|
24889
26743
|
progress: rowProgress,
|
|
24890
26744
|
ledger,
|
|
@@ -25311,14 +27165,14 @@ function toVaultPath(relativePath2, separator) {
|
|
|
25311
27165
|
function compileAll(patterns) {
|
|
25312
27166
|
return patterns.flatMap((raw) => {
|
|
25313
27167
|
const pattern = normalizePattern(raw);
|
|
25314
|
-
return pattern === null ? [] : [
|
|
27168
|
+
return pattern === null ? [] : [compile2(pattern)];
|
|
25315
27169
|
});
|
|
25316
27170
|
}
|
|
25317
27171
|
function normalizePattern(raw) {
|
|
25318
27172
|
const pattern = raw.trim().replace(/\\/g, "/").replace(/^(?:\.\/|\/)+/, "").replace(/\/+$/, "");
|
|
25319
27173
|
return pattern === "" ? null : pattern;
|
|
25320
27174
|
}
|
|
25321
|
-
function
|
|
27175
|
+
function compile2(pattern) {
|
|
25322
27176
|
if (pattern === "**" || pattern === "**/*") return { kind: "all" };
|
|
25323
27177
|
if (!/[*?]/.test(pattern)) return { kind: "prefix", prefix: pattern };
|
|
25324
27178
|
if (pattern.endsWith("/**") && !/[*?]/.test(pattern.slice(0, -3))) {
|
|
@@ -26554,7 +28408,7 @@ var SEP_RE = new RegExp(`^[\\t ]*${COLUMN_SEP_SOURCE}[\\t ]*$`);
|
|
|
26554
28408
|
var FENCE_RE = /^[\t ]*(`{3,}|~{3,})/;
|
|
26555
28409
|
function readColumnLayout(lines) {
|
|
26556
28410
|
const places = new Array(lines.length);
|
|
26557
|
-
let
|
|
28411
|
+
let group2 = -1;
|
|
26558
28412
|
let fence = null;
|
|
26559
28413
|
let open = null;
|
|
26560
28414
|
const close = (end) => {
|
|
@@ -26564,7 +28418,7 @@ function readColumnLayout(lines) {
|
|
|
26564
28418
|
const until = columns[index + 1]?.line ?? end;
|
|
26565
28419
|
for (let line = column.line; line < until; line++) {
|
|
26566
28420
|
places[line] = {
|
|
26567
|
-
group,
|
|
28421
|
+
group: group2,
|
|
26568
28422
|
index,
|
|
26569
28423
|
count: columns.length,
|
|
26570
28424
|
...column.ratio === void 0 ? {} : { ratio: column.ratio },
|
|
@@ -26586,7 +28440,7 @@ function readColumnLayout(lines) {
|
|
|
26586
28440
|
}
|
|
26587
28441
|
if (START_RE.test(line)) {
|
|
26588
28442
|
close(i);
|
|
26589
|
-
|
|
28443
|
+
group2++;
|
|
26590
28444
|
open = { start: i, columns: [] };
|
|
26591
28445
|
return;
|
|
26592
28446
|
}
|
|
@@ -26612,6 +28466,6 @@ function formatRatio(ratio) {
|
|
|
26612
28466
|
return String(Math.round(ratio * 100) / 100);
|
|
26613
28467
|
}
|
|
26614
28468
|
|
|
26615
|
-
export { BaseFileGenerator, BlankLineCollapser, CONFIG_FILE, CalloutRestorer, CalloutTransformer, ChangeDetector, ColorAnnotator, ConfigManager, ConfigSchema, ConflictResolver, ConnectionBoundaryError, ConnectionScopeError, ConversionPipeline, DB_VIEWS_FILE, DB_VIEWS_PATH, DEFAULT_CONFIG, DEFAULT_TITLE_PROPERTY, DEFAULT_WATCH_DEBOUNCE_MS, DIFF_SIGN, DatabaseSyncer, EmbedResolver, EntryEditor, FileHandler, FileWatcher, FilteredVaultFS, FrontmatterExtractor, FrontmatterGenerator, GITIGNORE_ENTRY, IGNORE_FILE, INTERNAL_DIR, INTERNAL_DIR_GLOB, ImageHandler, InlineAnnotationPreserver, InlineDBParser, LocalImageRestorer, MARKER_BRAND, MathNormalizer, MentionToWikilink, NOTION_COLOR_NAMES, NOTION_TOKEN_PREFIX, NOTION_TO_BASES_VIEW, NodeVaultFS, NotionBlockBuilder, NotionClient, OBSIDIAN_CONFIG_DIR, PathFilter, PendingWalError, PreserveMarkerCollector, PreserveMarkerInjector, PropertiesTableInjector, PropertiesTableRestorer, PropertyMapper, RENDER_LINE_RULES, SAVED_STATE_TABLES_QUERY, SEPARATOR_BODY, STATE_DB_FILE, STATE_DB_MIGRATIONS, STATE_DB_PATH, SavedStateDbError, ScopedVaultFS, SidecarGenerator, StateDB, StateDbLockedError, StateDbUnavailableError, StateLock, SyncBusyError, SyncOrchestrator, TreeMapper, UnsupportedBlockStripper, ViewDataProvider, WIKILINK_LOOKUPS, WatchSyncService, WikilinkResolver, applicableChoices, assertNoPendingWal, basesViewMappingOf, basesViewTypeOf, choiceForStrategy, classifyBodyFidelity, columnLabel, compactNotionId, computeBufferHash, computeHash, configDirFrom, connectionFolderOf, createDefaultPipeline, extractCalendarEntries, filterByMonth, filterEntries, findConnectionFolders, findNestedFolderPages, formatUnifiedDiff, generateColorCSS, generateId, getLogger, getNotionBgColor, getNotionColor, getVisibleProperties, groupEntries, hasColumnLayout, hasDesignMarker, inAnyPathScope, isHeadingLine, isHeadingMarkerLine, isInternalPath, isMarkdownPath, isRemoteDeletion, lineDiff, lintRenderedMarkdown, matchesPathScope, normalizeNotionId, normalizeVaultPath, noteNameFromPath, notionIdFromInput, notionIdsEqual, operationLabel, parseDesignColor, parseIgnorePatterns, pendingWalBytes, readColumnLayout, readDesignLine, resolvePropertyName, sanitizeFileName, sectionLinesWithHeadingMarkers, setLogger, sortEntries, stateDbLockPath, stateDbWalPath, stripMarkdownExtension, summarizeFidelity, systemTimeZone, threeWayMerge, toVaultPath, translateNotionFilter, verifyDatabaseCompleteness, verifyPageCompleteness, wikilinkLookupsIn };
|
|
28469
|
+
export { BaseFileGenerator, BlankLineCollapser, CONFIG_FILE, CalloutRestorer, CalloutTransformer, ChangeDetector, ColorAnnotator, ConfigManager, ConfigSchema, ConflictResolver, ConnectionBoundaryError, ConnectionScopeError, ConversionPipeline, DB_VIEWS_FILE, DB_VIEWS_PATH, DEFAULT_CONFIG, DEFAULT_TITLE_PROPERTY, DEFAULT_WATCH_DEBOUNCE_MS, DIFF_SIGN, DatabaseSyncer, EmbedResolver, EntryEditor, FileHandler, FileWatcher, FilteredVaultFS, FrontmatterExtractor, FrontmatterGenerator, GITIGNORE_ENTRY, IGNORE_FILE, INTERNAL_DIR, INTERNAL_DIR_GLOB, ImageHandler, InlineAnnotationPreserver, InlineDBParser, LocalImageRestorer, MARKER_BRAND, MathNormalizer, MentionToWikilink, NOTION_COLOR_NAMES, NOTION_TOKEN_PREFIX, NOTION_TO_BASES_VIEW, NodeVaultFS, NotionBlockBuilder, NotionClient, OBSIDIAN_CONFIG_DIR, PathFilter, PendingWalError, PreserveMarkerCollector, PreserveMarkerInjector, PropertiesTableInjector, PropertiesTableRestorer, PropertyMapper, RENDER_LINE_RULES, SAVED_STATE_TABLES_QUERY, SEPARATOR_BODY, STATE_DB_FILE, STATE_DB_MIGRATIONS, STATE_DB_PATH, SavedStateDbError, ScopedVaultFS, SidecarGenerator, StateDB, StateDbLockedError, StateDbUnavailableError, StateLock, SyncBusyError, SyncOrchestrator, TreeMapper, UnsupportedBlockStripper, ViewDataProvider, WIKILINK_LOOKUPS, WatchSyncService, WikilinkResolver, applicableChoices, assertNoPendingWal, basesViewMappingOf, basesViewTypeOf, choiceForStrategy, classifyBodyFidelity, columnLabel, compactNotionId, computeBufferHash, computeHash, configDirFrom, connectionFolderOf, createDefaultPipeline, extractCalendarEntries, filterByMonth, filterEntries, findConnectionFolders, findNestedFolderPages, formatUnifiedDiff, generateColorCSS, generateId, getLogger, getNotionBgColor, getNotionColor, getVisibleProperties, groupEntries, hasColumnLayout, hasDesignMarker, inAnyPathScope, isHeadingLine, isHeadingMarkerLine, isInternalPath, isMarkdownPath, isRemoteDeletion, lineDiff, lintRenderedMarkdown, matchesPathScope, normalizeNotionId, normalizeVaultPath, noteNameFromPath, notionIdFromInput, notionIdsEqual, operationLabel, parseDesignColor, parseIgnorePatterns, pendingWalBytes, readColumnLayout, readDesignLine, resolvePropertyName, sanitizeFileName, sectionLinesWithHeadingMarkers, setLogger, shortNotionId, sortEntries, stateDbLockPath, stateDbWalPath, stripMarkdownExtension, summarizeFidelity, systemTimeZone, threeWayMerge, toVaultPath, translateNotionFilter, verifyDatabaseCompleteness, verifyPageCompleteness, wikilinkLookupsIn };
|
|
26616
28470
|
//# sourceMappingURL=index.js.map
|
|
26617
28471
|
//# sourceMappingURL=index.js.map
|