@im-nobsidian/core 0.11.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 +299 -25
- package/dist/index.js +2855 -387
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { LogLevel, Client } from '@notionhq/client';
|
|
3
|
-
import { readFile, mkdir, writeFile, unlink, rename, stat, readdir } from 'fs/promises';
|
|
3
|
+
import { readFile, mkdir, writeFile, unlink, rmdir, rename, stat, readdir } from 'fs/promises';
|
|
4
4
|
import { join, dirname, sep, relative } from 'path';
|
|
5
5
|
import { mkdirSync, unlinkSync, utimesSync, openSync, writeSync, closeSync, readFileSync, statSync, existsSync, readlinkSync } from 'fs';
|
|
6
6
|
import Database from 'better-sqlite3';
|
|
@@ -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 = {
|
|
@@ -1476,6 +1480,13 @@ function normalizeAnnotations(annotations) {
|
|
|
1476
1480
|
};
|
|
1477
1481
|
}
|
|
1478
1482
|
var DEFAULT_STYLE = JSON.stringify(normalizeAnnotations(void 0));
|
|
1483
|
+
function titleRunsOf(properties) {
|
|
1484
|
+
for (const prop of Object.values(properties)) {
|
|
1485
|
+
const value = prop;
|
|
1486
|
+
if (value?.type === "title") return Array.isArray(value.title) ? value.title : null;
|
|
1487
|
+
}
|
|
1488
|
+
return null;
|
|
1489
|
+
}
|
|
1479
1490
|
function stringifyFrontmatter(content, data) {
|
|
1480
1491
|
const astral = maskAstralCharacters(data);
|
|
1481
1492
|
const out = matter.stringify(
|
|
@@ -1660,6 +1671,7 @@ function notionFileIdOf(url) {
|
|
|
1660
1671
|
}
|
|
1661
1672
|
function fileUrlOfBlock(block) {
|
|
1662
1673
|
const media = block.type ? block[block.type] : void 0;
|
|
1674
|
+
if (block.type === "embed") return media?.url ?? null;
|
|
1663
1675
|
return media?.file?.url ?? media?.external?.url ?? null;
|
|
1664
1676
|
}
|
|
1665
1677
|
|
|
@@ -1671,6 +1683,9 @@ function isFrontmatterDate(value) {
|
|
|
1671
1683
|
}
|
|
1672
1684
|
var RANGE_END_SUFFIX = "_end";
|
|
1673
1685
|
var DATE_VALUED_TYPES = /* @__PURE__ */ new Set(["date", "formula", "rollup"]);
|
|
1686
|
+
function mayHaveRangeEnd(type) {
|
|
1687
|
+
return DATE_VALUED_TYPES.has(type);
|
|
1688
|
+
}
|
|
1674
1689
|
var TIMESTAMP_TYPES = /* @__PURE__ */ new Set(["created_time", "last_edited_time"]);
|
|
1675
1690
|
var READ_ONLY_TYPES = /* @__PURE__ */ new Set([
|
|
1676
1691
|
"created_time",
|
|
@@ -1879,6 +1894,13 @@ var PropertyMapper = class _PropertyMapper {
|
|
|
1879
1894
|
relationKeys(keys) {
|
|
1880
1895
|
return keys.filter((key) => this.schema.get(key)?.type === "relation");
|
|
1881
1896
|
}
|
|
1897
|
+
/** 이 속성들 가운데 Notion 이 값을 정하는 것(수식 · 롤업 · ID · 생성일 …) — 볼트에서 고쳐도 보낼 수 없다. */
|
|
1898
|
+
readOnlyKeys(keys) {
|
|
1899
|
+
return keys.filter((key) => {
|
|
1900
|
+
const type = this.schema.get(key)?.type;
|
|
1901
|
+
return type !== void 0 && READ_ONLY_TYPES.has(type);
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1882
1904
|
/**
|
|
1883
1905
|
* 값을 보낼 수 있는 속성만, pull 이 적는 모양으로 — 원격 값과 로컬 값을 견줄 때 쓴다.
|
|
1884
1906
|
*
|
|
@@ -2307,13 +2329,6 @@ function dateText(value, zone) {
|
|
|
2307
2329
|
if (!DATE_REGEX.test(text)) return null;
|
|
2308
2330
|
return isWallTime(text) ? toZonedIso(text, zone) : text;
|
|
2309
2331
|
}
|
|
2310
|
-
function titleRunsOf(remote) {
|
|
2311
|
-
for (const prop of Object.values(remote)) {
|
|
2312
|
-
const value = prop;
|
|
2313
|
-
if (value?.type === "title") return Array.isArray(value.title) ? value.title : null;
|
|
2314
|
-
}
|
|
2315
|
-
return null;
|
|
2316
|
-
}
|
|
2317
2332
|
function richTextRunsOf(remote, key) {
|
|
2318
2333
|
const value = remote?.[key];
|
|
2319
2334
|
return value?.type === "rich_text" && Array.isArray(value.rich_text) ? value.rich_text : null;
|
|
@@ -2762,8 +2777,7 @@ function isNotionAccessDenied(error) {
|
|
|
2762
2777
|
return e.code === "restricted_resource" || e.status === 403;
|
|
2763
2778
|
}
|
|
2764
2779
|
function isTrashedOrArchived(page) {
|
|
2765
|
-
|
|
2766
|
-
return inTrash || page.archived === true;
|
|
2780
|
+
return page.in_trash === true || page.archived === true;
|
|
2767
2781
|
}
|
|
2768
2782
|
function isNotionBadRequest(error) {
|
|
2769
2783
|
return notionStatusOf(error) === 400;
|
|
@@ -2796,6 +2810,11 @@ var DiscoveryTooLargeError = class extends Error {
|
|
|
2796
2810
|
partial;
|
|
2797
2811
|
};
|
|
2798
2812
|
var NOT_IDEMPOTENT = { idempotent: false };
|
|
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
|
+
}
|
|
2799
2818
|
function parentDatabaseIdOf(page) {
|
|
2800
2819
|
const parent = page.parent;
|
|
2801
2820
|
if (parent?.type !== "data_source_id" && parent?.type !== "database_id") return null;
|
|
@@ -2841,8 +2860,8 @@ var NotionClient = class _NotionClient {
|
|
|
2841
2860
|
// 전역 쿨다운 밖에서, 로그 없이 흐른다.
|
|
2842
2861
|
retry: false,
|
|
2843
2862
|
// SDK 5.27+ 는 브라우저 환경(Obsidian 렌더러)에서 토큰을 받으면 던진다 — 남이 여는 페이지에 토큰이 실리는
|
|
2844
|
-
// 것을 막는 검사다. 플러그인의 토큰은 사용자 기기의 볼트 설정에 있다.
|
|
2845
|
-
|
|
2863
|
+
// 것을 막는 검사다. 플러그인의 토큰은 사용자 기기의 볼트 설정에 있다.
|
|
2864
|
+
dangerouslyAllowBrowser: true,
|
|
2846
2865
|
...options.fetch ? { fetch: options.fetch } : {}
|
|
2847
2866
|
},
|
|
2848
2867
|
options.notifications ?? DEFAULT_CONFIG.sync.notifications
|
|
@@ -2943,7 +2962,7 @@ var NotionClient = class _NotionClient {
|
|
|
2943
2962
|
);
|
|
2944
2963
|
}
|
|
2945
2964
|
async createPage(params) {
|
|
2946
|
-
const parent = params
|
|
2965
|
+
const parent = rowParentOf(params);
|
|
2947
2966
|
const properties = params.parentType === "page" ? { title: { title: [{ text: { content: params.title } }] } } : params.properties ?? {
|
|
2948
2967
|
title: { title: [{ text: { content: params.title } }] }
|
|
2949
2968
|
};
|
|
@@ -2953,7 +2972,7 @@ var NotionClient = class _NotionClient {
|
|
|
2953
2972
|
properties,
|
|
2954
2973
|
children: params.children
|
|
2955
2974
|
}),
|
|
2956
|
-
|
|
2975
|
+
CREATES_BLOCKS
|
|
2957
2976
|
);
|
|
2958
2977
|
}
|
|
2959
2978
|
/**
|
|
@@ -2978,7 +2997,7 @@ var NotionClient = class _NotionClient {
|
|
|
2978
2997
|
}
|
|
2979
2998
|
})
|
|
2980
2999
|
),
|
|
2981
|
-
|
|
3000
|
+
CREATES_BLOCKS
|
|
2982
3001
|
);
|
|
2983
3002
|
const dataSourceId = db.data_sources?.[0]?.id ?? await this.primaryDataSourceId(db.id);
|
|
2984
3003
|
if (!dataSourceId) throw new Error(`\uB9CC\uB4E0 DB(${db.id})\uC758 data source \uB97C \uC54C \uC218 \uC5C6\uC74C`);
|
|
@@ -3013,7 +3032,7 @@ var NotionClient = class _NotionClient {
|
|
|
3013
3032
|
return ds.properties ?? {};
|
|
3014
3033
|
}
|
|
3015
3034
|
async createPageWithMarkdown(params) {
|
|
3016
|
-
const parent = params
|
|
3035
|
+
const parent = rowParentOf(params);
|
|
3017
3036
|
const properties = params.parentType === "page" ? { title: { title: [{ text: { content: params.title } }] } } : params.properties ?? {
|
|
3018
3037
|
title: { title: [{ text: { content: params.title } }] }
|
|
3019
3038
|
};
|
|
@@ -3023,7 +3042,7 @@ var NotionClient = class _NotionClient {
|
|
|
3023
3042
|
properties,
|
|
3024
3043
|
markdown: params.markdown
|
|
3025
3044
|
}),
|
|
3026
|
-
|
|
3045
|
+
CREATES_BLOCKS
|
|
3027
3046
|
);
|
|
3028
3047
|
}
|
|
3029
3048
|
/**
|
|
@@ -3175,6 +3194,7 @@ var NotionClient = class _NotionClient {
|
|
|
3175
3194
|
const dataSources = db.data_sources ?? [];
|
|
3176
3195
|
const merged = {};
|
|
3177
3196
|
const owners = [];
|
|
3197
|
+
const own = /* @__PURE__ */ new Map();
|
|
3178
3198
|
let anyAccessible = false;
|
|
3179
3199
|
for (const ds of dataSources) {
|
|
3180
3200
|
try {
|
|
@@ -3184,6 +3204,7 @@ var NotionClient = class _NotionClient {
|
|
|
3184
3204
|
owners.push(dsObj.parent?.database_id);
|
|
3185
3205
|
if (dsObj.properties && Object.keys(dsObj.properties).length > 0) {
|
|
3186
3206
|
anyAccessible = true;
|
|
3207
|
+
own.set(ds.id, dsObj.properties);
|
|
3187
3208
|
for (const [name, prop] of Object.entries(dsObj.properties)) {
|
|
3188
3209
|
if (!(name in merged)) merged[name] = prop;
|
|
3189
3210
|
}
|
|
@@ -3195,7 +3216,11 @@ var NotionClient = class _NotionClient {
|
|
|
3195
3216
|
const meta = {
|
|
3196
3217
|
databaseId,
|
|
3197
3218
|
title: Array.isArray(db.title) ? db.title[0]?.plain_text ?? "" : "",
|
|
3198
|
-
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
|
+
})),
|
|
3199
3224
|
properties: anyAccessible ? merged : db.properties ?? {},
|
|
3200
3225
|
isInline: db.is_inline === true,
|
|
3201
3226
|
appearance: {
|
|
@@ -3404,7 +3429,7 @@ var NotionClient = class _NotionClient {
|
|
|
3404
3429
|
if (metas.length > 1) {
|
|
3405
3430
|
const label = metas.map((m) => m.name || m.id).join(", ");
|
|
3406
3431
|
getLogger().warn(
|
|
3407
|
-
`[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.`
|
|
3408
3433
|
);
|
|
3409
3434
|
}
|
|
3410
3435
|
const all = [];
|
|
@@ -3438,7 +3463,7 @@ var NotionClient = class _NotionClient {
|
|
|
3438
3463
|
});
|
|
3439
3464
|
}
|
|
3440
3465
|
async archivePage(pageId) {
|
|
3441
|
-
await this.withRateLimit(() => this.client.pages.update({ page_id: pageId,
|
|
3466
|
+
await this.withRateLimit(() => this.client.pages.update({ page_id: pageId, in_trash: true }));
|
|
3442
3467
|
}
|
|
3443
3468
|
/**
|
|
3444
3469
|
* 파일 블록의 지금 받을 수 있는 주소. 블록이 없거나 볼 수 없으면 null 이다.
|
|
@@ -3582,7 +3607,7 @@ var NotionClient = class _NotionClient {
|
|
|
3582
3607
|
children: batch,
|
|
3583
3608
|
...after ? { position: { type: "after_block", after_block: { id: after } } } : {}
|
|
3584
3609
|
}),
|
|
3585
|
-
|
|
3610
|
+
CREATES_BLOCKS
|
|
3586
3611
|
);
|
|
3587
3612
|
const blocks = response.results.slice(0, batch.length);
|
|
3588
3613
|
created.push(...blocks);
|
|
@@ -3894,14 +3919,14 @@ var NotionClient = class _NotionClient {
|
|
|
3894
3919
|
const blockOwner = /* @__PURE__ */ new Map();
|
|
3895
3920
|
const resolveBlockOwner = async (blockId) => {
|
|
3896
3921
|
const seen = /* @__PURE__ */ new Set();
|
|
3897
|
-
const
|
|
3922
|
+
const settle2 = (owner) => {
|
|
3898
3923
|
for (const b of seen) blockOwner.set(b, owner);
|
|
3899
3924
|
return owner;
|
|
3900
3925
|
};
|
|
3901
3926
|
let bid = blockId;
|
|
3902
3927
|
for (let i = 0; i < 20 && bid && !seen.has(bid); i++) {
|
|
3903
3928
|
const cached = blockOwner.get(bid);
|
|
3904
|
-
if (cached !== void 0) return
|
|
3929
|
+
if (cached !== void 0) return settle2(cached);
|
|
3905
3930
|
seen.add(bid);
|
|
3906
3931
|
let bp;
|
|
3907
3932
|
try {
|
|
@@ -3910,14 +3935,14 @@ var NotionClient = class _NotionClient {
|
|
|
3910
3935
|
getLogger().warn(
|
|
3911
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)}`
|
|
3912
3937
|
);
|
|
3913
|
-
return
|
|
3938
|
+
return settle2(null);
|
|
3914
3939
|
}
|
|
3915
|
-
if (bp.type === "page_id" && bp.page_id) return
|
|
3940
|
+
if (bp.type === "page_id" && bp.page_id) return settle2(bp.page_id);
|
|
3916
3941
|
if (bp.type === "block_id" && bp.block_id) {
|
|
3917
3942
|
bid = bp.block_id;
|
|
3918
3943
|
continue;
|
|
3919
3944
|
}
|
|
3920
|
-
return
|
|
3945
|
+
return settle2(null);
|
|
3921
3946
|
}
|
|
3922
3947
|
return null;
|
|
3923
3948
|
};
|
|
@@ -4011,6 +4036,15 @@ var NotionClient = class _NotionClient {
|
|
|
4011
4036
|
async deleteView(viewId) {
|
|
4012
4037
|
await this.withRateLimit(() => this.client.views.delete({ view_id: viewId }));
|
|
4013
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
|
+
}
|
|
4014
4048
|
async getView(viewId) {
|
|
4015
4049
|
const view = await this.withRateLimit(() => this.client.views.retrieve({ view_id: viewId }));
|
|
4016
4050
|
const v = view;
|
|
@@ -4086,6 +4120,12 @@ var NotionClient = class _NotionClient {
|
|
|
4086
4120
|
* 슬롯을 붙잡아 남을 막는 대신, 다 같이 보는 이 게이트로 쉬게 한다.
|
|
4087
4121
|
*/
|
|
4088
4122
|
cooldownUntil = 0;
|
|
4123
|
+
/**
|
|
4124
|
+
* Free 워크스페이스 블록 한도에 막힌 거절과 그 시각. 막힌 동안 블록을 만드는 요청은 보내지 않고 이 거절을 그대로
|
|
4125
|
+
* 돌려준다 — 플랜을 올리기 전에는 풀리지 않으므로 노트마다 같은 거절을 받으러 가지 않는다. 그 사이 플랜을
|
|
4126
|
+
* 올렸을 수 있어 {@link BLOCK_LIMIT_RECHECK_MS} 가 지나면 다시 보내 본다.
|
|
4127
|
+
*/
|
|
4128
|
+
blockLimit = null;
|
|
4089
4129
|
/**
|
|
4090
4130
|
* 요청 1건을 rate limit 슬롯 안에서 실행하고, 재시도 가능한 실패는 백오프 후 다시 시도한다.
|
|
4091
4131
|
*
|
|
@@ -4106,6 +4146,10 @@ var NotionClient = class _NotionClient {
|
|
|
4106
4146
|
*/
|
|
4107
4147
|
async withRateLimit(fn, options) {
|
|
4108
4148
|
const idempotent = options?.idempotent ?? true;
|
|
4149
|
+
if (options?.createsBlocks && this.blockLimit) {
|
|
4150
|
+
if (Date.now() - this.blockLimit.at < BLOCK_LIMIT_RECHECK_MS) throw this.blockLimit.error;
|
|
4151
|
+
this.blockLimit = null;
|
|
4152
|
+
}
|
|
4109
4153
|
this.requestsSent++;
|
|
4110
4154
|
let backoffMs = 0;
|
|
4111
4155
|
for (let attempt = 0; ; attempt++) {
|
|
@@ -4115,6 +4159,12 @@ var NotionClient = class _NotionClient {
|
|
|
4115
4159
|
this.lastRequestTime = Date.now();
|
|
4116
4160
|
return await fn();
|
|
4117
4161
|
} catch (error) {
|
|
4162
|
+
if (isWorkspaceBlockLimit(error)) {
|
|
4163
|
+
const limited = new NotionBlockLimitError(error);
|
|
4164
|
+
this.blockLimit = { error: limited, at: Date.now() };
|
|
4165
|
+
getLogger().warn(`[Im-Nobsidian] ${limited.message}`);
|
|
4166
|
+
throw limited;
|
|
4167
|
+
}
|
|
4118
4168
|
if (!isRetryable(error, idempotent) || attempt >= this.maxRetries) throw error;
|
|
4119
4169
|
const retryAfterMs = extractRetryAfter(error);
|
|
4120
4170
|
backoffMs = retryAfterMs !== null ? retryAfterMs * (1 + Math.random() * 0.25) : this.retryBaseDelayMs * Math.pow(this.retryBackoffFactor, attempt) * (0.5 + Math.random() * 0.5);
|
|
@@ -4179,10 +4229,31 @@ function isAmbiguousFailure(error) {
|
|
|
4179
4229
|
return status === 500 || status === 502 || status === 503 || status === 504;
|
|
4180
4230
|
}
|
|
4181
4231
|
function isRequestBlocked(error) {
|
|
4182
|
-
|
|
4232
|
+
return rateLimitReasonOf(error) === "public_api_request_blocked";
|
|
4233
|
+
}
|
|
4234
|
+
function rateLimitReasonOf(error) {
|
|
4235
|
+
if (notionStatusOf(error) !== 429) return null;
|
|
4183
4236
|
const data = error.additional_data;
|
|
4184
|
-
return data?.rate_limit_reason === "
|
|
4237
|
+
return typeof data?.rate_limit_reason === "string" ? data.rate_limit_reason : null;
|
|
4185
4238
|
}
|
|
4239
|
+
function isWorkspaceBlockLimit(error) {
|
|
4240
|
+
if (notionStatusOf(error) !== 403) return false;
|
|
4241
|
+
const data = error.additional_data;
|
|
4242
|
+
return data?.block_limit !== void 0 && data.block_limit !== null;
|
|
4243
|
+
}
|
|
4244
|
+
var BLOCK_LIMIT_RECHECK_MS = 5 * 6e4;
|
|
4245
|
+
var NotionBlockLimitError = class extends Error {
|
|
4246
|
+
status = 403;
|
|
4247
|
+
code = "restricted_resource";
|
|
4248
|
+
constructor(cause) {
|
|
4249
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
4250
|
+
super(
|
|
4251
|
+
`Notion \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uAC00 \uBB34\uB8CC \uBE14\uB85D \uD55C\uB3C4\uB97C \uB2E4 \uC368\uC11C \uC0C8 \uB0B4\uC6A9\uC744 \uB9CC\uB4E4 \uC218 \uC5C6\uC74C \u2014 Notion \uC5D0\uC11C \uD50C\uB79C\uC744 \uC62C\uB9AC\uBA74 \uD480\uB9B0\uB2E4 (Notion: ${reason})`,
|
|
4252
|
+
{ cause }
|
|
4253
|
+
);
|
|
4254
|
+
this.name = "NotionBlockLimitError";
|
|
4255
|
+
}
|
|
4256
|
+
};
|
|
4186
4257
|
function isRetryable(error, idempotent) {
|
|
4187
4258
|
if (isRequestBlocked(error)) return false;
|
|
4188
4259
|
return isExplicitRejection(error) || idempotent && isAmbiguousFailure(error);
|
|
@@ -4193,10 +4264,13 @@ function describeRetryCause(error) {
|
|
|
4193
4264
|
const parts = [];
|
|
4194
4265
|
if (typeof e.status === "number") parts.push(`status ${e.status}`);
|
|
4195
4266
|
if (typeof e.code === "string") parts.push(e.code);
|
|
4267
|
+
const reason = rateLimitReasonOf(error);
|
|
4268
|
+
if (reason) parts.push(reason);
|
|
4196
4269
|
if (typeof e.message === "string" && e.message) parts.push(e.message.slice(0, 120));
|
|
4197
4270
|
return parts.length > 0 ? parts.join(" \xB7 ") : "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958";
|
|
4198
4271
|
}
|
|
4199
4272
|
var MAX_RETRY_AFTER_MS = 6e4;
|
|
4273
|
+
var MAX_SHARED_RETRY_AFTER_MS = 5 * 6e4;
|
|
4200
4274
|
function readHeader(headers, name) {
|
|
4201
4275
|
if (typeof headers !== "object" || headers === null) return null;
|
|
4202
4276
|
const get = headers.get;
|
|
@@ -4223,7 +4297,8 @@ function extractRetryAfter(error) {
|
|
|
4223
4297
|
ms = Number.isNaN(at) ? Number.NaN : at - Date.now();
|
|
4224
4298
|
}
|
|
4225
4299
|
if (!Number.isFinite(ms) || ms <= 0) return null;
|
|
4226
|
-
|
|
4300
|
+
const cap = rateLimitReasonOf(error) === "public_api_space_request_rate_limit" ? MAX_SHARED_RETRY_AFTER_MS : MAX_RETRY_AFTER_MS;
|
|
4301
|
+
return Math.min(ms, cap);
|
|
4227
4302
|
}
|
|
4228
4303
|
function sleep(ms) {
|
|
4229
4304
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
@@ -5061,6 +5136,7 @@ function deferredSyncedRefMarker(index) {
|
|
|
5061
5136
|
}
|
|
5062
5137
|
var TOGGLE_START = compactMarker("toggle:start");
|
|
5063
5138
|
var TOGGLE_END = compactMarker("toggle:end");
|
|
5139
|
+
var LARGE_FILE_MARKER = compactMarker("large-file");
|
|
5064
5140
|
var TOGGLE_HEADING_START = compactMarker("toggle-heading");
|
|
5065
5141
|
var TOGGLE_HEADING_END = compactMarker("toggle-heading:end");
|
|
5066
5142
|
function calloutIndentMarker(depth2) {
|
|
@@ -5143,7 +5219,6 @@ var MARKER_TOKEN_RE = new RegExp(
|
|
|
5143
5219
|
|
|
5144
5220
|
// src/converter/pipeline.ts
|
|
5145
5221
|
var INLINE_DB_PROBE = new RegExp(`%% ${MARKER_BRAND_RE}:inline-db`);
|
|
5146
|
-
var TOGGLE_HEADING_PROBE = new RegExp(`%% ${MARKER_BRAND_RE}:toggle-heading`);
|
|
5147
5222
|
var ConversionPipeline = class {
|
|
5148
5223
|
preProcessors = [];
|
|
5149
5224
|
postProcessors = [];
|
|
@@ -5155,11 +5230,14 @@ var ConversionPipeline = class {
|
|
|
5155
5230
|
this.postProcessors.push(processor);
|
|
5156
5231
|
this.postProcessors.sort((a, b) => a.order - b.order);
|
|
5157
5232
|
}
|
|
5233
|
+
/**
|
|
5234
|
+
* 토글 헤딩은 Markdown API 경로가 변환한다(`toggle-heading.ts`) — 블록 경로로 보내면 콜아웃이 옛 이모지 꼴로
|
|
5235
|
+
* 바뀐다(`CalloutTransformer`). 예전에 여기 있던 토글 헤딩 탐지는 띄어 쓴 마커를 찾아 한 번도 맞지 않았다.
|
|
5236
|
+
*/
|
|
5158
5237
|
selectPath(content) {
|
|
5159
5238
|
const hasInlineDb = INLINE_DB_PROBE.test(content);
|
|
5160
5239
|
const hasColumnLayout2 = />\s*\[!col\]/.test(content);
|
|
5161
|
-
|
|
5162
|
-
if (hasInlineDb || hasColumnLayout2 || hasToggleHeading) {
|
|
5240
|
+
if (hasInlineDb || hasColumnLayout2) {
|
|
5163
5241
|
return "block-api";
|
|
5164
5242
|
}
|
|
5165
5243
|
return "markdown-api";
|
|
@@ -5788,6 +5866,27 @@ function obsidianMathCloseAt(line, open) {
|
|
|
5788
5866
|
return close;
|
|
5789
5867
|
}
|
|
5790
5868
|
|
|
5869
|
+
// src/converter/media-labels.ts
|
|
5870
|
+
var MEDIA_LABELS = {
|
|
5871
|
+
audio: "\u{1F50A}",
|
|
5872
|
+
video: "\u{1F3AC}",
|
|
5873
|
+
pdf: "\u{1F4C4}",
|
|
5874
|
+
file: "\u{1F4CE}",
|
|
5875
|
+
html: "\u{1F310}"
|
|
5876
|
+
};
|
|
5877
|
+
var LEGACY_MEDIA_ICONS = ["\u{1F39E}"];
|
|
5878
|
+
var MEDIA_PLACEHOLDER_CAPTIONS = new Set(Object.keys(MEDIA_LABELS));
|
|
5879
|
+
function mediaLabel(kind, caption) {
|
|
5880
|
+
return `${MEDIA_LABELS[kind]} ${caption || kind}`;
|
|
5881
|
+
}
|
|
5882
|
+
function mediaLinkRe(kind) {
|
|
5883
|
+
return new RegExp(`\\[${MEDIA_LABELS[kind]}\\s*([^\\]]*)\\]\\(([^)]+)\\)`, "g");
|
|
5884
|
+
}
|
|
5885
|
+
var MEDIA_LABEL_PREFIX_RE = new RegExp(
|
|
5886
|
+
`^(?:${[...Object.values(MEDIA_LABELS), ...LEGACY_MEDIA_ICONS].join("|")})\\s*`,
|
|
5887
|
+
"u"
|
|
5888
|
+
);
|
|
5889
|
+
|
|
5791
5890
|
// src/converter/container-head.ts
|
|
5792
5891
|
var CALLOUT_HEAD_LINE_RE = /^([ \t]{0,3})> \[!(\w+)\]([-+])?[ \t]*(.*)$/;
|
|
5793
5892
|
var TOGGLE_HEAD_SOURCE = String.raw`^([ \t]{0,3})> \[!toggle\]-`;
|
|
@@ -5862,7 +5961,7 @@ var EmbedResolver = class {
|
|
|
5862
5961
|
(match, alt, url, offset) => {
|
|
5863
5962
|
if (code.some(([start, end]) => offset >= start && offset < end)) return match;
|
|
5864
5963
|
if (isVideoUrl(url)) {
|
|
5865
|
-
return `[
|
|
5964
|
+
return `[${mediaLabel("video", alt)}](${url})`;
|
|
5866
5965
|
}
|
|
5867
5966
|
if (isExternalUrl(url)) {
|
|
5868
5967
|
images.push({ url, isExternal: true });
|
|
@@ -6889,8 +6988,8 @@ function assemble(p, fitted) {
|
|
|
6889
6988
|
addLines(linesAt, spot.line, spot.exact ? lines : requote(lines, neighbor(p, spot.line)));
|
|
6890
6989
|
}
|
|
6891
6990
|
}
|
|
6892
|
-
for (const
|
|
6893
|
-
addLines(linesAt,
|
|
6991
|
+
for (const group2 of groupLoose(p, loose)) {
|
|
6992
|
+
addLines(linesAt, group2.at, fitLines(p, group2.at, group2.lines));
|
|
6894
6993
|
}
|
|
6895
6994
|
return joinLines(p, linesAt, inlineAt);
|
|
6896
6995
|
}
|
|
@@ -7426,6 +7525,112 @@ var BlankLineCollapser = class {
|
|
|
7426
7525
|
}
|
|
7427
7526
|
};
|
|
7428
7527
|
|
|
7528
|
+
// src/converter/heading-markers.ts
|
|
7529
|
+
var MARKER = `%%${MARKER_BRAND_RE}:(?:block-color:[a-z_]+|toggle-heading)%%`;
|
|
7530
|
+
var MARKERS = `(?:${MARKER}[ \\t]*)+`;
|
|
7531
|
+
var HEADING_WITH_MARKERS_RE = new RegExp(`^([\\t ]*)(#{1,6}[ \\t].*?)[ \\t]+(${MARKERS})$`, "gm");
|
|
7532
|
+
var MARKER_LINE_AFTER_HEADING_RE = new RegExp(
|
|
7533
|
+
`^([\\t ]*#{1,6}[ \\t].*?)[ \\t]*\\n[\\t ]*(${MARKERS})$`,
|
|
7534
|
+
"gm"
|
|
7535
|
+
);
|
|
7536
|
+
var HEADING_LINE_RE = /^[\t ]*#{1,6}[ \t]/;
|
|
7537
|
+
var MARKER_LINE_RE = new RegExp(`^[\\t ]*${MARKERS}$`);
|
|
7538
|
+
function placeHeadingMarkersBelow(content) {
|
|
7539
|
+
return mapOutsideCodeFences(
|
|
7540
|
+
content,
|
|
7541
|
+
(segment) => segment.replace(
|
|
7542
|
+
HEADING_WITH_MARKERS_RE,
|
|
7543
|
+
(_m, indent, heading, markers) => `${indent}${heading}
|
|
7544
|
+
${indent}${markers.trim()}`
|
|
7545
|
+
)
|
|
7546
|
+
);
|
|
7547
|
+
}
|
|
7548
|
+
function liftHeadingMarkers(content) {
|
|
7549
|
+
return mapOutsideCodeFences(
|
|
7550
|
+
content,
|
|
7551
|
+
(segment) => segment.replace(
|
|
7552
|
+
MARKER_LINE_AFTER_HEADING_RE,
|
|
7553
|
+
(_m, heading, markers) => `${heading} ${markers.trim()}`
|
|
7554
|
+
)
|
|
7555
|
+
);
|
|
7556
|
+
}
|
|
7557
|
+
function isHeadingLine(line) {
|
|
7558
|
+
return HEADING_LINE_RE.test(line);
|
|
7559
|
+
}
|
|
7560
|
+
function isHeadingMarkerLine(line) {
|
|
7561
|
+
return MARKER_LINE_RE.test(line);
|
|
7562
|
+
}
|
|
7563
|
+
function sectionLinesWithHeadingMarkers(all, start, end) {
|
|
7564
|
+
const lines = all.slice(start, end + 1);
|
|
7565
|
+
const last = lines.length - 1;
|
|
7566
|
+
const next = all[end + 1];
|
|
7567
|
+
if (last >= 0 && next !== void 0 && isHeadingLine(lines[last]) && isHeadingMarkerLine(next)) {
|
|
7568
|
+
lines[last] = `${lines[last]} ${next.trim()}`;
|
|
7569
|
+
}
|
|
7570
|
+
return lines;
|
|
7571
|
+
}
|
|
7572
|
+
|
|
7573
|
+
// src/converter/pre-processors/heading-marker-lifter.ts
|
|
7574
|
+
var HeadingMarkerLifter = class {
|
|
7575
|
+
name = "HeadingMarkerLifter";
|
|
7576
|
+
order = 0;
|
|
7577
|
+
process(input) {
|
|
7578
|
+
if (input.context.direction !== "push") {
|
|
7579
|
+
return { content: input.content, metadata: input.metadata };
|
|
7580
|
+
}
|
|
7581
|
+
return { content: liftHeadingMarkers(input.content), metadata: input.metadata };
|
|
7582
|
+
}
|
|
7583
|
+
};
|
|
7584
|
+
|
|
7585
|
+
// src/converter/degrade-link.ts
|
|
7586
|
+
function degradeLink(label, url, marker) {
|
|
7587
|
+
return `[${label}](${url.replace(/\(/g, "%28").replace(/\)/g, "%29")})${marker}`;
|
|
7588
|
+
}
|
|
7589
|
+
var DEGRADE_LINK_SOURCE = "(?:\\[(?:\\\\.|[^\\\\\\]])*\\]\\([^)]*\\))?";
|
|
7590
|
+
function embedVaultLink(src, caption = "") {
|
|
7591
|
+
const label = caption.replace(/<[^>]*>/g, "").replace(/[[\]\\]/g, "").trim();
|
|
7592
|
+
return degradeLink(label === "" ? "\u{1F517} Embed" : `\u{1F517} ${label}`, src, embedMarker(src, caption));
|
|
7593
|
+
}
|
|
7594
|
+
|
|
7595
|
+
// src/converter/large-file-link.ts
|
|
7596
|
+
var LARGE_FILE_LINK_RE = new RegExp(` ?${DEGRADE_LINK_SOURCE}${LARGE_FILE_MARKER}`, "g");
|
|
7597
|
+
function notionFileLocation(pageId, blockId) {
|
|
7598
|
+
const page = `https://www.notion.so/${compactNotionId(pageId)}`;
|
|
7599
|
+
return blockId ? `${page}#${compactNotionId(blockId)}` : page;
|
|
7600
|
+
}
|
|
7601
|
+
function fileSizeLabel(bytes) {
|
|
7602
|
+
const mb = bytes / 1024 / 1024;
|
|
7603
|
+
return mb >= 1 ? `${Math.round(mb)}MB` : `${Math.max(1, Math.round(bytes / 1024))}KB`;
|
|
7604
|
+
}
|
|
7605
|
+
function largeFileLink(location, bytes) {
|
|
7606
|
+
return ` ${degradeLink(`\u2197 Notion \uC5D0\uC11C \uC5F4\uAE30 \xB7 ${fileSizeLabel(bytes)}`, location, LARGE_FILE_MARKER)}`;
|
|
7607
|
+
}
|
|
7608
|
+
function withLargeFileLink(content, fileLink, link) {
|
|
7609
|
+
const at = content.indexOf(fileLink);
|
|
7610
|
+
if (at < 0) return content;
|
|
7611
|
+
const end = at + fileLink.length;
|
|
7612
|
+
const sticky = new RegExp(LARGE_FILE_LINK_RE.source, "y");
|
|
7613
|
+
sticky.lastIndex = end;
|
|
7614
|
+
const existing = sticky.exec(content);
|
|
7615
|
+
const rest = existing ? end + existing[0].length : end;
|
|
7616
|
+
return `${content.slice(0, end)}${link}${content.slice(rest)}`;
|
|
7617
|
+
}
|
|
7618
|
+
function stripLargeFileLinks(content) {
|
|
7619
|
+
return content.includes(LARGE_FILE_MARKER) ? content.replace(LARGE_FILE_LINK_RE, "") : content;
|
|
7620
|
+
}
|
|
7621
|
+
|
|
7622
|
+
// src/converter/pre-processors/large-file-link-stripper.ts
|
|
7623
|
+
var LargeFileLinkStripper = class {
|
|
7624
|
+
name = "LargeFileLinkStripper";
|
|
7625
|
+
order = 0;
|
|
7626
|
+
process(input) {
|
|
7627
|
+
if (input.context.direction !== "push") {
|
|
7628
|
+
return { content: input.content, metadata: input.metadata };
|
|
7629
|
+
}
|
|
7630
|
+
return { content: stripLargeFileLinks(input.content), metadata: input.metadata };
|
|
7631
|
+
}
|
|
7632
|
+
};
|
|
7633
|
+
|
|
7429
7634
|
// src/utils/wikilink-title.ts
|
|
7430
7635
|
function wikilinkTitleFromPath(obsidianPath) {
|
|
7431
7636
|
return noteNameFromPath(obsidianPath);
|
|
@@ -7436,7 +7641,7 @@ function formatWikilink(target, label) {
|
|
|
7436
7641
|
|
|
7437
7642
|
// src/converter/link-restore.ts
|
|
7438
7643
|
var NOTION_LINK_REGEX = new RegExp(
|
|
7439
|
-
`\\[${MARKER_LABEL_CAPTURE}\\]\\((?:https:\\/\\/(?:www\\.)?notion\\.so\\/|notion:\\/\\/)([a-f0-9-]+)\\)(?!%%${MARKER_BRAND_RE}:(?:unknown|embed)
|
|
7644
|
+
`\\[${MARKER_LABEL_CAPTURE}\\]\\((?:https:\\/\\/(?:www\\.)?notion\\.so\\/|notion:\\/\\/)([a-f0-9-]+)\\)(?!%%${MARKER_BRAND_RE}:(?:unknown|embed):|${LARGE_FILE_MARKER})`,
|
|
7440
7645
|
"g"
|
|
7441
7646
|
);
|
|
7442
7647
|
var WIKILINK_PRESERVE_REGEX = new RegExp(
|
|
@@ -7701,7 +7906,7 @@ var PreserveMarkerInjector = class {
|
|
|
7701
7906
|
const rendered = this.renderMarker(m);
|
|
7702
7907
|
if (rendered === null) return false;
|
|
7703
7908
|
if (content.includes(rendered)) return false;
|
|
7704
|
-
return !this.
|
|
7909
|
+
return !this.restoredElsewhere(m);
|
|
7705
7910
|
});
|
|
7706
7911
|
if (missing.length === 0) {
|
|
7707
7912
|
return { content, metadata: input.metadata };
|
|
@@ -7736,15 +7941,11 @@ var PreserveMarkerInjector = class {
|
|
|
7736
7941
|
const body = entries.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
|
|
7737
7942
|
return spacedMarker(`${marker.type}:${body}`);
|
|
7738
7943
|
}
|
|
7739
|
-
/**
|
|
7740
|
-
|
|
7741
|
-
const params = marker.params ?? {};
|
|
7944
|
+
/** 다른 복원기가 맡는 마커 — 본문에 다시 끼우면 중복 · 오염이다. */
|
|
7945
|
+
restoredElsewhere(marker) {
|
|
7742
7946
|
switch (marker.type) {
|
|
7743
|
-
case "wikilink":
|
|
7744
|
-
|
|
7745
|
-
if (!text) return false;
|
|
7746
|
-
return content.includes(`[[${text}]]`) || content.includes(`[[${text}|`);
|
|
7747
|
-
}
|
|
7947
|
+
case "wikilink":
|
|
7948
|
+
return true;
|
|
7748
7949
|
case "local-image":
|
|
7749
7950
|
case "local-file":
|
|
7750
7951
|
return true;
|
|
@@ -7867,6 +8068,53 @@ function mergeRowFrontmatter(localNote, pulled, owned) {
|
|
|
7867
8068
|
}
|
|
7868
8069
|
return frontmatterLines(values);
|
|
7869
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
|
+
}
|
|
7870
8118
|
function mergedKeyOrder(localKeys, pulledKeys, isOwned) {
|
|
7871
8119
|
const pulledSet = new Set(pulledKeys);
|
|
7872
8120
|
const order = localKeys.filter((key) => !isOwned(key) || pulledSet.has(key));
|
|
@@ -7900,15 +8148,15 @@ function localChunks(localNote, data) {
|
|
|
7900
8148
|
}
|
|
7901
8149
|
const chunks = [];
|
|
7902
8150
|
const seen = [];
|
|
7903
|
-
for (const [i,
|
|
7904
|
-
let end =
|
|
7905
|
-
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--;
|
|
7906
8154
|
if (i === 0) {
|
|
7907
8155
|
if (end > 0) return null;
|
|
7908
|
-
chunks.push({ key: "", body: [], trailing:
|
|
8156
|
+
chunks.push({ key: "", body: [], trailing: group2 });
|
|
7909
8157
|
continue;
|
|
7910
8158
|
}
|
|
7911
|
-
const body =
|
|
8159
|
+
const body = group2.slice(0, end);
|
|
7912
8160
|
let parsed;
|
|
7913
8161
|
try {
|
|
7914
8162
|
const split = splitFrontmatter(`---
|
|
@@ -7925,7 +8173,7 @@ ${body.join("\n")}
|
|
|
7925
8173
|
const key = keys[0];
|
|
7926
8174
|
if (!Object.hasOwn(data, key) || !sameFrontmatterValue(parsed[key], data[key])) return null;
|
|
7927
8175
|
seen.push(key);
|
|
7928
|
-
chunks.push({ key, body, trailing:
|
|
8176
|
+
chunks.push({ key, body, trailing: group2.slice(end) });
|
|
7929
8177
|
}
|
|
7930
8178
|
const dataKeys = Object.keys(data);
|
|
7931
8179
|
return seen.length === dataKeys.length && seen.every((key, i) => key === dataKeys[i]) ? chunks : null;
|
|
@@ -8604,7 +8852,10 @@ var CommentRestorer = class {
|
|
|
8604
8852
|
if (input.context.direction !== "pull" || local === void 0) {
|
|
8605
8853
|
return { content: input.content, metadata: input.metadata };
|
|
8606
8854
|
}
|
|
8607
|
-
return {
|
|
8855
|
+
return {
|
|
8856
|
+
content: restoreComments(input.content, liftHeadingMarkers(local)),
|
|
8857
|
+
metadata: input.metadata
|
|
8858
|
+
};
|
|
8608
8859
|
}
|
|
8609
8860
|
};
|
|
8610
8861
|
|
|
@@ -8845,8 +9096,8 @@ ${brokenLines(note2)}`;
|
|
|
8845
9096
|
const lines = Math.max(note.split("\n").length, pulled.split("\n").length);
|
|
8846
9097
|
const limit = Math.min(TRIAL_LIMIT, Math.floor(TRIAL_LINE_LIMIT / lines));
|
|
8847
9098
|
const trial = new Trial(sent, target, slots, limit - 2);
|
|
8848
|
-
const failed = trial.bisect(spots, (
|
|
8849
|
-
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];
|
|
8850
9101
|
});
|
|
8851
9102
|
for (const { slot, run } of failed) {
|
|
8852
9103
|
if (run) trial.refine(run.local, run.pulled, (lines2) => trial.put(slot, lines2));
|
|
@@ -8953,9 +9204,10 @@ function pieces(local, pulled) {
|
|
|
8953
9204
|
let j = 0;
|
|
8954
9205
|
const zip = (to) => {
|
|
8955
9206
|
const theirs = pulled.slice(j, to);
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
9207
|
+
const paired = Math.min(pending.length, theirs.length);
|
|
9208
|
+
for (let k = 0; k < paired; k++) out.push([[pending[k]], [theirs[k]]]);
|
|
9209
|
+
if (pending.length > paired) out.push([pending.slice(paired), []]);
|
|
9210
|
+
for (let k = paired; k < theirs.length; k++) out.push([[], [theirs[k]]]);
|
|
8959
9211
|
pending = [];
|
|
8960
9212
|
j = to;
|
|
8961
9213
|
};
|
|
@@ -9007,20 +9259,20 @@ var Trial = class {
|
|
|
9007
9259
|
*/
|
|
9008
9260
|
bisect(items, swap, check = () => this.fits(), tried = true) {
|
|
9009
9261
|
const failed = [];
|
|
9010
|
-
const visit = (
|
|
9011
|
-
if (
|
|
9262
|
+
const visit = (group2, seen) => {
|
|
9263
|
+
if (group2.length === 0) return;
|
|
9012
9264
|
if (!seen) {
|
|
9013
|
-
swap(
|
|
9265
|
+
swap(group2, true);
|
|
9014
9266
|
if (check()) return;
|
|
9015
|
-
swap(
|
|
9267
|
+
swap(group2, false);
|
|
9016
9268
|
}
|
|
9017
|
-
if (
|
|
9018
|
-
failed.push(...
|
|
9269
|
+
if (group2.length === 1 || this.budget <= 0) {
|
|
9270
|
+
failed.push(...group2);
|
|
9019
9271
|
return;
|
|
9020
9272
|
}
|
|
9021
|
-
const half =
|
|
9022
|
-
visit(
|
|
9023
|
-
visit(
|
|
9273
|
+
const half = group2.length >> 1;
|
|
9274
|
+
visit(group2.slice(0, half), false);
|
|
9275
|
+
visit(group2.slice(half), false);
|
|
9024
9276
|
};
|
|
9025
9277
|
visit(items, tried);
|
|
9026
9278
|
return failed;
|
|
@@ -9060,8 +9312,8 @@ var Trial = class {
|
|
|
9060
9312
|
const differs = own.flatMap((lines, k) => sameLines(lines, theirs[k]) ? [] : [k]);
|
|
9061
9313
|
const failed = this.bisect(
|
|
9062
9314
|
differs,
|
|
9063
|
-
(
|
|
9064
|
-
for (const k of
|
|
9315
|
+
(group2, useLocal) => {
|
|
9316
|
+
for (const k of group2) chosen[k] = [...(useLocal ? own : theirs)[k]];
|
|
9065
9317
|
},
|
|
9066
9318
|
() => put(chosen.flat())
|
|
9067
9319
|
);
|
|
@@ -9130,8 +9382,8 @@ var Trial = class {
|
|
|
9130
9382
|
);
|
|
9131
9383
|
this.bisect(
|
|
9132
9384
|
spots,
|
|
9133
|
-
(
|
|
9134
|
-
for (const spot of
|
|
9385
|
+
(group2, useLocal) => {
|
|
9386
|
+
for (const spot of group2) {
|
|
9135
9387
|
chosen[spot.index] = [...useLocal ? spot.local : spot.pulled];
|
|
9136
9388
|
}
|
|
9137
9389
|
},
|
|
@@ -9172,6 +9424,18 @@ function readable(note) {
|
|
|
9172
9424
|
}
|
|
9173
9425
|
}
|
|
9174
9426
|
|
|
9427
|
+
// src/converter/post-processors/heading-marker-placer.ts
|
|
9428
|
+
var HeadingMarkerPlacer = class {
|
|
9429
|
+
name = "HeadingMarkerPlacer";
|
|
9430
|
+
order = 140;
|
|
9431
|
+
process(input) {
|
|
9432
|
+
if (input.context.direction !== "pull") {
|
|
9433
|
+
return { content: input.content, metadata: input.metadata };
|
|
9434
|
+
}
|
|
9435
|
+
return { content: placeHeadingMarkersBelow(input.content), metadata: input.metadata };
|
|
9436
|
+
}
|
|
9437
|
+
};
|
|
9438
|
+
|
|
9175
9439
|
// src/converter/literal-escapes.ts
|
|
9176
9440
|
var ASCII_PUNCT_RE = /[!-/:-@[-`{-~]/;
|
|
9177
9441
|
var MATH_FENCE_LINE_RE = /^(?:[ \t]*>)*[ \t]*\$\$[ \t]*$/;
|
|
@@ -9752,6 +10016,8 @@ function findBodyEnd(lines, from, level) {
|
|
|
9752
10016
|
// src/utils/sanitize.ts
|
|
9753
10017
|
var INVALID_CHARS = /[<>:"/\\|?*\x00-\x1f]/g;
|
|
9754
10018
|
var WIKILINK_UNSAFE_CHARS = /[[\]]/g;
|
|
10019
|
+
var LINK_SUBPATH_CHARS = /[#^]/g;
|
|
10020
|
+
var TRAILING_DOTS_AND_SPACES = /[.\s]+$/;
|
|
9755
10021
|
var TRAILING_DOTS = /\.+$/;
|
|
9756
10022
|
var RESERVED_NAMES = /* @__PURE__ */ new Set([
|
|
9757
10023
|
"CON",
|
|
@@ -9777,15 +10043,38 @@ var RESERVED_NAMES = /* @__PURE__ */ new Set([
|
|
|
9777
10043
|
"LPT8",
|
|
9778
10044
|
"LPT9"
|
|
9779
10045
|
]);
|
|
9780
|
-
|
|
9781
|
-
|
|
9782
|
-
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
|
|
9786
|
-
|
|
10046
|
+
var FILE_NAME_MAX_BYTES = 200;
|
|
10047
|
+
var FILE_SYSTEM_NAME_MAX_BYTES = 255;
|
|
10048
|
+
var utf8 = new TextEncoder();
|
|
10049
|
+
function utf8Bytes(text) {
|
|
10050
|
+
return utf8.encode(text).length;
|
|
10051
|
+
}
|
|
10052
|
+
function truncateBytes(name, maxBytes) {
|
|
10053
|
+
if (utf8.encode(name).length <= maxBytes) return name;
|
|
10054
|
+
let out = "";
|
|
10055
|
+
let bytes = 0;
|
|
10056
|
+
for (const { segment } of new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(
|
|
10057
|
+
name
|
|
10058
|
+
)) {
|
|
10059
|
+
const size = utf8.encode(segment).length;
|
|
10060
|
+
if (bytes + size > maxBytes) break;
|
|
10061
|
+
out += segment;
|
|
10062
|
+
bytes += size;
|
|
9787
10063
|
}
|
|
9788
|
-
return
|
|
10064
|
+
return out;
|
|
10065
|
+
}
|
|
10066
|
+
function guarded(name) {
|
|
10067
|
+
const safe = RESERVED_NAMES.has(name.toUpperCase()) ? `_${name}` : name;
|
|
10068
|
+
return safe.length === 0 ? "_untitled" : safe;
|
|
10069
|
+
}
|
|
10070
|
+
function sanitizeFileName(name) {
|
|
10071
|
+
const cleaned = name.replace(INVALID_CHARS, "_").replace(WIKILINK_UNSAFE_CHARS, "_").replace(LINK_SUBPATH_CHARS, "_").trim();
|
|
10072
|
+
return guarded(truncateBytes(cleaned, FILE_NAME_MAX_BYTES).replace(TRAILING_DOTS_AND_SPACES, ""));
|
|
10073
|
+
}
|
|
10074
|
+
function legacySanitizeFileName(name) {
|
|
10075
|
+
return guarded(
|
|
10076
|
+
name.replace(INVALID_CHARS, "_").replace(WIKILINK_UNSAFE_CHARS, "_").replace(TRAILING_DOTS, "").trim().slice(0, 200)
|
|
10077
|
+
);
|
|
9789
10078
|
}
|
|
9790
10079
|
|
|
9791
10080
|
// src/converter/nfm-attrs.ts
|
|
@@ -10067,8 +10356,8 @@ function parseRows(body) {
|
|
|
10067
10356
|
}
|
|
10068
10357
|
function parseCols(body) {
|
|
10069
10358
|
const head = body.search(ROW_START_RE);
|
|
10070
|
-
const
|
|
10071
|
-
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] ?? ""));
|
|
10072
10361
|
}
|
|
10073
10362
|
function sameAttrs(a, b) {
|
|
10074
10363
|
return a.length === b.length && a.every(([k, v]) => b.some(([bk, bv]) => bk === k && bv === v));
|
|
@@ -10289,16 +10578,6 @@ function breaksToEmptyBlocks(content) {
|
|
|
10289
10578
|
});
|
|
10290
10579
|
}
|
|
10291
10580
|
|
|
10292
|
-
// src/converter/degrade-link.ts
|
|
10293
|
-
function degradeLink(label, url, marker) {
|
|
10294
|
-
return `[${label}](${url.replace(/\(/g, "%28").replace(/\)/g, "%29")})${marker}`;
|
|
10295
|
-
}
|
|
10296
|
-
var DEGRADE_LINK_SOURCE = "(?:\\[(?:\\\\.|[^\\\\\\]])*\\]\\([^)]*\\))?";
|
|
10297
|
-
function embedVaultLink(src, caption = "") {
|
|
10298
|
-
const label = caption.replace(/<[^>]*>/g, "").replace(/[[\]\\]/g, "").trim();
|
|
10299
|
-
return degradeLink(label === "" ? "\u{1F517} Embed" : `\u{1F517} ${label}`, src, embedMarker(src, caption));
|
|
10300
|
-
}
|
|
10301
|
-
|
|
10302
10581
|
// src/converter/placeholder-indent.ts
|
|
10303
10582
|
var PLACEHOLDER_LINE_RE = new RegExp(`^([ \\t]*)${MEDIA_PLACEHOLDER_HEAD}`);
|
|
10304
10583
|
var LEADING_SPACE_RE3 = /^[ \t]*/;
|
|
@@ -11240,6 +11519,7 @@ var NOTION_AUDIO_RE = /<audio src="([^"]*)">([\s\S]*?)<\/audio>/g;
|
|
|
11240
11519
|
var NOTION_VIDEO_RE = /<video src="([^"]*)">([\s\S]*?)<\/video>/g;
|
|
11241
11520
|
var NOTION_PDF_RE = /<pdf src="([^"]*)">([\s\S]*?)<\/pdf>/g;
|
|
11242
11521
|
var NOTION_FILE_RE = /<file src="([^"]*)">([\s\S]*?)<\/file>/g;
|
|
11522
|
+
var NOTION_HOSTED_EMBED_RE = /<embed\s+src="([^"]*)"[^>]*>([^\n]*?)\s*<\/embed>/g;
|
|
11243
11523
|
var NOTION_TAB_RE = /<tab title="([^"]*)">([\s\S]*?)<\/tab>/g;
|
|
11244
11524
|
var NOTION_LABELED_PAGE_LINK_RE = new RegExp(
|
|
11245
11525
|
`\\[${MARKER_LABEL_CAPTURE}\\]\\(https?:\\/\\/(?:[a-z]+\\.)?notion\\.(?:so|com)\\/(?:p\\/)?([a-f0-9]{32}|[a-f0-9-]{36})\\)`,
|
|
@@ -11294,7 +11574,7 @@ function unescapeBrackets(content) {
|
|
|
11294
11574
|
);
|
|
11295
11575
|
}
|
|
11296
11576
|
function obsidianToNotionEnhanced(obsidian) {
|
|
11297
|
-
let result = obsidian;
|
|
11577
|
+
let result = liftHeadingMarkers(stripLargeFileLinks(obsidian));
|
|
11298
11578
|
result = quotedCodeToChildren(result);
|
|
11299
11579
|
result = breaksToEmptyBlocks(result);
|
|
11300
11580
|
result = pipeTablesToNfmTables(result);
|
|
@@ -11556,20 +11836,20 @@ ${indented}
|
|
|
11556
11836
|
function convertMediaTags(content) {
|
|
11557
11837
|
let result = content;
|
|
11558
11838
|
result = result.replace(NOTION_AUDIO_RE, (_match, src, caption) => {
|
|
11559
|
-
|
|
11560
|
-
return cap ? `[\u{1F50A} ${cap}](${src})` : `[\u{1F50A} audio](${src})`;
|
|
11839
|
+
return `[${mediaLabel("audio", caption.trim())}](${src})`;
|
|
11561
11840
|
});
|
|
11562
11841
|
result = result.replace(NOTION_VIDEO_RE, (_match, src, caption) => {
|
|
11563
|
-
|
|
11564
|
-
return cap ? `[\u{1F3AC} ${cap}](${src})` : `[\u{1F3AC} video](${src})`;
|
|
11842
|
+
return `[${mediaLabel("video", caption.trim())}](${src})`;
|
|
11565
11843
|
});
|
|
11566
11844
|
result = result.replace(NOTION_PDF_RE, (_match, src, caption) => {
|
|
11567
|
-
|
|
11568
|
-
return cap ? `[\u{1F4C4} ${cap}](${src})` : `[\u{1F4C4} pdf](${src})`;
|
|
11845
|
+
return `[${mediaLabel("pdf", caption.trim())}](${src})`;
|
|
11569
11846
|
});
|
|
11570
11847
|
result = result.replace(NOTION_FILE_RE, (_match, src, caption) => {
|
|
11571
|
-
|
|
11572
|
-
|
|
11848
|
+
return `[${mediaLabel("file", caption.trim())}](${src})`;
|
|
11849
|
+
});
|
|
11850
|
+
result = result.replace(NOTION_HOSTED_EMBED_RE, (match, src, caption) => {
|
|
11851
|
+
if (!isNotionHostedFileUrl(src)) return match;
|
|
11852
|
+
return `[${mediaLabel("html", caption.trim())}](${src})`;
|
|
11573
11853
|
});
|
|
11574
11854
|
return result;
|
|
11575
11855
|
}
|
|
@@ -11806,10 +12086,11 @@ $$`;
|
|
|
11806
12086
|
function unescapeNotionChars(content) {
|
|
11807
12087
|
return content.replace(/\\~/g, "~").replace(/\\\^/g, "^");
|
|
11808
12088
|
}
|
|
11809
|
-
var OBSIDIAN_MEDIA_AUDIO_RE =
|
|
11810
|
-
var OBSIDIAN_MEDIA_VIDEO_RE =
|
|
11811
|
-
var OBSIDIAN_MEDIA_PDF_RE =
|
|
11812
|
-
var OBSIDIAN_MEDIA_FILE_RE =
|
|
12089
|
+
var OBSIDIAN_MEDIA_AUDIO_RE = mediaLinkRe("audio");
|
|
12090
|
+
var OBSIDIAN_MEDIA_VIDEO_RE = mediaLinkRe("video");
|
|
12091
|
+
var OBSIDIAN_MEDIA_PDF_RE = mediaLinkRe("pdf");
|
|
12092
|
+
var OBSIDIAN_MEDIA_FILE_RE = mediaLinkRe("file");
|
|
12093
|
+
var OBSIDIAN_MEDIA_HTML_RE = mediaLinkRe("html");
|
|
11813
12094
|
function restoreMediaTags(content) {
|
|
11814
12095
|
let result = content;
|
|
11815
12096
|
const restore = (tag) => (_match, caption, src) => {
|
|
@@ -11820,6 +12101,10 @@ function restoreMediaTags(content) {
|
|
|
11820
12101
|
result = result.replace(OBSIDIAN_MEDIA_VIDEO_RE, restore("video"));
|
|
11821
12102
|
result = result.replace(OBSIDIAN_MEDIA_PDF_RE, restore("pdf"));
|
|
11822
12103
|
result = result.replace(OBSIDIAN_MEDIA_FILE_RE, restore("file"));
|
|
12104
|
+
result = result.replace(OBSIDIAN_MEDIA_HTML_RE, (_match, caption, src) => {
|
|
12105
|
+
const cap = caption.trim() === "html" ? "" : caption;
|
|
12106
|
+
return `<embed src="${src}">${cap}</embed>`;
|
|
12107
|
+
});
|
|
11823
12108
|
return result;
|
|
11824
12109
|
}
|
|
11825
12110
|
var OBSIDIAN_TAB_RE = /^> \[!tab\]\s*(.+)((?:\n> .*)*)/gm;
|
|
@@ -12090,6 +12375,8 @@ function sortKeys(value) {
|
|
|
12090
12375
|
// src/converter/pipeline-factory.ts
|
|
12091
12376
|
function createDefaultPipeline(options) {
|
|
12092
12377
|
const pipeline = new ConversionPipeline();
|
|
12378
|
+
pipeline.registerPreProcessor(new HeadingMarkerLifter());
|
|
12379
|
+
pipeline.registerPreProcessor(new LargeFileLinkStripper());
|
|
12093
12380
|
pipeline.registerPreProcessor(new InlineAnnotationPreserver());
|
|
12094
12381
|
pipeline.registerPreProcessor(new UnsupportedBlockStripper());
|
|
12095
12382
|
pipeline.registerPreProcessor(new CommentStripper());
|
|
@@ -12120,6 +12407,7 @@ function createDefaultPipeline(options) {
|
|
|
12120
12407
|
pipeline.registerPostProcessor(
|
|
12121
12408
|
new LocalFormRestorer((note, context, children) => sentForm(pipeline, note, context, children))
|
|
12122
12409
|
);
|
|
12410
|
+
pipeline.registerPostProcessor(new HeadingMarkerPlacer());
|
|
12123
12411
|
return pipeline;
|
|
12124
12412
|
}
|
|
12125
12413
|
|
|
@@ -12227,6 +12515,11 @@ function isNotionUploadable(path) {
|
|
|
12227
12515
|
const ext = /\.[^.]+$/.exec(name)?.[0].toLowerCase();
|
|
12228
12516
|
return ext !== void 0 && NOTION_UPLOADABLE_EXTENSIONS.has(ext);
|
|
12229
12517
|
}
|
|
12518
|
+
var HTML_BLOCK_EXTENSIONS = /* @__PURE__ */ new Set([".html", ".htm"]);
|
|
12519
|
+
function isHtmlBlockFile(path) {
|
|
12520
|
+
const ext = /\.[^./]+$/.exec(path)?.[0].toLowerCase();
|
|
12521
|
+
return ext !== void 0 && HTML_BLOCK_EXTENSIONS.has(ext);
|
|
12522
|
+
}
|
|
12230
12523
|
function getBlockType(filename) {
|
|
12231
12524
|
const ext = filename.match(/\.[^.]+$/)?.[0]?.toLowerCase();
|
|
12232
12525
|
if (!ext) return "file";
|
|
@@ -12354,10 +12647,15 @@ function toWikilinkAlias(label) {
|
|
|
12354
12647
|
return label.replace(/!?\[([^\]]*)\]\(([^)]*)\)/g, "$1").replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_m, t, d) => d ?? t).replace(/[\r\n]+/g, " ").replace(/\\([!-/:-@[-`{-~])/g, "$1").replace(/\]\]/g, ")").replace(/\|/g, "-").trim();
|
|
12355
12648
|
}
|
|
12356
12649
|
var INTERNAL_FILE_URL_RE = /^file:\/\/%7B.*%7D%7D$/;
|
|
12357
|
-
var
|
|
12358
|
-
|
|
12359
|
-
|
|
12360
|
-
|
|
12650
|
+
var MEDIA_TAG_NAMES = "file|video|audio|pdf|embed";
|
|
12651
|
+
function mediaTagRe(src) {
|
|
12652
|
+
return new RegExp(
|
|
12653
|
+
`<(?:${MEDIA_TAG_NAMES})\\s+src="(${src})">([\\s\\S]*?)<\\/(?:${MEDIA_TAG_NAMES})>`,
|
|
12654
|
+
"g"
|
|
12655
|
+
);
|
|
12656
|
+
}
|
|
12657
|
+
var MEDIA_TAG_RE = mediaTagRe('[^"]+');
|
|
12658
|
+
var MEDIA_TAG_NAME_RE = new RegExp(`^<(${MEDIA_TAG_NAMES})\\s`);
|
|
12361
12659
|
var PLACEHOLDER_IMAGE_STEMS = /* @__PURE__ */ new Set(["image", "untitled"]);
|
|
12362
12660
|
var EMBED_SIZE_RE = /^\d+(?:x\d+)?$/;
|
|
12363
12661
|
function splitEmbedTarget(target) {
|
|
@@ -12381,8 +12679,8 @@ function embedTargetFromCaption(target, caption, uploadedName) {
|
|
|
12381
12679
|
return description ? sized(`${path}|${alias}`) : `${path}|${alias}`;
|
|
12382
12680
|
}
|
|
12383
12681
|
function captionOfLabel(label) {
|
|
12384
|
-
const caption = label.replace(
|
|
12385
|
-
return
|
|
12682
|
+
const caption = label.replace(MEDIA_LABEL_PREFIX_RE, "").replace(/\\(.)/g, "$1").trim();
|
|
12683
|
+
return MEDIA_LABEL_PREFIX_RE.test(label) && MEDIA_PLACEHOLDER_CAPTIONS.has(caption) ? "" : caption;
|
|
12386
12684
|
}
|
|
12387
12685
|
function fileNameOf2(caption, url) {
|
|
12388
12686
|
return savedNameOf(caption, uploadedFileNameFromUrl(url));
|
|
@@ -12409,7 +12707,7 @@ function isNotionMediaUrl(url) {
|
|
|
12409
12707
|
function notionMediaSpans(markdown) {
|
|
12410
12708
|
const spans = [];
|
|
12411
12709
|
for (const link of scanMarkdownLinks(markdown)) {
|
|
12412
|
-
if (!link.isEmbed && !
|
|
12710
|
+
if (!link.isEmbed && !MEDIA_LABEL_PREFIX_RE.test(link.label)) continue;
|
|
12413
12711
|
if (isNotionMediaUrl(link.url)) spans.push(link);
|
|
12414
12712
|
}
|
|
12415
12713
|
for (const tag of markdown.matchAll(MEDIA_TAG_RE)) {
|
|
@@ -12481,6 +12779,7 @@ function mediaBlock2(kind, fileUploadId, vaultTarget, target) {
|
|
|
12481
12779
|
const description = captionOfEmbed(target);
|
|
12482
12780
|
const caption = description ? [{ type: "text", text: { content: description } }] : [];
|
|
12483
12781
|
const file = { type: "file_upload", file_upload: { id: fileUploadId }, caption };
|
|
12782
|
+
if (kind !== "image" && isHtmlBlockFile(vaultTarget)) return { type: "embed", embed: file };
|
|
12484
12783
|
const type = kind === "image" ? "image" : getBlockType(baseName(vaultTarget));
|
|
12485
12784
|
return type === "file" ? { type, file: { ...file, name: baseName(vaultTarget) } } : { type, [type]: file };
|
|
12486
12785
|
}
|
|
@@ -13125,17 +13424,15 @@ var ImageHandler = class {
|
|
|
13125
13424
|
}
|
|
13126
13425
|
async downloadAllFiles(markdown, pageTitle, pageId) {
|
|
13127
13426
|
const fileLinks = scanMarkdownLinks(markdown).filter(
|
|
13128
|
-
(s) => !s.isEmbed &&
|
|
13427
|
+
(s) => !s.isEmbed && MEDIA_LABEL_PREFIX_RE.test(s.label)
|
|
13129
13428
|
);
|
|
13130
13429
|
const notionHttpMatches = fileLinks.filter(
|
|
13131
13430
|
(s) => /^https?:\/\//.test(s.url) && this.isNotionFileUrl(s.url)
|
|
13132
13431
|
);
|
|
13133
13432
|
const mdInternalMatches = fileLinks.filter((s) => INTERNAL_FILE_URL_RE.test(s.url));
|
|
13134
|
-
const
|
|
13135
|
-
const xmlHttpMatches = [...markdown.matchAll(xmlHttpRegex)];
|
|
13433
|
+
const xmlHttpMatches = [...markdown.matchAll(mediaTagRe('https?:\\/\\/[^"]+'))];
|
|
13136
13434
|
const notionXmlHttpMatches = xmlHttpMatches.filter((m) => m[1] && this.isNotionFileUrl(m[1]));
|
|
13137
|
-
const
|
|
13138
|
-
const xmlInternalMatches = [...markdown.matchAll(xmlInternalRegex)];
|
|
13435
|
+
const xmlInternalMatches = [...markdown.matchAll(mediaTagRe("file:\\/\\/%7B.*?%7D%7D"))];
|
|
13139
13436
|
const totalMatches = notionHttpMatches.length + mdInternalMatches.length + notionXmlHttpMatches.length + xmlInternalMatches.length;
|
|
13140
13437
|
if (totalMatches === 0) {
|
|
13141
13438
|
return { content: markdown, downloads: [] };
|
|
@@ -13147,9 +13444,12 @@ var ImageHandler = class {
|
|
|
13147
13444
|
for (const span of notionHttpMatches) {
|
|
13148
13445
|
await sema.acquire();
|
|
13149
13446
|
try {
|
|
13150
|
-
const caption = fileNameOf2(span.label.replace(
|
|
13447
|
+
const caption = fileNameOf2(span.label.replace(MEDIA_LABEL_PREFIX_RE, ""), span.url);
|
|
13151
13448
|
const download = await this.downloadFile(span.url, pageTitle, caption);
|
|
13152
|
-
if (!download.localPath)
|
|
13449
|
+
if (!download.localPath) {
|
|
13450
|
+
result = keepLargeFile(result, span.full, download, pageId);
|
|
13451
|
+
continue;
|
|
13452
|
+
}
|
|
13153
13453
|
downloads.push(download);
|
|
13154
13454
|
const obsidianLink = fileEmbedOf(download.localPath, download.name ?? caption);
|
|
13155
13455
|
result = result.replace(span.full, () => obsidianLink);
|
|
@@ -13178,9 +13478,12 @@ var ImageHandler = class {
|
|
|
13178
13478
|
getLogger().warn(`[Im-Nobsidian] \uD30C\uC77C Block URL \uC870\uD68C \uC2E4\uD328: ${fileName2}`);
|
|
13179
13479
|
continue;
|
|
13180
13480
|
}
|
|
13181
|
-
const caption = savedNameOf(span.label.replace(
|
|
13481
|
+
const caption = savedNameOf(span.label.replace(MEDIA_LABEL_PREFIX_RE, ""), fileName2);
|
|
13182
13482
|
const download = await this.downloadFile(realUrl, pageTitle, caption);
|
|
13183
|
-
if (!download.localPath)
|
|
13483
|
+
if (!download.localPath) {
|
|
13484
|
+
result = keepLargeFile(result, span.full, download, pageId, blockId);
|
|
13485
|
+
continue;
|
|
13486
|
+
}
|
|
13184
13487
|
downloads.push(download);
|
|
13185
13488
|
const obsidianLink = fileEmbedOf(download.localPath, download.name ?? caption);
|
|
13186
13489
|
result = result.replace(span.full, () => obsidianLink);
|
|
@@ -13203,7 +13506,10 @@ var ImageHandler = class {
|
|
|
13203
13506
|
const url = match[1];
|
|
13204
13507
|
const caption = fileNameOf2(match[2] ?? "", url);
|
|
13205
13508
|
const download = await this.downloadFile(url, pageTitle, caption);
|
|
13206
|
-
if (!download.localPath)
|
|
13509
|
+
if (!download.localPath) {
|
|
13510
|
+
result = keepLargeFile(result, match[0], download, pageId);
|
|
13511
|
+
continue;
|
|
13512
|
+
}
|
|
13207
13513
|
downloads.push(download);
|
|
13208
13514
|
const obsidianLink = fileEmbedOf(download.localPath, download.name ?? caption);
|
|
13209
13515
|
result = result.replace(match[0], () => obsidianLink);
|
|
@@ -13235,7 +13541,10 @@ var ImageHandler = class {
|
|
|
13235
13541
|
}
|
|
13236
13542
|
const caption = savedNameOf(match[2] ?? "", fileName2);
|
|
13237
13543
|
const download = await this.downloadFile(realUrl, pageTitle, caption);
|
|
13238
|
-
if (!download.localPath)
|
|
13544
|
+
if (!download.localPath) {
|
|
13545
|
+
result = keepLargeFile(result, match[0], download, pageId, blockId);
|
|
13546
|
+
continue;
|
|
13547
|
+
}
|
|
13239
13548
|
downloads.push(download);
|
|
13240
13549
|
const obsidianLink = fileEmbedOf(download.localPath, download.name ?? caption);
|
|
13241
13550
|
result = result.replace(match[0], () => obsidianLink);
|
|
@@ -13286,18 +13595,21 @@ var ImageHandler = class {
|
|
|
13286
13595
|
const contentLength = response.headers.get("content-length");
|
|
13287
13596
|
const MAX_FILE_SIZE = this.maxFileSizeBytes;
|
|
13288
13597
|
if (contentLength && parseInt(contentLength, 10) > MAX_FILE_SIZE) {
|
|
13289
|
-
const
|
|
13598
|
+
const bytes = parseInt(contentLength, 10);
|
|
13290
13599
|
getLogger().info(
|
|
13291
|
-
`[Im-Nobsidian] \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uAC74\uB108\uB700 \u2014 \uD06C\uAE30 \uC0C1\uD55C \uCD08\uACFC (${
|
|
13600
|
+
`[Im-Nobsidian] \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uAC74\uB108\uB700 \u2014 \uD06C\uAE30 \uC0C1\uD55C \uCD08\uACFC (${fileSizeLabel(bytes)} > ${fileSizeLabel(MAX_FILE_SIZE)}), Notion \uC5D0\uC11C \uC5F4\uAE30 \uB9C1\uD06C\uB97C \uB460: ${caption}`
|
|
13292
13601
|
);
|
|
13293
|
-
return { originalUrl: url, localPath: "", hash: "", size: 0 };
|
|
13602
|
+
return { originalUrl: url, localPath: "", hash: "", size: 0, skippedBytes: bytes };
|
|
13294
13603
|
}
|
|
13295
13604
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
13296
13605
|
const fileHash = createHash("sha256").update(buffer).digest("hex");
|
|
13297
13606
|
const hash = nameHashOf(fileHash);
|
|
13298
13607
|
const extension = this.guessFileExtension(url, response.headers.get("content-type"));
|
|
13299
13608
|
const name = withoutOwnHash(caption, hash);
|
|
13300
|
-
const safeName =
|
|
13609
|
+
const safeName = truncateBytes(
|
|
13610
|
+
name.replace(/[^a-zA-Z0-9가-힣\s._-]/g, "").replace(/\s+/g, "-").slice(0, 80),
|
|
13611
|
+
FILE_SYSTEM_NAME_MAX_BYTES - utf8Bytes(`-${hash}${extension}`)
|
|
13612
|
+
);
|
|
13301
13613
|
const fileName2 = `${safeName}-${hash}${extension}`;
|
|
13302
13614
|
const localPath = `${this.attachmentFolder}/${fileName2}`;
|
|
13303
13615
|
await this.vaultFs.ensureFolder(this.attachmentFolder);
|
|
@@ -13359,6 +13671,49 @@ var ImageHandler = class {
|
|
|
13359
13671
|
return "";
|
|
13360
13672
|
}
|
|
13361
13673
|
};
|
|
13674
|
+
function keepLargeFile(content, fileLink, download, pageId, blockId) {
|
|
13675
|
+
if (download.skippedBytes === void 0 || !pageId) return content;
|
|
13676
|
+
const link = largeFileLink(notionFileLocation(pageId, blockId), download.skippedBytes);
|
|
13677
|
+
return withLargeFileLink(content, fileLink, link);
|
|
13678
|
+
}
|
|
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
|
+
}
|
|
13362
13717
|
|
|
13363
13718
|
// src/view/notion-filter-translator.ts
|
|
13364
13719
|
function translateNotionFilter(filter, schema, resolveName) {
|
|
@@ -13407,7 +13762,7 @@ function translateNode(node, ctx) {
|
|
|
13407
13762
|
function translateLeaf(leaf2, ctx) {
|
|
13408
13763
|
const timestamp = leaf2["timestamp"];
|
|
13409
13764
|
if (typeof timestamp === "string") {
|
|
13410
|
-
const ref2 = timestamp === "created_time" ? "
|
|
13765
|
+
const ref2 = BASES_TIMESTAMPS[timestamp === "created_time" ? "created_time" : "last_edited_time"];
|
|
13411
13766
|
const cond = leaf2[timestamp];
|
|
13412
13767
|
if (!isRecord(cond)) return unsupported(ctx, `timestamp '${timestamp}' \uD544\uD130`);
|
|
13413
13768
|
return translateDate(ref2, cond, ctx, `timestamp '${timestamp}'`);
|
|
@@ -13531,8 +13886,8 @@ function translateStatus(ref, cond, ctx, label, propName) {
|
|
|
13531
13886
|
expanded.push(name);
|
|
13532
13887
|
continue;
|
|
13533
13888
|
}
|
|
13534
|
-
const
|
|
13535
|
-
const members =
|
|
13889
|
+
const group2 = prop?.groups?.find((g) => g.name === name);
|
|
13890
|
+
const members = group2 && prop ? expandGroup(group2, prop) : null;
|
|
13536
13891
|
if (members === null) {
|
|
13537
13892
|
return unsupported(ctx, `${label}: \uC0C1\uD0DC '${name}' \uC744 \uC635\uC158\uC73C\uB85C \uC804\uAC1C\uD558\uC9C0 \uBABB\uD568`);
|
|
13538
13893
|
}
|
|
@@ -13543,8 +13898,8 @@ function translateStatus(ref, cond, ctx, label, propName) {
|
|
|
13543
13898
|
}
|
|
13544
13899
|
return equalityOverNames(ref, op, dedupe(expanded), ctx, label);
|
|
13545
13900
|
}
|
|
13546
|
-
function expandGroup(
|
|
13547
|
-
const ids =
|
|
13901
|
+
function expandGroup(group2, prop) {
|
|
13902
|
+
const ids = group2.optionIds;
|
|
13548
13903
|
if (!ids || ids.length === 0) return [];
|
|
13549
13904
|
const byId = new Map((prop.options ?? []).map((o) => [o.id, o.name]));
|
|
13550
13905
|
const names = [];
|
|
@@ -13584,29 +13939,32 @@ function translateEmptiness(ref, cond, ctx, label) {
|
|
|
13584
13939
|
const [op] = firstEntry(cond);
|
|
13585
13940
|
return unsupported(ctx, `${label}: \uC5F0\uC0B0\uC790 '${op}' \uBBF8\uC9C0\uC6D0`);
|
|
13586
13941
|
}
|
|
13942
|
+
var DATE_COMPARISONS = {
|
|
13943
|
+
equals: "==",
|
|
13944
|
+
before: "<",
|
|
13945
|
+
after: ">",
|
|
13946
|
+
on_or_before: "<=",
|
|
13947
|
+
on_or_after: ">="
|
|
13948
|
+
};
|
|
13587
13949
|
function translateDate(ref, cond, ctx, label) {
|
|
13588
13950
|
const empt = translateEmptinessOnly(ref, cond);
|
|
13589
13951
|
if (empt) return empt;
|
|
13590
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) };
|
|
13591
13955
|
if (typeof value !== "string") {
|
|
13592
13956
|
return unsupported(ctx, `${label}: \uC0C1\uB300 \uB0A0\uC9DC \uC870\uAC74 '${op}' \uC740 \uC62E\uAE30\uC9C0 \uC54A\uC74C`);
|
|
13593
13957
|
}
|
|
13594
|
-
const
|
|
13595
|
-
|
|
13596
|
-
|
|
13597
|
-
|
|
13598
|
-
|
|
13599
|
-
|
|
13600
|
-
case "after":
|
|
13601
|
-
return `${ref} > ${lit}`;
|
|
13602
|
-
case "on_or_before":
|
|
13603
|
-
return `${ref} <= ${lit}`;
|
|
13604
|
-
case "on_or_after":
|
|
13605
|
-
return `${ref} >= ${lit}`;
|
|
13606
|
-
default:
|
|
13607
|
-
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`);
|
|
13608
13964
|
}
|
|
13965
|
+
return `${ref} ${symbol} ${str(value)}`;
|
|
13609
13966
|
}
|
|
13967
|
+
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(?:[T ][\d:.]+(?:Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
13610
13968
|
function translateEmptinessOnly(ref, cond) {
|
|
13611
13969
|
if (cond["is_empty"] === true) return `${ref}.isEmpty()`;
|
|
13612
13970
|
if (cond["is_not_empty"] === true) return `!${ref}.isEmpty()`;
|
|
@@ -13688,8 +14046,16 @@ function filterGroup(node) {
|
|
|
13688
14046
|
return ["not", node.not];
|
|
13689
14047
|
}
|
|
13690
14048
|
var BASE_FILE_HEADER = "# Auto-generated by Im-Nobsidian";
|
|
14049
|
+
function inFolderFilter(folderPath) {
|
|
14050
|
+
return `file.inFolder("${folderPath}")`;
|
|
14051
|
+
}
|
|
14052
|
+
var IN_FOLDER_FILTER_RE = /file\.inFolder\("([^"\n]*)"\)/g;
|
|
13691
14053
|
var BaseFileGenerator = class {
|
|
13692
14054
|
generate(options) {
|
|
14055
|
+
return this.generateWithViews(options).content;
|
|
14056
|
+
}
|
|
14057
|
+
/** `.base` 내용과 함께 쓴 보기마다 어느 Notion 보기에서 왔는지 돌려준다 — pull 이 기록해 push 가 견준다. */
|
|
14058
|
+
generateWithViews(options) {
|
|
13693
14059
|
const lines = [];
|
|
13694
14060
|
lines.push(`${BASE_FILE_HEADER} from Notion DB: ${options.databaseName}`);
|
|
13695
14061
|
lines.push(`# Database ID: ${options.databaseId}`);
|
|
@@ -13697,13 +14063,18 @@ var BaseFileGenerator = class {
|
|
|
13697
14063
|
this.writeFilters(lines, options.folderPath);
|
|
13698
14064
|
this.writeFormulas(lines, options.viewsConfig.views);
|
|
13699
14065
|
this.writePropertyDisplayNames(lines, options.schema);
|
|
13700
|
-
|
|
13701
|
-
|
|
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 };
|
|
13702
14073
|
}
|
|
13703
14074
|
writeFilters(lines, folderPath) {
|
|
13704
14075
|
lines.push("filters:");
|
|
13705
14076
|
lines.push(" and:");
|
|
13706
|
-
lines.push(` -
|
|
14077
|
+
lines.push(` - ${inFolderFilter(folderPath)}`);
|
|
13707
14078
|
lines.push(` - file.ext == "md"`);
|
|
13708
14079
|
lines.push("");
|
|
13709
14080
|
}
|
|
@@ -13729,12 +14100,13 @@ var BaseFileGenerator = class {
|
|
|
13729
14100
|
lines.push("");
|
|
13730
14101
|
}
|
|
13731
14102
|
writeViews(lines, views, schema, rangeEnds = /* @__PURE__ */ new Map()) {
|
|
13732
|
-
const
|
|
14103
|
+
const converted = views.map((v) => this.convertView(v, schema, rangeEnds)).filter((v) => v !== null);
|
|
14104
|
+
const basesViews = converted.map((c) => c.view);
|
|
13733
14105
|
if (basesViews.length === 0) {
|
|
13734
14106
|
lines.push("views:");
|
|
13735
14107
|
lines.push(" - type: table");
|
|
13736
14108
|
lines.push(" name: Table");
|
|
13737
|
-
return;
|
|
14109
|
+
return [];
|
|
13738
14110
|
}
|
|
13739
14111
|
this.dedupeViewNames(basesViews);
|
|
13740
14112
|
lines.push("views:");
|
|
@@ -13766,6 +14138,7 @@ var BaseFileGenerator = class {
|
|
|
13766
14138
|
this.writeFilterValue(lines, view.filters, " ");
|
|
13767
14139
|
}
|
|
13768
14140
|
}
|
|
14141
|
+
return converted.map(({ viewId, lossyFilter }) => ({ viewId, lossyFilter }));
|
|
13769
14142
|
}
|
|
13770
14143
|
/** `filters:` 키와 그 값을 쓴다. 단일 식이면 한 줄, 그룹이면 블록. */
|
|
13771
14144
|
writeFilterValue(lines, node, indent) {
|
|
@@ -13825,6 +14198,7 @@ var BaseFileGenerator = class {
|
|
|
13825
14198
|
convertView(view, schema, rangeEnds) {
|
|
13826
14199
|
const basesType = basesViewMappingOf(view.type).type;
|
|
13827
14200
|
if (!basesType) return null;
|
|
14201
|
+
let lossyFilter = false;
|
|
13828
14202
|
const result = {
|
|
13829
14203
|
type: basesType,
|
|
13830
14204
|
name: view.name || view.type
|
|
@@ -13838,7 +14212,7 @@ var BaseFileGenerator = class {
|
|
|
13838
14212
|
return end === void 0 ? [this.toNoteRef(name)] : [this.toNoteRef(name), this.toNoteRef(end)];
|
|
13839
14213
|
});
|
|
13840
14214
|
if (visible.length > 0) {
|
|
13841
|
-
result.order = [
|
|
14215
|
+
result.order = [BASES_TITLE, ...visible];
|
|
13842
14216
|
}
|
|
13843
14217
|
}
|
|
13844
14218
|
if (view.sorts && view.sorts.length > 0) {
|
|
@@ -13848,7 +14222,7 @@ var BaseFileGenerator = class {
|
|
|
13848
14222
|
const propName = this.resolvePropertyName(s.property, schema) ?? s.property;
|
|
13849
14223
|
property = this.toNoteRef(propName);
|
|
13850
14224
|
} else if (s.timestamp) {
|
|
13851
|
-
property = s.timestamp
|
|
14225
|
+
property = BASES_TIMESTAMPS[s.timestamp];
|
|
13852
14226
|
} else {
|
|
13853
14227
|
return null;
|
|
13854
14228
|
}
|
|
@@ -13884,14 +14258,15 @@ var BaseFileGenerator = class {
|
|
|
13884
14258
|
}
|
|
13885
14259
|
}
|
|
13886
14260
|
if (view.filter !== void 0 && view.filter !== null) {
|
|
13887
|
-
const { node } = translateNotionFilter(
|
|
14261
|
+
const { node, untranslated } = translateNotionFilter(
|
|
13888
14262
|
view.filter,
|
|
13889
14263
|
schema,
|
|
13890
14264
|
(id) => this.resolvePropertyName(id, schema)
|
|
13891
14265
|
);
|
|
13892
14266
|
if (node) result.filters = node;
|
|
14267
|
+
lossyFilter = untranslated.length > 0 || node === null;
|
|
13893
14268
|
}
|
|
13894
|
-
return result;
|
|
14269
|
+
return { view: result, viewId: view.id, lossyFilter };
|
|
13895
14270
|
}
|
|
13896
14271
|
toNoteRef(propertyName) {
|
|
13897
14272
|
return propertyName;
|
|
@@ -14541,9 +14916,9 @@ function noteTitle(frontmatter, path) {
|
|
|
14541
14916
|
return explicitTitle(frontmatter) ?? wikilinkTitleFromPath(path);
|
|
14542
14917
|
}
|
|
14543
14918
|
function isFileNameOf(title, fileName2) {
|
|
14544
|
-
|
|
14545
|
-
|
|
14546
|
-
|
|
14919
|
+
return [sanitizeFileName(title), legacySanitizeFileName(title)].some(
|
|
14920
|
+
(safe) => fileName2 === safe || fileName2.startsWith(safe) && /^ \([0-9a-f]{8,32}\)$/i.test(fileName2.slice(safe.length))
|
|
14921
|
+
);
|
|
14547
14922
|
}
|
|
14548
14923
|
function followsFileName(title, path) {
|
|
14549
14924
|
return isFileNameOf(title, wikilinkTitleFromPath(path));
|
|
@@ -14571,8 +14946,11 @@ function titleMayChange(base, current, from, to) {
|
|
|
14571
14946
|
function extractTitle(filePath) {
|
|
14572
14947
|
return noteNameFromPath(filePath);
|
|
14573
14948
|
}
|
|
14574
|
-
function titleProperty(title) {
|
|
14575
|
-
|
|
14949
|
+
function titleProperty(title, remote) {
|
|
14950
|
+
const runs = remote ? titleRunsOf(remote) : null;
|
|
14951
|
+
return {
|
|
14952
|
+
title: { title: (runs && patchRichText(runs, title)) ?? [{ text: { content: title } }] }
|
|
14953
|
+
};
|
|
14576
14954
|
}
|
|
14577
14955
|
function extractAliases(properties) {
|
|
14578
14956
|
const raw = properties.aliases ?? properties.alias;
|
|
@@ -14599,10 +14977,14 @@ function selectDbRowFiles(files, localFolder) {
|
|
|
14599
14977
|
return rel.length > 0 && !rel.includes("/");
|
|
14600
14978
|
});
|
|
14601
14979
|
}
|
|
14980
|
+
var PAGE_ID_SUFFIX_LENGTHS = [8, 16, 32];
|
|
14602
14981
|
function pagePathCandidates(dir, safeName, pageId) {
|
|
14603
14982
|
const base = dir ? `${dir}/${safeName}` : safeName;
|
|
14604
14983
|
const raw = pageId.replace(/-/g, "");
|
|
14605
|
-
return [
|
|
14984
|
+
return [
|
|
14985
|
+
`${base}.md`,
|
|
14986
|
+
...PAGE_ID_SUFFIX_LENGTHS.map((len) => `${base} (${raw.slice(0, len)}).md`)
|
|
14987
|
+
];
|
|
14606
14988
|
}
|
|
14607
14989
|
function resolveDbRowPath(localFolder, safeName, pageId, lookupByPath) {
|
|
14608
14990
|
const ownedBySelf = (candidate) => {
|
|
@@ -14613,9 +14995,9 @@ function resolveDbRowPath(localFolder, safeName, pageId, lookupByPath) {
|
|
|
14613
14995
|
const candidates = pagePathCandidates(localFolder, safeName, pageId);
|
|
14614
14996
|
return candidates.find(ownedBySelf) ?? candidates[candidates.length - 1];
|
|
14615
14997
|
}
|
|
14616
|
-
function inClaimOrder(items, chosen,
|
|
14998
|
+
function inClaimOrder(items, chosen, idOf2) {
|
|
14617
14999
|
const slots = items.flatMap((item, index) => chosen(item) ? [index] : []);
|
|
14618
|
-
const key = (item) => compactNotionId(
|
|
15000
|
+
const key = (item) => compactNotionId(idOf2(item));
|
|
14619
15001
|
const members = slots.map((index) => items[index]).sort((a, b) => key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0);
|
|
14620
15002
|
const ordered = [...items];
|
|
14621
15003
|
slots.forEach((slot, k) => ordered[slot] = members[k]);
|
|
@@ -14975,12 +15357,17 @@ var DbBaseFiles = class {
|
|
|
14975
15357
|
get(databaseId) {
|
|
14976
15358
|
return this.load().get(compactNotionId(databaseId));
|
|
14977
15359
|
}
|
|
15360
|
+
/** 기록한 DB 모두 — 키는 줄인 DB id. */
|
|
15361
|
+
all() {
|
|
15362
|
+
return [...this.load().entries()];
|
|
15363
|
+
}
|
|
14978
15364
|
set(databaseId, info) {
|
|
14979
15365
|
const entries = this.load();
|
|
14980
15366
|
const key = compactNotionId(databaseId);
|
|
14981
15367
|
const known = entries.get(key);
|
|
14982
|
-
|
|
14983
|
-
|
|
15368
|
+
const next = normalize(info);
|
|
15369
|
+
if (known && JSON.stringify(known) === JSON.stringify(next)) return;
|
|
15370
|
+
entries.set(key, next);
|
|
14984
15371
|
this.loadedRaw = JSON.stringify(Object.fromEntries(entries));
|
|
14985
15372
|
this.stateDb.setMeta(DB_BASE_FILES_META_KEY, this.loadedRaw);
|
|
14986
15373
|
}
|
|
@@ -14997,7 +15384,16 @@ var DbBaseFiles = class {
|
|
|
14997
15384
|
for (const [key, value] of Object.entries(parsed)) {
|
|
14998
15385
|
const entry = value;
|
|
14999
15386
|
if (typeof entry?.basePath === "string" && typeof entry.title === "string") {
|
|
15000
|
-
this.entries.set(
|
|
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
|
+
);
|
|
15001
15397
|
}
|
|
15002
15398
|
}
|
|
15003
15399
|
} catch {
|
|
@@ -15005,53 +15401,632 @@ var DbBaseFiles = class {
|
|
|
15005
15401
|
return this.entries;
|
|
15006
15402
|
}
|
|
15007
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
|
+
}
|
|
15008
15413
|
|
|
15009
|
-
// src/sync/
|
|
15010
|
-
var
|
|
15011
|
-
|
|
15012
|
-
|
|
15013
|
-
var LINKED_DBS_META_KEY = "linked_dbs";
|
|
15014
|
-
function parseDiscoveredDbs(raw) {
|
|
15015
|
-
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;
|
|
15016
15418
|
try {
|
|
15017
|
-
|
|
15018
|
-
|
|
15419
|
+
doc = parseFrontmatter(`---
|
|
15420
|
+
${text}
|
|
15421
|
+
---
|
|
15422
|
+
`).data;
|
|
15019
15423
|
} catch {
|
|
15020
|
-
return
|
|
15424
|
+
return null;
|
|
15021
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 };
|
|
15022
15430
|
}
|
|
15023
|
-
function
|
|
15024
|
-
const
|
|
15025
|
-
|
|
15026
|
-
|
|
15027
|
-
|
|
15028
|
-
|
|
15029
|
-
|
|
15030
|
-
|
|
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
|
+
};
|
|
15446
|
+
}
|
|
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;
|
|
15454
|
+
}
|
|
15455
|
+
function columnName(raw) {
|
|
15456
|
+
const name = String(raw ?? "");
|
|
15457
|
+
return name.startsWith("note.") ? name.slice("note.".length) : name;
|
|
15458
|
+
}
|
|
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;
|
|
15472
|
+
}
|
|
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
|
+
});
|
|
15031
15511
|
}
|
|
15512
|
+
return { views: records, rest: restSignature(parsed, records) };
|
|
15032
15513
|
}
|
|
15033
|
-
function
|
|
15034
|
-
|
|
15035
|
-
|
|
15036
|
-
|
|
15037
|
-
|
|
15038
|
-
|
|
15039
|
-
|
|
15040
|
-
return
|
|
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
|
+
);
|
|
15041
15524
|
}
|
|
15525
|
+
return value;
|
|
15042
15526
|
}
|
|
15043
15527
|
|
|
15044
|
-
// src/sync/
|
|
15045
|
-
|
|
15046
|
-
|
|
15047
|
-
|
|
15048
|
-
|
|
15049
|
-
|
|
15050
|
-
|
|
15051
|
-
|
|
15052
|
-
|
|
15053
|
-
|
|
15054
|
-
|
|
15528
|
+
// src/sync/sidecar-bases.ts
|
|
15529
|
+
var SIDECAR_BASES_META_KEY = "sidecar_bases";
|
|
15530
|
+
var SidecarBases = class {
|
|
15531
|
+
constructor(stateDb) {
|
|
15532
|
+
this.stateDb = stateDb;
|
|
15533
|
+
}
|
|
15534
|
+
stateDb;
|
|
15535
|
+
get(databaseId) {
|
|
15536
|
+
return this.load()[compactNotionId(databaseId)];
|
|
15537
|
+
}
|
|
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 {};
|
|
15565
|
+
}
|
|
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);
|
|
15576
|
+
}
|
|
15577
|
+
} else {
|
|
15578
|
+
for (const ds of meta.dataSources) {
|
|
15579
|
+
if (ds.properties) seen[compactNotionId(ds.id)] = schemaOf(ds.properties);
|
|
15580
|
+
}
|
|
15581
|
+
}
|
|
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;
|
|
15614
|
+
}
|
|
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);
|
|
15632
|
+
}
|
|
15633
|
+
}
|
|
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
|
+
}
|
|
15638
|
+
}
|
|
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);
|
|
15641
|
+
}
|
|
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;
|
|
15055
16030
|
changed = true;
|
|
15056
16031
|
}
|
|
15057
16032
|
if (changed) stateDb.setMeta(DISCOVERED_DBS_META_KEY, JSON.stringify(databases));
|
|
@@ -15082,9 +16057,9 @@ async function retargetBaseFiles(vaultFs, moves) {
|
|
|
15082
16057
|
if (!file.path.endsWith(BASE_SUFFIX)) continue;
|
|
15083
16058
|
const content = await vaultFs.readFile(file.path);
|
|
15084
16059
|
if (!isGeneratedDbArtifact(file.path, content)) continue;
|
|
15085
|
-
const next = content.replace(
|
|
16060
|
+
const next = content.replace(IN_FOLDER_FILTER_RE, (whole, folder) => {
|
|
15086
16061
|
const to = finalPath(folder, folders);
|
|
15087
|
-
return to === folder ? whole :
|
|
16062
|
+
return to === folder ? whole : inFolderFilter(to);
|
|
15088
16063
|
});
|
|
15089
16064
|
if (next !== content) await vaultFs.writeFile(file.path, next);
|
|
15090
16065
|
}
|
|
@@ -15094,7 +16069,6 @@ async function retargetBaseFiles(vaultFs, moves) {
|
|
|
15094
16069
|
);
|
|
15095
16070
|
}
|
|
15096
16071
|
}
|
|
15097
|
-
var IN_FOLDER = /file\.inFolder\("([^"\n]*)"\)/g;
|
|
15098
16072
|
function finalPath(path, moves) {
|
|
15099
16073
|
let current = path;
|
|
15100
16074
|
for (const move of moves) {
|
|
@@ -15632,6 +16606,8 @@ var DatabaseSyncer = class {
|
|
|
15632
16606
|
this.observation = observation;
|
|
15633
16607
|
this.noteRowBody = noteRowBody;
|
|
15634
16608
|
this.baseFileInfo = new DbBaseFiles(stateDb);
|
|
16609
|
+
this.sidecarBases = new SidecarBases(stateDb);
|
|
16610
|
+
this.schemaLedger = new SchemaLedger(stateDb);
|
|
15635
16611
|
this.relocator = new NoteRelocator(stateDb, vaultFs, config.notion.databases ?? []);
|
|
15636
16612
|
this.propertyMapper = PropertyMapper.fromConfig(config);
|
|
15637
16613
|
this.propertyMapper.setWikilinkResolver({
|
|
@@ -15660,7 +16636,7 @@ var DatabaseSyncer = class {
|
|
|
15660
16636
|
*/
|
|
15661
16637
|
appearances = /* @__PURE__ */ new WeakMap();
|
|
15662
16638
|
baseFileGenerator = new BaseFileGenerator();
|
|
15663
|
-
/** 사용자가
|
|
16639
|
+
/** 사용자가 만들거나 고친 `.base` 라 덮어쓰지 않았다고 이미 알린 경로 — 한 pull 이 같은 DB 를 여러 번 다시 쓴다. */
|
|
15664
16640
|
keptUserBases = /* @__PURE__ */ new Set();
|
|
15665
16641
|
sidecarGenerator = new SidecarGenerator();
|
|
15666
16642
|
/** Notion 에서 바꾼 제목으로 이름을 바꾼 행과 실제로 옮긴 차례 — pull 마감이 가져가 다른 노트의 링크를 고친다. */
|
|
@@ -15669,11 +16645,23 @@ var DatabaseSyncer = class {
|
|
|
15669
16645
|
renamed: [],
|
|
15670
16646
|
applied: []
|
|
15671
16647
|
};
|
|
16648
|
+
/** 볼트에서 사라져 되살린 행의 경로 — pull 마감이 가져가 화면에 알린다(D8). */
|
|
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();
|
|
15672
16658
|
/**
|
|
15673
16659
|
* 실제로 기록한 .base 경로/DB 제목 — placeholder 임베드 재작성(F22)의 SSOT. 이번 pull 이 조회하지
|
|
15674
16660
|
* 않은 DB 의 것도 남아 있다({@link DbBaseFiles}).
|
|
15675
16661
|
*/
|
|
15676
16662
|
baseFileInfo;
|
|
16663
|
+
/** DB 마다 지난 pull 이 본 속성 — Notion 에서 바꾼 속성 이름을 행 노트에 옮기는 근거(ADR-045). */
|
|
16664
|
+
schemaLedger;
|
|
15677
16665
|
get decorations() {
|
|
15678
16666
|
return this.decorationsCache ??= new PageDecorations(this.notionClient, this.imageHandler);
|
|
15679
16667
|
}
|
|
@@ -15739,12 +16727,14 @@ var DatabaseSyncer = class {
|
|
|
15739
16727
|
const schema = await this.notionClient.getDatabaseSchema(dbConfig.databaseId, meta);
|
|
15740
16728
|
this.propertyMapper.loadSchema(schema);
|
|
15741
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;
|
|
15742
16732
|
if (wholeDbInScope) {
|
|
15743
16733
|
const viewsConfig = await this.pullDatabaseViews(dbConfig);
|
|
15744
16734
|
await this.generateBaseFile(dbConfig, viewsConfig, meta, void 0, this.rangeEndsOf(pages));
|
|
15745
16735
|
}
|
|
15746
16736
|
let created = 0;
|
|
15747
|
-
let updated =
|
|
16737
|
+
let updated = followed.rekeyed.size;
|
|
15748
16738
|
let restored = 0;
|
|
15749
16739
|
const conflicts = [];
|
|
15750
16740
|
const failed = [];
|
|
@@ -15759,7 +16749,14 @@ var DatabaseSyncer = class {
|
|
|
15759
16749
|
for (const page of pages) {
|
|
15760
16750
|
try {
|
|
15761
16751
|
const record = this.stateDb.getByNotionId(page.id);
|
|
15762
|
-
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
|
+
);
|
|
15763
16760
|
if (action !== "skip") pending.push({ page, action });
|
|
15764
16761
|
else if (record && (wholeDbInScope || inAnyPathScope(record.obsidianPath, scope))) {
|
|
15765
16762
|
unchanged.push(page);
|
|
@@ -15778,7 +16775,7 @@ var DatabaseSyncer = class {
|
|
|
15778
16775
|
try {
|
|
15779
16776
|
const record = this.stateDb.getByNotionId(page.id);
|
|
15780
16777
|
const outcome = await withDeadline(
|
|
15781
|
-
() => action === "reform" && record ? this.
|
|
16778
|
+
() => action === "reform" && record ? this.rewriteStaleKeys(page, record, refreshedKeys(page)) : this.pullDatabasePage(page, dbConfig),
|
|
15782
16779
|
this.config.advanced.itemTimeoutMs,
|
|
15783
16780
|
`pull ${dbConfig.localFolder}/${page.id}`
|
|
15784
16781
|
);
|
|
@@ -15786,8 +16783,10 @@ var DatabaseSyncer = class {
|
|
|
15786
16783
|
created++;
|
|
15787
16784
|
if (outcome.action === "written") writtenPaths.push(outcome.path);
|
|
15788
16785
|
} else if (outcome.action === "written") {
|
|
15789
|
-
if (action === "restore")
|
|
15790
|
-
|
|
16786
|
+
if (action === "restore") {
|
|
16787
|
+
restored++;
|
|
16788
|
+
this.restoredRows.push(outcome.path);
|
|
16789
|
+
} else if (!followed.rekeyed.has(record.id)) updated++;
|
|
15791
16790
|
writtenPaths.push(outcome.path);
|
|
15792
16791
|
} else if (outcome.action === "conflict") {
|
|
15793
16792
|
conflicts.push(outcome.conflict);
|
|
@@ -15950,32 +16949,35 @@ var DatabaseSyncer = class {
|
|
|
15950
16949
|
* (틀린) 전제로 db-row 를 제외해 두었기에 양쪽 어디에도 복원 경로가 없는 상태였다.
|
|
15951
16950
|
* deleteSync 가 켜져 있으면 되살리지 않는다 — 지운 것은 Notion 에서도 지우라는 뜻이고, sync 는
|
|
15952
16951
|
* pull 을 먼저 돌려 되살리면 뒤이은 push 가 지울 것을 잃는다(detectMissingLocalFiles 와 같다).
|
|
15953
|
-
* · 예전 버전이 적은 날짜 모양이 남은
|
|
15954
|
-
* F-08).
|
|
16952
|
+
* · 예전 버전이 적은 날짜 모양이 남은 행, Notion 이 새로 만들거나 타입을 바꾼 속성의 값이 다른 행은 그 줄만
|
|
16953
|
+
* 고쳐 쓴다({@link staleKeysToRewrite}, F-08 · ADR-045).
|
|
16954
|
+
*
|
|
16955
|
+
* @param refreshed 이 행의 data source 에서 새로 생기거나 타입이 바뀐 속성의 키(ADR-045).
|
|
15955
16956
|
*/
|
|
15956
|
-
async rowAction(dbConfig, page, record, wholeDbInScope, scope) {
|
|
16957
|
+
async rowAction(dbConfig, page, record, wholeDbInScope, scope, refreshed) {
|
|
15957
16958
|
if (!wholeDbInScope && !(record && inAnyPathScope(record.obsidianPath, scope))) return "skip";
|
|
15958
16959
|
if (record && this.vaultFs.excludesPath?.(record.obsidianPath)) return "skip";
|
|
15959
16960
|
if (!record) return "pull";
|
|
15960
16961
|
const misplaced = !isDirectDbRowPath(dbConfig.localFolder, record.obsidianPath);
|
|
15961
16962
|
const changed = misplaced || this.remoteVerdict(record, page) !== "unchanged";
|
|
15962
|
-
if (!changed && await this.
|
|
16963
|
+
if (!changed && await this.staleKeysToRewrite(record, page, refreshed)) return "reform";
|
|
15963
16964
|
if (this.config.sync.deleteSync || await this.vaultFs.exists(record.obsidianPath)) {
|
|
15964
16965
|
return changed ? "pull" : "skip";
|
|
15965
16966
|
}
|
|
15966
16967
|
return "restore";
|
|
15967
16968
|
}
|
|
15968
16969
|
/**
|
|
15969
|
-
* 원격이 그대로인 행이지만 예전 버전이 적은 날짜 모양(`…Z` · `…+09:00` ·
|
|
15970
|
-
*
|
|
15971
|
-
*
|
|
16970
|
+
* 원격이 그대로인 행이지만 그 줄만 고쳐 쓸 키가 있는가 — 예전 버전이 적은 날짜 모양(`…Z` · `…+09:00` ·
|
|
16971
|
+
* `{start, end}`, F-08, {@link PropertyMapper.outdatedDateKeys}), 또는 Notion 이 새로 만들거나 타입을 바꾼
|
|
16972
|
+
* 속성의 값이 적힌 것과 다른 키(ADR-045 — 수식 열을 더하면 행마다 값이 생기지만 행의 수정 시각은 그대로다).
|
|
16973
|
+
* 고쳐 쓰면 사본도 새 값이 되어 다음 pull 은 건너뛴다.
|
|
15972
16974
|
*
|
|
15973
16975
|
* 지난 동기화 뒤 로컬을 고친 행은 고르지 않는다 — 로컬 편집을 지키느라 쓰지 않으니 pull 마다 고르기만
|
|
15974
16976
|
* 한다. 그 편집을 올린 뒤의 pull 이 고른다. 로컬 파일을 읽지 못하면 고르지 않는다.
|
|
15975
16977
|
*/
|
|
15976
|
-
async
|
|
16978
|
+
async staleKeysToRewrite(record, page, refreshed) {
|
|
15977
16979
|
const base = snapshotFrontmatter(record.baseSnapshot);
|
|
15978
|
-
if (base === null || this.
|
|
16980
|
+
if (base === null || this.staleKeys(base, rowProperties(page), refreshed).length === 0) {
|
|
15979
16981
|
return false;
|
|
15980
16982
|
}
|
|
15981
16983
|
try {
|
|
@@ -15984,25 +16986,37 @@ var DatabaseSyncer = class {
|
|
|
15984
16986
|
return false;
|
|
15985
16987
|
}
|
|
15986
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
|
+
}
|
|
15987
17001
|
/**
|
|
15988
|
-
*
|
|
15989
|
-
*
|
|
15990
|
-
*
|
|
17002
|
+
* 원격이 그대로인 행에서 {@link staleKeys} 의 키만 받은 값으로 고쳐 쓴다 — 본문 · 첨부는 다시 받지 않는다. 본문은
|
|
17003
|
+
* 한 글자도 바꾸지 않고, 다른 키의 줄은 그대로 둔다(키 단위로 나눠 읽을 수 없는 frontmatter 는 값만 지킨다 —
|
|
17004
|
+
* {@link mergeRowFrontmatter}).
|
|
15991
17005
|
*
|
|
15992
17006
|
* 예전에는 행을 통째로 다시 받았다. 행마다 본문을 읽어 요청이 늘고, 다시 그릴 때마다 달라지는 본문(빈
|
|
15993
17007
|
* 블록 · 첨부 이름)까지 바뀌었다 — 실볼트 사본에서 다시 받은 149행 중 99행.
|
|
15994
17008
|
*
|
|
15995
17009
|
* 고른 뒤 로컬을 고쳤으면 쓰지 않는다(`skipped`) — 이어지는 push 가 올린다.
|
|
15996
17010
|
*/
|
|
15997
|
-
async
|
|
17011
|
+
async rewriteStaleKeys(page, record, refreshed) {
|
|
15998
17012
|
const path = record.obsidianPath;
|
|
15999
17013
|
const local = await this.vaultFs.readFile(path);
|
|
16000
17014
|
if (computeHash(local) !== record.contentHash) return { action: "skipped", path };
|
|
16001
17015
|
const raw = rowProperties(page);
|
|
16002
|
-
const keys = this.
|
|
17016
|
+
const keys = this.staleKeys(splitFrontmatter(local).data, raw, refreshed);
|
|
16003
17017
|
const fresh = this.propertyMapper.fromNotionProperties(raw);
|
|
16004
17018
|
const pulled = Object.fromEntries(
|
|
16005
|
-
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]])
|
|
16006
17020
|
);
|
|
16007
17021
|
const lines = keys.length > 0 ? mergeRowFrontmatter(local, pulled, new Set(keys)) : null;
|
|
16008
17022
|
const content = lines === null ? null : replaceFrontmatterLines(local, lines);
|
|
@@ -16015,6 +17029,86 @@ var DatabaseSyncer = class {
|
|
|
16015
17029
|
});
|
|
16016
17030
|
return { action: "written", path };
|
|
16017
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
|
+
}
|
|
16018
17112
|
/** 이 DB 의 추적 행 가운데 이번 조회에 없던 것 — 지워졌는지는 아직 모른다. */
|
|
16019
17113
|
unlistedRows(dbConfig, pages, paths) {
|
|
16020
17114
|
const listed = new Set(pages.map((page) => normalizeNotionId(page.id)));
|
|
@@ -16126,7 +17220,7 @@ var DatabaseSyncer = class {
|
|
|
16126
17220
|
lastSynced: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16127
17221
|
views: [{ id: "default", name: "Table", type: "table" }]
|
|
16128
17222
|
};
|
|
16129
|
-
const
|
|
17223
|
+
const generated = this.baseFileGenerator.generateWithViews({
|
|
16130
17224
|
databaseId: dbConfig.databaseId,
|
|
16131
17225
|
databaseName: dbName,
|
|
16132
17226
|
schema: schemaFull,
|
|
@@ -16134,19 +17228,34 @@ var DatabaseSyncer = class {
|
|
|
16134
17228
|
folderPath: rowsFolder ?? dbConfig.localFolder,
|
|
16135
17229
|
rangeEnds
|
|
16136
17230
|
});
|
|
17231
|
+
const baseContent = generated.content;
|
|
16137
17232
|
const safeName = sanitizeFileName(dbName);
|
|
16138
17233
|
const basePath = `${dbConfig.localFolder}/${safeName}${BASE_SUFFIX}`;
|
|
16139
|
-
|
|
17234
|
+
const previous = this.baseFileInfo.get(dbConfig.databaseId);
|
|
17235
|
+
const existing = await this.vaultFs.readFile(basePath).catch(() => null);
|
|
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 };
|
|
16140
17246
|
if (!this.keptUserBases.has(basePath)) {
|
|
16141
17247
|
this.keptUserBases.add(basePath);
|
|
16142
17248
|
getLogger().info(
|
|
16143
17249
|
`[DB Sync] \uC9C1\uC811 \uB9CC\uB4E0 .base \uB77C Notion \uBCF4\uAE30\uB85C \uB36E\uC5B4\uC4F0\uC9C0 \uC54A\uC74C: ${basePath} \u2014 Notion \uBCF4\uAE30\uB97C \uBC1B\uC73C\uB824\uBA74 \uC774 \uD30C\uC77C\uC758 \uC774\uB984\uC744 \uBC14\uAFB8\uC138\uC694`
|
|
16144
17250
|
);
|
|
16145
17251
|
}
|
|
16146
|
-
} else {
|
|
17252
|
+
} else if (existing !== null && existing !== baseContent && editedSince(previous, basePath, existing)) {
|
|
17253
|
+
record = { ...previous, title: dbName };
|
|
17254
|
+
this.warnEditedBase(basePath);
|
|
17255
|
+
} else if (existing !== baseContent) {
|
|
16147
17256
|
await this.vaultFs.writeFile(basePath, baseContent);
|
|
16148
17257
|
}
|
|
16149
|
-
this.baseFileInfo.set(dbConfig.databaseId,
|
|
17258
|
+
this.baseFileInfo.set(dbConfig.databaseId, record);
|
|
16150
17259
|
getLogger().debug(`[DB Sync] .base \uD30C\uC77C \uC0DD\uC131: ${basePath}`);
|
|
16151
17260
|
await this.generateSidecar(
|
|
16152
17261
|
dbConfig,
|
|
@@ -16157,7 +17266,7 @@ var DatabaseSyncer = class {
|
|
|
16157
17266
|
meta.isInline,
|
|
16158
17267
|
await this.appearanceOf(meta, safeName)
|
|
16159
17268
|
);
|
|
16160
|
-
await this.cleanupStaleDbArtifacts(dbConfig.localFolder, safeName);
|
|
17269
|
+
await this.cleanupStaleDbArtifacts(dbConfig.localFolder, safeName, previous);
|
|
16161
17270
|
} catch (error) {
|
|
16162
17271
|
getLogger().warn(`[DB Sync] .base \uD30C\uC77C \uC0DD\uC131 \uC2E4\uD328 (\uACC4\uC18D \uC9C4\uD589):`, error);
|
|
16163
17272
|
}
|
|
@@ -16189,17 +17298,21 @@ var DatabaseSyncer = class {
|
|
|
16189
17298
|
...a.showTitle ? {} : { showTitle: false }
|
|
16190
17299
|
};
|
|
16191
17300
|
}
|
|
16192
|
-
/**
|
|
16193
|
-
|
|
16194
|
-
|
|
16195
|
-
|
|
17301
|
+
/** 볼트에서 고친 `.base` 를 덮어쓰거나 지우지 않았다고 한 번 알린다. */
|
|
17302
|
+
warnEditedBase(path) {
|
|
17303
|
+
this.heldBases.add(path);
|
|
17304
|
+
if (this.keptUserBases.has(path)) return;
|
|
17305
|
+
this.keptUserBases.add(path);
|
|
17306
|
+
getLogger().warn(
|
|
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`
|
|
17308
|
+
);
|
|
16196
17309
|
}
|
|
16197
17310
|
/**
|
|
16198
17311
|
* DB 제목이 바뀌면 `.base`/`.notion.json` 파일명(safeName)이 바뀌어 옛 이름의 산출물이
|
|
16199
17312
|
* 같은 폴더에 고아로 잔존한다(drift·부활 원인). DB 폴더 **직속**의 산출물 중 현재 이름이
|
|
16200
17313
|
* 아닌 것을 삭제해 멱등을 보장한다. 하위 폴더(중첩 DB)의 산출물은 보호한다.
|
|
16201
17314
|
*/
|
|
16202
|
-
async cleanupStaleDbArtifacts(localFolder, safeName) {
|
|
17315
|
+
async cleanupStaleDbArtifacts(localFolder, safeName, previous) {
|
|
16203
17316
|
try {
|
|
16204
17317
|
const keep = /* @__PURE__ */ new Set([
|
|
16205
17318
|
`${localFolder}/${safeName}${BASE_SUFFIX}`,
|
|
@@ -16208,7 +17321,13 @@ var DatabaseSyncer = class {
|
|
|
16208
17321
|
const files = await this.vaultFs.listNonMarkdownFiles();
|
|
16209
17322
|
const stale = selectStaleDbArtifacts(files, localFolder, keep);
|
|
16210
17323
|
for (const path of stale) {
|
|
16211
|
-
|
|
17324
|
+
const content = await this.vaultFs.readFile(path).catch(() => null);
|
|
17325
|
+
if (content === null || !isGeneratedDbArtifact(path, content)) continue;
|
|
17326
|
+
if (this.heldSidecars.has(path)) continue;
|
|
17327
|
+
if (editedSince(previous, path, content)) {
|
|
17328
|
+
this.warnEditedBase(path);
|
|
17329
|
+
continue;
|
|
17330
|
+
}
|
|
16212
17331
|
await this.vaultFs.deleteFile(path);
|
|
16213
17332
|
getLogger().info(`[DB Sync] \uACE0\uC544 DB \uC0B0\uCD9C\uBB3C \uC0AD\uC81C(rename): ${path}`);
|
|
16214
17333
|
}
|
|
@@ -16234,8 +17353,23 @@ var DatabaseSyncer = class {
|
|
|
16234
17353
|
const sidecarPath = `${dbConfig.localFolder}/${safeName}${SIDECAR_SUFFIX}`;
|
|
16235
17354
|
const serialized = this.sidecarGenerator.serialize(sidecar);
|
|
16236
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
|
+
});
|
|
16237
17372
|
if (existing === serialized) return;
|
|
16238
|
-
if (existing !== null && !isGeneratedDbArtifact(sidecarPath, existing)) return;
|
|
16239
17373
|
await this.vaultFs.writeFile(sidecarPath, serialized);
|
|
16240
17374
|
if (sidecar.degraded.length > 0) {
|
|
16241
17375
|
const unrep = sidecar.degraded.filter((d) => d.kind === "view-unrepresentable").length;
|
|
@@ -16386,6 +17520,12 @@ var DatabaseSyncer = class {
|
|
|
16386
17520
|
}
|
|
16387
17521
|
return (path) => this.stateDb.getByPath(path) ?? (untracked.has(path) ? { notionPageId: null } : null);
|
|
16388
17522
|
}
|
|
17523
|
+
/** 되살린 행의 경로를 가져가고 비운다. */
|
|
17524
|
+
takeRestoredRows() {
|
|
17525
|
+
const rows = this.restoredRows;
|
|
17526
|
+
this.restoredRows = [];
|
|
17527
|
+
return rows;
|
|
17528
|
+
}
|
|
16389
17529
|
/** 이름을 바꾼 행을 가져가고 비운다. */
|
|
16390
17530
|
takeRenamedRows() {
|
|
16391
17531
|
const rows = this.renamedRows;
|
|
@@ -16558,6 +17698,9 @@ var DatabaseSyncer = class {
|
|
|
16558
17698
|
function rowProperties(page) {
|
|
16559
17699
|
return page.properties ?? {};
|
|
16560
17700
|
}
|
|
17701
|
+
function editedSince(previous, path, content) {
|
|
17702
|
+
return previous?.basePath === path && previous.hash !== void 0 && computeHash(content) !== previous.hash;
|
|
17703
|
+
}
|
|
16561
17704
|
|
|
16562
17705
|
// src/audit/completeness.ts
|
|
16563
17706
|
function groupLocalRowsByDatabase(local) {
|
|
@@ -16654,6 +17797,40 @@ async function verifyPageCompleteness(remote, local, rootPageId) {
|
|
|
16654
17797
|
};
|
|
16655
17798
|
}
|
|
16656
17799
|
|
|
17800
|
+
// src/audit/nested-folder-pages.ts
|
|
17801
|
+
var ID_SUFFIX = new RegExp(
|
|
17802
|
+
` \\((?:${PAGE_ID_SUFFIX_LENGTHS.map((len) => `[0-9a-f]{${len}}`).join("|")})\\)$`
|
|
17803
|
+
);
|
|
17804
|
+
function folderOf3(record) {
|
|
17805
|
+
if (record.fileType === "db-row") return null;
|
|
17806
|
+
if (isFolderRecord(record)) return record.obsidianPath;
|
|
17807
|
+
const slash = record.obsidianPath.lastIndexOf("/");
|
|
17808
|
+
if (slash < 0) return null;
|
|
17809
|
+
const folder = record.obsidianPath.slice(0, slash);
|
|
17810
|
+
const stem = stripMarkdownExtension(record.obsidianPath.slice(slash + 1)).replace(ID_SUFFIX, "");
|
|
17811
|
+
return stem === folder.slice(folder.lastIndexOf("/") + 1) ? folder : null;
|
|
17812
|
+
}
|
|
17813
|
+
function findNestedFolderPages(records) {
|
|
17814
|
+
const byPage = /* @__PURE__ */ new Map();
|
|
17815
|
+
for (const record of records) {
|
|
17816
|
+
if (record.notionPageId !== null) byPage.set(compactNotionId(record.notionPageId), record);
|
|
17817
|
+
}
|
|
17818
|
+
const found2 = [];
|
|
17819
|
+
for (const inner of records) {
|
|
17820
|
+
if (inner.notionPageId === null || inner.notionParentId === null) continue;
|
|
17821
|
+
const folder = folderOf3(inner);
|
|
17822
|
+
if (folder === null) continue;
|
|
17823
|
+
const outer = byPage.get(compactNotionId(inner.notionParentId));
|
|
17824
|
+
if (!outer || outer === inner || folderOf3(outer) !== folder) continue;
|
|
17825
|
+
found2.push({
|
|
17826
|
+
folder,
|
|
17827
|
+
outer: { path: outer.obsidianPath, notionPageId: outer.notionPageId },
|
|
17828
|
+
inner: { path: inner.obsidianPath, notionPageId: inner.notionPageId }
|
|
17829
|
+
});
|
|
17830
|
+
}
|
|
17831
|
+
return found2.sort((a, b) => a.folder < b.folder ? -1 : a.folder > b.folder ? 1 : 0);
|
|
17832
|
+
}
|
|
17833
|
+
|
|
16657
17834
|
// src/conflict/merger.ts
|
|
16658
17835
|
function threeWayMerge(base, local, remote) {
|
|
16659
17836
|
const baseLines = base.split("\n");
|
|
@@ -16675,19 +17852,19 @@ function threeWayMerge(base, local, remote) {
|
|
|
16675
17852
|
}
|
|
16676
17853
|
const regionStart = first.baseStart;
|
|
16677
17854
|
let regionEnd = first.baseEnd;
|
|
16678
|
-
const
|
|
17855
|
+
const group2 = [first];
|
|
16679
17856
|
i++;
|
|
16680
17857
|
while (i < hunks.length) {
|
|
16681
17858
|
const next = hunks[i];
|
|
16682
17859
|
const overlaps = next.baseStart < regionEnd || regionEnd === regionStart && next.baseStart === regionStart;
|
|
16683
17860
|
if (!overlaps) break;
|
|
16684
17861
|
regionEnd = Math.max(regionEnd, next.baseEnd);
|
|
16685
|
-
|
|
17862
|
+
group2.push(next);
|
|
16686
17863
|
i++;
|
|
16687
17864
|
}
|
|
16688
17865
|
const baseRegion = baseLines.slice(regionStart, regionEnd);
|
|
16689
|
-
const localRegion = applyHunks(baseLines,
|
|
16690
|
-
const remoteRegion = applyHunks(baseLines,
|
|
17866
|
+
const localRegion = applyHunks(baseLines, group2, 0, regionStart, regionEnd);
|
|
17867
|
+
const remoteRegion = applyHunks(baseLines, group2, 2, regionStart, regionEnd);
|
|
16691
17868
|
if (arraysEqual(localRegion, baseRegion)) {
|
|
16692
17869
|
merged.push(...remoteRegion);
|
|
16693
17870
|
} else if (arraysEqual(remoteRegion, baseRegion)) {
|
|
@@ -16738,10 +17915,10 @@ function computeHunks(base, modified, side) {
|
|
|
16738
17915
|
emitGap(base.length, modified.length);
|
|
16739
17916
|
return hunks;
|
|
16740
17917
|
}
|
|
16741
|
-
function applyHunks(baseLines,
|
|
17918
|
+
function applyHunks(baseLines, group2, side, regionStart, regionEnd) {
|
|
16742
17919
|
const out = [];
|
|
16743
17920
|
let b = regionStart;
|
|
16744
|
-
for (const hunk of
|
|
17921
|
+
for (const hunk of group2) {
|
|
16745
17922
|
if (hunk.side !== side) continue;
|
|
16746
17923
|
if (hunk.baseStart > b) out.push(...baseLines.slice(b, hunk.baseStart));
|
|
16747
17924
|
out.push(...hunk.content);
|
|
@@ -17068,6 +18245,9 @@ var ScopedVaultFS = class {
|
|
|
17068
18245
|
async ensureFolder(path) {
|
|
17069
18246
|
return await this.base.ensureFolder(this.toBase(path, true));
|
|
17070
18247
|
}
|
|
18248
|
+
async removeEmptyFolder(path) {
|
|
18249
|
+
return await this.base.removeEmptyFolder?.(this.toBase(path, true)) ?? false;
|
|
18250
|
+
}
|
|
17071
18251
|
/** 맡은 노트만 읽는다 — 볼트 전체의 본문을 읽지 않는다. */
|
|
17072
18252
|
async listMarkdownFiles() {
|
|
17073
18253
|
const stats = await this.listMarkdownFileStats();
|
|
@@ -17145,7 +18325,7 @@ function nextFullReconcileAt(lastFullPullAt, intervalSec) {
|
|
|
17145
18325
|
const last = lastFullPullAt === null ? NaN : Date.parse(lastFullPullAt);
|
|
17146
18326
|
return Number.isFinite(last) ? new Date(last + intervalSec * 1e3).toISOString() : null;
|
|
17147
18327
|
}
|
|
17148
|
-
function remoteScanInfo(scan, lastFullPullAt, deleteSync) {
|
|
18328
|
+
function remoteScanInfo(scan, lastFullPullAt, deleteSync, deletionsChecked = false) {
|
|
17149
18329
|
return scan.kind === "full" ? {
|
|
17150
18330
|
kind: "full",
|
|
17151
18331
|
reason: scan.reason,
|
|
@@ -17156,94 +18336,1008 @@ function remoteScanInfo(scan, lastFullPullAt, deleteSync) {
|
|
|
17156
18336
|
kind: "incremental",
|
|
17157
18337
|
lastFullAt: scan.lastFullAt,
|
|
17158
18338
|
nextFullAt: scan.nextFullAt,
|
|
17159
|
-
deletionsDeferred: deleteSync
|
|
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}`);
|
|
18750
|
+
}
|
|
18751
|
+
return tokens;
|
|
18752
|
+
}
|
|
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
|
+
});
|
|
18916
|
+
}
|
|
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 } };
|
|
19016
|
+
}
|
|
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`);
|
|
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`);
|
|
19032
|
+
return {
|
|
19033
|
+
head: { property: decodePropertyId(prop.id) },
|
|
19034
|
+
type: prop.type,
|
|
19035
|
+
label: `\uC18D\uC131 '${ref.name}'`
|
|
17160
19036
|
};
|
|
17161
19037
|
}
|
|
17162
|
-
|
|
17163
|
-
|
|
17164
|
-
return selection.kind === "all" || selection.ids.has(compactNotionId(databaseId));
|
|
19038
|
+
function isPendingRange(filter) {
|
|
19039
|
+
return typeof filter["pending"] === "object" && filter["pending"] !== null;
|
|
17165
19040
|
}
|
|
17166
|
-
function
|
|
17167
|
-
|
|
17168
|
-
|
|
17169
|
-
const
|
|
17170
|
-
|
|
17171
|
-
|
|
17172
|
-
|
|
17173
|
-
|
|
17174
|
-
|
|
17175
|
-
|
|
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
|
+
}
|
|
17176
19057
|
}
|
|
19058
|
+
return out;
|
|
17177
19059
|
}
|
|
17178
|
-
function
|
|
17179
|
-
return
|
|
19060
|
+
function squash(text) {
|
|
19061
|
+
return text.replace(/\s+/g, "");
|
|
17180
19062
|
}
|
|
17181
|
-
|
|
17182
|
-
|
|
17183
|
-
|
|
17184
|
-
|
|
17185
|
-
|
|
17186
|
-
|
|
17187
|
-
|
|
17188
|
-
|
|
17189
|
-
|
|
17190
|
-
|
|
17191
|
-
|
|
17192
|
-
|
|
17193
|
-
|
|
17194
|
-
|
|
17195
|
-
|
|
17196
|
-
|
|
17197
|
-
|
|
17198
|
-
|
|
17199
|
-
|
|
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));
|
|
19067
|
+
}
|
|
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);
|
|
17200
19086
|
}
|
|
17201
|
-
|
|
17202
|
-
|
|
17203
|
-
|
|
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 };
|
|
17204
19186
|
}
|
|
17205
|
-
|
|
17206
|
-
|
|
17207
|
-
|
|
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 };
|
|
17208
19221
|
}
|
|
17209
|
-
|
|
17210
|
-
|
|
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) };
|
|
17211
19253
|
}
|
|
17212
|
-
|
|
17213
|
-
|
|
17214
|
-
|
|
17215
|
-
|
|
17216
|
-
|
|
17217
|
-
|
|
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);
|
|
17218
19302
|
}
|
|
17219
|
-
|
|
17220
|
-
|
|
17221
|
-
|
|
17222
|
-
|
|
17223
|
-
const syncing = this.syncing;
|
|
17224
|
-
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
|
+
);
|
|
17225
19307
|
}
|
|
17226
|
-
|
|
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
|
+
}
|
|
17227
19317
|
|
|
17228
19318
|
// src/sync/row-schema-cache.ts
|
|
17229
19319
|
var RowSchemaCache = class {
|
|
17230
|
-
constructor(loadSchema, wikilinkResolver, createMapper = () => new PropertyMapper(), loadPeople) {
|
|
19320
|
+
constructor(loadSchema, wikilinkResolver, createMapper = () => new PropertyMapper(), loadPeople, loadRowSource = async () => null) {
|
|
17231
19321
|
this.loadSchema = loadSchema;
|
|
17232
19322
|
this.wikilinkResolver = wikilinkResolver;
|
|
17233
19323
|
this.createMapper = createMapper;
|
|
17234
19324
|
this.loadPeople = loadPeople;
|
|
19325
|
+
this.loadRowSource = loadRowSource;
|
|
17235
19326
|
}
|
|
17236
19327
|
loadSchema;
|
|
17237
19328
|
wikilinkResolver;
|
|
17238
19329
|
createMapper;
|
|
17239
19330
|
loadPeople;
|
|
19331
|
+
loadRowSource;
|
|
17240
19332
|
mappers = /* @__PURE__ */ new Map();
|
|
19333
|
+
rowSources = /* @__PURE__ */ new Map();
|
|
17241
19334
|
people = null;
|
|
17242
|
-
|
|
17243
|
-
|
|
19335
|
+
/** @param dataSourceId 행이 사는 data source — 주면 그 소스의 스키마로 옮긴다. */
|
|
19336
|
+
mapperFor(databaseId, dataSourceId) {
|
|
19337
|
+
const key = `${compactNotionId(databaseId)}:${dataSourceId ? compactNotionId(dataSourceId) : ""}`;
|
|
17244
19338
|
const cached = this.mappers.get(key);
|
|
17245
19339
|
if (cached) return cached;
|
|
17246
|
-
const created = this.loadSchema(databaseId).then(async (schema) => {
|
|
19340
|
+
const created = this.loadSchema(databaseId, dataSourceId ?? void 0).then(async (schema) => {
|
|
17247
19341
|
const mapper = this.createMapper();
|
|
17248
19342
|
mapper.setWikilinkResolver(this.wikilinkResolver);
|
|
17249
19343
|
mapper.loadSchema(schema);
|
|
@@ -17256,8 +19350,24 @@ var RowSchemaCache = class {
|
|
|
17256
19350
|
});
|
|
17257
19351
|
return created;
|
|
17258
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
|
+
}
|
|
17259
19368
|
clear() {
|
|
17260
19369
|
this.mappers.clear();
|
|
19370
|
+
this.rowSources.clear();
|
|
17261
19371
|
this.people = null;
|
|
17262
19372
|
}
|
|
17263
19373
|
/** 사용자 목록을 못 읽으면 이름은 풀지 않는다 — id 로 적힌 사람만 보낸다(예전 동작). */
|
|
@@ -17539,8 +19649,7 @@ var InterruptedSyncRecovery = class {
|
|
|
17539
19649
|
if (isNotionObjectNotFound(error)) continue;
|
|
17540
19650
|
throw error;
|
|
17541
19651
|
}
|
|
17542
|
-
|
|
17543
|
-
if (inTrash || page.archived) continue;
|
|
19652
|
+
if (isTrashedOrArchived(page)) continue;
|
|
17544
19653
|
return page;
|
|
17545
19654
|
}
|
|
17546
19655
|
return null;
|
|
@@ -17559,8 +19668,7 @@ var InterruptedSyncRecovery = class {
|
|
|
17559
19668
|
});
|
|
17560
19669
|
for (const row of rows) {
|
|
17561
19670
|
if (this.stateDb.getByNotionId(row.id)) continue;
|
|
17562
|
-
|
|
17563
|
-
if (inTrash || row.archived) continue;
|
|
19671
|
+
if (isTrashedOrArchived(row)) continue;
|
|
17564
19672
|
return row;
|
|
17565
19673
|
}
|
|
17566
19674
|
return null;
|
|
@@ -17807,7 +19915,7 @@ var DatabaseDiscovery = class {
|
|
|
17807
19915
|
failed.push({
|
|
17808
19916
|
path: pathOf(parentPageId),
|
|
17809
19917
|
operation: "create",
|
|
17810
|
-
error: `DB ${
|
|
19918
|
+
error: `DB ${shortNotionId(dbId)} \uB97C \uC77D\uC9C0 \uBABB\uD568 \u2014 \uB2E4\uC74C pull \uC774 \uB2E4\uC2DC \uBCF8\uB2E4: ${error}`
|
|
17811
19919
|
});
|
|
17812
19920
|
};
|
|
17813
19921
|
const register = async (dbId, parentPageId) => {
|
|
@@ -17857,10 +19965,11 @@ var DatabaseDiscovery = class {
|
|
|
17857
19965
|
nextRetry.parents.push(...retry.parents);
|
|
17858
19966
|
nextRetry.dbs.push(...retry.dbs);
|
|
17859
19967
|
} else {
|
|
19968
|
+
const root = this.config.notion.rootPageId;
|
|
17860
19969
|
const scanIds = scanAll ? [
|
|
17861
|
-
|
|
19970
|
+
root,
|
|
17862
19971
|
...this.stateDb.getAll().filter((r) => r.notionPageId).map((r) => r.notionPageId)
|
|
17863
|
-
].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;
|
|
17864
19973
|
if (scanIds.length > 0) {
|
|
17865
19974
|
const { found: found2, unread } = await this.discoverChildDatabases(scanIds);
|
|
17866
19975
|
for (const { dbId, parentPageId } of found2) {
|
|
@@ -18059,7 +20168,7 @@ var DatabaseDiscovery = class {
|
|
|
18059
20168
|
}
|
|
18060
20169
|
let safeName;
|
|
18061
20170
|
if (dbTitle) {
|
|
18062
|
-
safeName = dbTitle
|
|
20171
|
+
safeName = databaseFolderName(dbTitle);
|
|
18063
20172
|
} else {
|
|
18064
20173
|
const parentName = parentFolder.split("/").pop() || "";
|
|
18065
20174
|
safeName = parentName ? `${parentName}-DB` : `db-${dbId.replace(/-/g, "").slice(0, 8)}`;
|
|
@@ -18152,6 +20261,12 @@ var DatabaseDiscovery = class {
|
|
|
18152
20261
|
return refs;
|
|
18153
20262
|
}
|
|
18154
20263
|
};
|
|
20264
|
+
function databaseFolderName(title) {
|
|
20265
|
+
return truncateBytes(
|
|
20266
|
+
title.replace(/[^a-zA-Z0-9가-힣\s_-]/g, "").replace(/\s+/g, "-"),
|
|
20267
|
+
FILE_SYSTEM_NAME_MAX_BYTES
|
|
20268
|
+
);
|
|
20269
|
+
}
|
|
18155
20270
|
|
|
18156
20271
|
// src/sync/folder-placement.ts
|
|
18157
20272
|
var FolderPlacement = class {
|
|
@@ -18975,7 +21090,7 @@ var RemoteRenameFollower = class {
|
|
|
18975
21090
|
/** 바꿀 것이 없으면 null. */
|
|
18976
21091
|
async followOne(record, dryRun) {
|
|
18977
21092
|
const page = await this.pageOf(record.notionPageId);
|
|
18978
|
-
if (page
|
|
21093
|
+
if (isTrashedOrArchived(page)) return null;
|
|
18979
21094
|
const title = this.notionClient.extractTitle(page);
|
|
18980
21095
|
if (!retitledInNotion(...await this.frontmatters(record), record.obsidianPath, title)) {
|
|
18981
21096
|
return null;
|
|
@@ -19116,7 +21231,7 @@ function rewriteTarget(target, source, context, kind) {
|
|
|
19116
21231
|
const shown = keepExtension ? destination : destination.slice(0, -".md".length);
|
|
19117
21232
|
let next;
|
|
19118
21233
|
if (resolved.relative) {
|
|
19119
|
-
next = relativePath(
|
|
21234
|
+
next = relativePath(folderOf5(source.to), shown);
|
|
19120
21235
|
if (!linkpath.startsWith("./") && next.startsWith("./")) next = next.slice(2);
|
|
19121
21236
|
} else if (style === "name") {
|
|
19122
21237
|
const name = baseName3(shown);
|
|
@@ -19138,14 +21253,14 @@ function resolveLinkpath(linkpath, sourcePath, files) {
|
|
|
19138
21253
|
const matches = byName(files, baseName3(candidate)).filter((file) => file.endsWith(suffix));
|
|
19139
21254
|
if (matches.length === 1) return found(matches[0], linkpath);
|
|
19140
21255
|
if (matches.length > 1) {
|
|
19141
|
-
const local = `${folderPrefix(
|
|
21256
|
+
const local = `${folderPrefix(folderOf5(sourcePath))}${candidate}`;
|
|
19142
21257
|
return matches.includes(local) ? found(local, linkpath) : null;
|
|
19143
21258
|
}
|
|
19144
21259
|
}
|
|
19145
21260
|
return null;
|
|
19146
21261
|
}
|
|
19147
21262
|
function resolveRelative(linkpath, sourcePath, files) {
|
|
19148
|
-
const joined = normalizeSegments(`${folderPrefix(
|
|
21263
|
+
const joined = normalizeSegments(`${folderPrefix(folderOf5(sourcePath))}${linkpath}`);
|
|
19149
21264
|
if (joined === null) return null;
|
|
19150
21265
|
for (const candidate of withExtension(joined)) {
|
|
19151
21266
|
if (files.has(candidate)) return { ...found(candidate, linkpath), relative: true };
|
|
@@ -19204,7 +21319,7 @@ function relativePath(from, to) {
|
|
|
19204
21319
|
function encodeMarkdownTarget(next, original) {
|
|
19205
21320
|
return original.includes("%") || next.includes(" ") ? next.replace(/ /g, "%20") : next;
|
|
19206
21321
|
}
|
|
19207
|
-
function
|
|
21322
|
+
function folderOf5(path) {
|
|
19208
21323
|
const slash = path.lastIndexOf("/");
|
|
19209
21324
|
return slash === -1 ? "" : path.slice(0, slash);
|
|
19210
21325
|
}
|
|
@@ -20155,15 +22270,15 @@ var DatabaseCreator = class {
|
|
|
20155
22270
|
*/
|
|
20156
22271
|
async addStatus(dataSourceId, status, folder) {
|
|
20157
22272
|
const groupOf = /* @__PURE__ */ new Map();
|
|
20158
|
-
for (const
|
|
20159
|
-
for (const option of
|
|
22273
|
+
for (const group2 of status.groups ?? []) {
|
|
22274
|
+
for (const option of group2.options) groupOf.set(option, group2.name);
|
|
20160
22275
|
}
|
|
20161
22276
|
const unknown = [];
|
|
20162
22277
|
const options = status.options.map((option) => {
|
|
20163
|
-
const
|
|
20164
|
-
if (
|
|
20165
|
-
if (NOTION_STATUS_GROUPS.includes(
|
|
20166
|
-
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})`);
|
|
20167
22282
|
return option;
|
|
20168
22283
|
});
|
|
20169
22284
|
await this.notionClient.updateDataSourceProperties(dataSourceId, {
|
|
@@ -20473,9 +22588,7 @@ var DatabaseCreator = class {
|
|
|
20473
22588
|
discovered.push({ databaseId, localFolder: folder, titleProperty: titleProperty2 });
|
|
20474
22589
|
this.stateDb.setMeta(DISCOVERED_DBS_META_KEY, JSON.stringify(discovered));
|
|
20475
22590
|
}
|
|
20476
|
-
|
|
20477
|
-
pending.add(compactNotionId(databaseId));
|
|
20478
|
-
this.stateDb.setMeta(PENDING_DATABASES_META_KEY, serializePendingDatabases(pending));
|
|
22591
|
+
addPendingDatabases(this.stateDb, [databaseId]);
|
|
20479
22592
|
}
|
|
20480
22593
|
async readSidecar(path) {
|
|
20481
22594
|
try {
|
|
@@ -20780,6 +22893,7 @@ var PagePusher = class {
|
|
|
20780
22893
|
* (ADR-017). relation 을 보내면 가리킨 행의 반대쪽 relation 이, 행을 만들면 그 행의 수식 · ID 가 바뀐다.
|
|
20781
22894
|
*/
|
|
20782
22895
|
markRemoteUnverified(pageIds) {
|
|
22896
|
+
const databases = [];
|
|
20783
22897
|
for (const pageId of new Set(pageIds)) {
|
|
20784
22898
|
const target = this.stateDb.getByNotionId(pageId);
|
|
20785
22899
|
if (!target?.notionLastEdited) continue;
|
|
@@ -20788,6 +22902,76 @@ var PagePusher = class {
|
|
|
20788
22902
|
lastEditedBy: null,
|
|
20789
22903
|
seenAt: null
|
|
20790
22904
|
});
|
|
22905
|
+
if (target.fileType === "db-row" && target.notionParentId) {
|
|
22906
|
+
databases.push(target.notionParentId);
|
|
22907
|
+
}
|
|
22908
|
+
}
|
|
22909
|
+
addPendingDatabases(this.stateDb, databases);
|
|
22910
|
+
}
|
|
22911
|
+
/** 이번 실행에서 볼트의 값을 올리지 않은 읽기 전용 속성 — 노트 경로별. */
|
|
22912
|
+
readOnlySkips = /* @__PURE__ */ new Map();
|
|
22913
|
+
/** 올리지 않은 읽기 전용 속성을 꺼낸다(목록은 비운다). */
|
|
22914
|
+
takeReadOnlySkips() {
|
|
22915
|
+
const skips = [...this.readOnlySkips].sort(([a], [b]) => a.localeCompare(b)).map(([path, properties]) => ({ path, properties }));
|
|
22916
|
+
this.readOnlySkips.clear();
|
|
22917
|
+
return skips;
|
|
22918
|
+
}
|
|
22919
|
+
/**
|
|
22920
|
+
* dry-run 이 알릴 읽기 전용 속성 — 실제 push 처럼 볼트에서 바꾼 행 속성(새 행은 모든 속성) 가운데 Notion 이 값을
|
|
22921
|
+
* 정하는 것. 원격은 읽지 않고 지난 동기화 사본과 견준다 — 원격도 바뀌었으면 실제 push 와 다를 수 있다. 사본이
|
|
22922
|
+
* 없거나 frontmatter · 스키마를 읽지 못하면 그 노트는 건너뛴다(실제 push 가 이유와 함께 알린다).
|
|
22923
|
+
*/
|
|
22924
|
+
async previewReadOnlySkips(changes) {
|
|
22925
|
+
const skips = [];
|
|
22926
|
+
for (const change of changes) {
|
|
22927
|
+
const row = this.previewRow(change);
|
|
22928
|
+
if (!row) continue;
|
|
22929
|
+
try {
|
|
22930
|
+
const current = parseFrontmatter(await this.vaultFs.readFile(change.path));
|
|
22931
|
+
const { changed, cleared } = diffRowProperties(row.base, current.data);
|
|
22932
|
+
const mapper = await this.rowSchemas.mapperFor(row.databaseId);
|
|
22933
|
+
const readOnly = mapper.readOnlyKeys(
|
|
22934
|
+
withoutDecorations([...Object.keys(changed), ...cleared])
|
|
22935
|
+
);
|
|
22936
|
+
if (readOnly.length > 0) skips.push({ path: change.path, properties: readOnly });
|
|
22937
|
+
} catch {
|
|
22938
|
+
continue;
|
|
22939
|
+
}
|
|
22940
|
+
}
|
|
22941
|
+
return skips.sort((a, b) => a.path.localeCompare(b.path));
|
|
22942
|
+
}
|
|
22943
|
+
/** dry-run 이 견줄 행 — 새 행은 기준 없이, 고친 행은 지난 동기화 사본과. 행이 아니거나 사본이 없으면 null. */
|
|
22944
|
+
previewRow(change) {
|
|
22945
|
+
if (change.type === "created") {
|
|
22946
|
+
const databaseId2 = this.placement.newRowDatabaseOf(change.path);
|
|
22947
|
+
return databaseId2 ? { databaseId: databaseId2, base: null } : null;
|
|
22948
|
+
}
|
|
22949
|
+
if (change.type !== "modified") return null;
|
|
22950
|
+
const record = this.stateDb.getByPath(change.path);
|
|
22951
|
+
const databaseId = record ? rowDatabaseOf(this.config, record) : null;
|
|
22952
|
+
const base = record && databaseId ? this.baseRowState(record) : null;
|
|
22953
|
+
return databaseId && base ? { databaseId, base: base.properties } : null;
|
|
22954
|
+
}
|
|
22955
|
+
/**
|
|
22956
|
+
* 보내지 않은 속성을 알린다. 읽기 전용 속성은 경고로 따로 모은다 — 다음 pull 이 볼트의 값을 Notion 값으로 되돌리니
|
|
22957
|
+
* 고친 사람이 알아야 한다. 나머지(DB 에 없는 속성 · 변환 불가 …)는 예전처럼 정보 로그다.
|
|
22958
|
+
*/
|
|
22959
|
+
reportUnsent(path, mapper, skipped, what) {
|
|
22960
|
+
const unsent = withoutDecorations(skipped);
|
|
22961
|
+
const readOnly = mapper.readOnlyKeys(unsent);
|
|
22962
|
+
if (readOnly.length > 0) {
|
|
22963
|
+
this.readOnlySkips.set(path, readOnly);
|
|
22964
|
+
getLogger().warn(
|
|
22965
|
+
`[Im-Nobsidian] \uC77D\uAE30 \uC804\uC6A9 \uC18D\uC131 ${readOnly.length}\uAC1C\uB294 Notion \uC774 \uAC12\uC744 \uC815\uD574 \uC62C\uB9AC\uC9C0 \uC54A\uC74C \u2014 \uB2E4\uC74C pull \uC774 Notion \uAC12\uC73C\uB85C \uB418\uB3CC\uB9B0\uB2E4: ${path} \u2014 ${readOnly.join(", ")}`
|
|
22966
|
+
);
|
|
22967
|
+
} else {
|
|
22968
|
+
this.readOnlySkips.delete(path);
|
|
22969
|
+
}
|
|
22970
|
+
const others = unsent.filter((key) => !readOnly.includes(key));
|
|
22971
|
+
if (others.length > 0) {
|
|
22972
|
+
getLogger().info(
|
|
22973
|
+
`[Im-Nobsidian] ${what} ${others.length}\uAC1C\uB294 \uBCF4\uB0B4\uC9C0 \uC54A\uC74C(DB \uC5D0 \uC5C6\uB294 \uC18D\uC131 \xB7 \uBE44\uC6B8 \uC218 \uC5C6\uB294 \uC18D\uC131 \xB7 \uBCC0\uD658 \uBD88\uAC00): ${path} \u2014 ${others.join(", ")}`
|
|
22974
|
+
);
|
|
20791
22975
|
}
|
|
20792
22976
|
}
|
|
20793
22977
|
/** relation 을 보내지 못한 행을 꺼낸다(목록은 비운다). */
|
|
@@ -20812,7 +22996,7 @@ var PagePusher = class {
|
|
|
20812
22996
|
const selectedPath = this.pipeline.selectPath(content);
|
|
20813
22997
|
if (selectedPath === "block-api") {
|
|
20814
22998
|
getLogger().warn(
|
|
20815
|
-
`[Im-Nobsidian] "${path}" contains block-api features (inline-db/column
|
|
22999
|
+
`[Im-Nobsidian] "${path}" contains block-api features (inline-db/column) \u2014 converted with reduced fidelity in v0.1.0`
|
|
20816
23000
|
);
|
|
20817
23001
|
}
|
|
20818
23002
|
const conversionResult = this.pipeline.convertToNotion(content, {
|
|
@@ -20956,7 +23140,7 @@ var PagePusher = class {
|
|
|
20956
23140
|
const updatePath = this.pipeline.selectPath(content);
|
|
20957
23141
|
if (updatePath === "block-api") {
|
|
20958
23142
|
getLogger().warn(
|
|
20959
|
-
`[Im-Nobsidian] "${path}" contains block-api features (inline-db/column
|
|
23143
|
+
`[Im-Nobsidian] "${path}" contains block-api features (inline-db/column) \u2014 converted with reduced fidelity in v0.1.0`
|
|
20960
23144
|
);
|
|
20961
23145
|
}
|
|
20962
23146
|
const conversionResult = this.pipeline.convertToNotion(content, {
|
|
@@ -20987,7 +23171,7 @@ var PagePusher = class {
|
|
|
20987
23171
|
bodyFingerprint = await this.drift.remoteBodyFingerprintOf(record.notionPageId);
|
|
20988
23172
|
}
|
|
20989
23173
|
const newTitle = this.changedPageTitle(record, content, path);
|
|
20990
|
-
const propsToUpdate = newTitle === null ? void 0 : titleProperty(newTitle);
|
|
23174
|
+
const propsToUpdate = newTitle === null ? void 0 : titleProperty(newTitle, pagePropertiesOf(remote));
|
|
20991
23175
|
const decorated = await this.decorations.push(
|
|
20992
23176
|
record.notionPageId,
|
|
20993
23177
|
path,
|
|
@@ -21164,22 +23348,18 @@ var PagePusher = class {
|
|
|
21164
23348
|
);
|
|
21165
23349
|
}
|
|
21166
23350
|
const title = noteTitle(current.data, path);
|
|
21167
|
-
const
|
|
23351
|
+
const source = await this.rowSchemas.rowSourceFor(databaseId);
|
|
23352
|
+
const mapper = await this.rowSchemas.mapperFor(databaseId, source);
|
|
21168
23353
|
const changes = diffRowProperties(null, current.data);
|
|
21169
23354
|
const { properties, skipped } = mapper.toNotionPropertyChanges(changes, title, {
|
|
21170
23355
|
current: current.data,
|
|
21171
23356
|
uploads: await this.uploadsFor(mapper, changes.changed, path)
|
|
21172
23357
|
});
|
|
21173
|
-
|
|
21174
|
-
if (unsentProperties.length > 0) {
|
|
21175
|
-
getLogger().info(
|
|
21176
|
-
`[Im-Nobsidian] \uC0C8 \uD589 \uC18D\uC131 ${unsentProperties.length}\uAC1C\uB294 \uBCF4\uB0B4\uC9C0 \uC54A\uC74C(DB \uC5D0 \uC5C6\uB294 \uC18D\uC131 \xB7 \uC77D\uAE30 \uC804\uC6A9 \xB7 \uBCC0\uD658 \uBD88\uAC00): ${path} \u2014 ${unsentProperties.join(", ")}`
|
|
21177
|
-
);
|
|
21178
|
-
}
|
|
23358
|
+
this.reportUnsent(path, mapper, skipped, "\uC0C8 \uD589 \uC18D\uC131");
|
|
21179
23359
|
const selectedPath = this.pipeline.selectPath(content);
|
|
21180
23360
|
if (selectedPath === "block-api") {
|
|
21181
23361
|
getLogger().warn(
|
|
21182
|
-
`[Im-Nobsidian] "${path}" contains block-api features (inline-db/column
|
|
23362
|
+
`[Im-Nobsidian] "${path}" contains block-api features (inline-db/column) \u2014 converted with reduced fidelity in v0.1.0`
|
|
21183
23363
|
);
|
|
21184
23364
|
}
|
|
21185
23365
|
const conversionResult = this.pipeline.convertToNotion(content, {
|
|
@@ -21225,7 +23405,8 @@ var PagePusher = class {
|
|
|
21225
23405
|
title,
|
|
21226
23406
|
conversionResult.content,
|
|
21227
23407
|
properties,
|
|
21228
|
-
{ path, frontmatter: current.data }
|
|
23408
|
+
{ path, frontmatter: current.data },
|
|
23409
|
+
source
|
|
21229
23410
|
);
|
|
21230
23411
|
const { page } = created;
|
|
21231
23412
|
const mapping = this.stateDb.upsert({
|
|
@@ -21357,8 +23538,8 @@ var PagePusher = class {
|
|
|
21357
23538
|
`frontmatter \uB97C \uC77D\uC9C0 \uBABB\uD574 \uD589\uC744 \uBCF4\uB0B4\uC9C0 \uC54A\uC74C (${path}): ${error instanceof Error ? error.message : String(error)}`
|
|
21358
23539
|
);
|
|
21359
23540
|
}
|
|
21360
|
-
const mapper = await this.rowSchemas.mapperFor(databaseId);
|
|
21361
23541
|
const remote = await this.livePage(record);
|
|
23542
|
+
const mapper = await this.rowSchemas.mapperFor(databaseId, dataSourceIdOf(remote));
|
|
21362
23543
|
const drift = options.overwriteRemote ? "none" : await this.drift.remoteDrift(record, remote);
|
|
21363
23544
|
const against = options.overwriteRemote ? this.remoteRowState(mapper, remote) : this.baseRowState(record);
|
|
21364
23545
|
const title = noteTitle(current.data, path);
|
|
@@ -21378,12 +23559,7 @@ var PagePusher = class {
|
|
|
21378
23559
|
...against === null || options.overwriteRemote ? {} : { uploads: await this.uploadsFor(mapper, changes.changed, path) }
|
|
21379
23560
|
}
|
|
21380
23561
|
);
|
|
21381
|
-
|
|
21382
|
-
if (unsentProperties.length > 0) {
|
|
21383
|
-
getLogger().info(
|
|
21384
|
-
`[Im-Nobsidian] \uD589 \uC18D\uC131 ${unsentProperties.length}\uAC1C\uB294 \uBCF4\uB0B4\uC9C0 \uC54A\uC74C(DB \uC5D0 \uC5C6\uB294 \uC18D\uC131 \xB7 \uC77D\uAE30 \uC804\uC6A9 \xB7 \uBE44\uC6B8 \uC218 \uC5C6\uB294 \uC18D\uC131 \xB7 \uBCC0\uD658 \uBD88\uAC00): ${path} \u2014 ${unsentProperties.join(", ")}`
|
|
21385
|
-
);
|
|
21386
|
-
}
|
|
23562
|
+
this.reportUnsent(path, mapper, skipped, "\uD589 \uC18D\uC131");
|
|
21387
23563
|
const bodyChanged = against?.body == null || against.body !== current.body;
|
|
21388
23564
|
const settles = drift === "none";
|
|
21389
23565
|
if (bodyChanged && !this.drift.overwritesRemote(record, options.overwriteRemote)) {
|
|
@@ -21395,7 +23571,7 @@ var PagePusher = class {
|
|
|
21395
23571
|
const selectedPath = this.pipeline.selectPath(content);
|
|
21396
23572
|
if (selectedPath === "block-api") {
|
|
21397
23573
|
getLogger().warn(
|
|
21398
|
-
`[Im-Nobsidian] "${path}" contains block-api features (inline-db/column
|
|
23574
|
+
`[Im-Nobsidian] "${path}" contains block-api features (inline-db/column) \u2014 converted with reduced fidelity in v0.1.0`
|
|
21399
23575
|
);
|
|
21400
23576
|
}
|
|
21401
23577
|
conversionResult = this.pipeline.convertToNotion(content, {
|
|
@@ -21473,6 +23649,8 @@ var PagePusher = class {
|
|
|
21473
23649
|
aliases: extractAliases(current.data)
|
|
21474
23650
|
});
|
|
21475
23651
|
});
|
|
23652
|
+
if (mapper.readOnlyKeys(withoutDecorations(skipped)).length > 0)
|
|
23653
|
+
this.markRemoteUnverified([pageId]);
|
|
21476
23654
|
}
|
|
21477
23655
|
/**
|
|
21478
23656
|
* 지난 동기화 시점의 행. 사본이 없거나 읽지 못하면 null — 호출측은 비교할 기준이 없다고
|
|
@@ -21517,7 +23695,10 @@ var PagePusher = class {
|
|
|
21517
23695
|
const title = change.title ? change.title(this.notionClient.extractTitle(remote)) : null;
|
|
21518
23696
|
if (newParent !== null) await this.notionClient.movePage(pageId, newParent);
|
|
21519
23697
|
if (title !== null) {
|
|
21520
|
-
written = await this.notionClient.updatePageProperties(
|
|
23698
|
+
written = await this.notionClient.updatePageProperties(
|
|
23699
|
+
pageId,
|
|
23700
|
+
titleProperty(title, pagePropertiesOf(remote))
|
|
23701
|
+
);
|
|
21521
23702
|
} else if (newParent !== null) {
|
|
21522
23703
|
written = await this.notionClient.getPage(pageId);
|
|
21523
23704
|
}
|
|
@@ -21533,7 +23714,8 @@ var PagePusher = class {
|
|
|
21533
23714
|
* 버린 맨 앞 `# H1` 을 호출측이 매핑을 적은 뒤 되살린다({@link restoreCreatedHeading}). 블록으로
|
|
21534
23715
|
* 보냈으면 null 이다 — 블록은 보낸 그대로 생긴다.
|
|
21535
23716
|
*/
|
|
21536
|
-
async pushCreatePage(parentId, parentType, title, markdownContent, properties, note) {
|
|
23717
|
+
async pushCreatePage(parentId, parentType, title, markdownContent, properties, note, dataSourceId) {
|
|
23718
|
+
const parentSource = dataSourceId ? { dataSourceId } : {};
|
|
21537
23719
|
const decorate2 = async (page2) => await this.decorations.push(page2.id, note.path, note.frontmatter, {}) ?? page2;
|
|
21538
23720
|
if (this.config.conversion.preferMarkdownApi !== false) {
|
|
21539
23721
|
const deferred = deferSyncedReferences(obsidianToNotionEnhanced(markdownContent));
|
|
@@ -21548,7 +23730,8 @@ var PagePusher = class {
|
|
|
21548
23730
|
parentType,
|
|
21549
23731
|
title,
|
|
21550
23732
|
markdown,
|
|
21551
|
-
properties
|
|
23733
|
+
properties,
|
|
23734
|
+
...parentSource
|
|
21552
23735
|
})
|
|
21553
23736
|
);
|
|
21554
23737
|
return { page: page2, markdown, syncedRefs: deferred.refs, syncedRefText: deferred.removed };
|
|
@@ -21559,7 +23742,8 @@ var PagePusher = class {
|
|
|
21559
23742
|
parentId,
|
|
21560
23743
|
parentType,
|
|
21561
23744
|
title,
|
|
21562
|
-
properties
|
|
23745
|
+
properties,
|
|
23746
|
+
...parentSource
|
|
21563
23747
|
})
|
|
21564
23748
|
);
|
|
21565
23749
|
if (blocks.length > 0) {
|
|
@@ -21863,6 +24047,8 @@ var PagePuller = class {
|
|
|
21863
24047
|
// 제목이 같은 새 페이지끼리 경로를 고르는 차례({@link orderCreates}) — 페이지 → 앞 차례가 경로를 고르면
|
|
21864
24048
|
// 풀리는 약속과 내 차례를 끝내는 함수.
|
|
21865
24049
|
claimTurns = /* @__PURE__ */ new Map();
|
|
24050
|
+
// Notion 에서 사라져 볼트에서 지운 페이지의 하위 폴더 — pull 이 마칠 때 비었으면 지운다({@link removeVacatedFolders}).
|
|
24051
|
+
vacatedFolders = /* @__PURE__ */ new Set();
|
|
21866
24052
|
decorationsCache;
|
|
21867
24053
|
get decorations() {
|
|
21868
24054
|
return this.decorationsCache ??= new PageDecorations(this.notionClient, this.imageHandler);
|
|
@@ -21877,6 +24063,35 @@ var PagePuller = class {
|
|
|
21877
24063
|
this.claimTurns.clear();
|
|
21878
24064
|
this._pullImageCount = 0;
|
|
21879
24065
|
this._pullFileCount = 0;
|
|
24066
|
+
this.vacatedFolders.clear();
|
|
24067
|
+
}
|
|
24068
|
+
/**
|
|
24069
|
+
* 이번 pull 이 지운 페이지의 폴더가 비었으면 지운다 — 안쪽부터, 그래야 바깥이 빈다. 예전에는 노트만 지우고
|
|
24070
|
+
* 폴더를 남겨 볼트에 빈 폴더가 쌓였다. 사용자 파일 · 숨김 파일이 하나라도 남은 폴더는 둔다
|
|
24071
|
+
* ({@link VaultFS.removeEmptyFolder}).
|
|
24072
|
+
*/
|
|
24073
|
+
async removeVacatedFolders() {
|
|
24074
|
+
const folders = [...this.vacatedFolders].sort(
|
|
24075
|
+
(a, b) => b.split("/").length - a.split("/").length
|
|
24076
|
+
);
|
|
24077
|
+
this.vacatedFolders.clear();
|
|
24078
|
+
for (const folder of folders) {
|
|
24079
|
+
try {
|
|
24080
|
+
await this.vaultFs.removeEmptyFolder?.(folder);
|
|
24081
|
+
} catch (error) {
|
|
24082
|
+
getLogger().warn(`[Im-Nobsidian] \uBE48 \uD3F4\uB354 ${folder} \uB97C \uC9C0\uC6B0\uC9C0 \uBABB\uD568:`, error);
|
|
24083
|
+
}
|
|
24084
|
+
}
|
|
24085
|
+
}
|
|
24086
|
+
/**
|
|
24087
|
+
* 원격에서 사라진 페이지의 하위 폴더를 적어 둔다 — 그 아래 페이지도 함께 사라졌으면 pull 끝에 빈다. 볼트 맨 위와
|
|
24088
|
+
* DB 폴더는 적지 않는다 — DB 는 페이지가 아니다.
|
|
24089
|
+
*/
|
|
24090
|
+
noteVacatedFolder(record) {
|
|
24091
|
+
const lookup = this.placement.folderLookup();
|
|
24092
|
+
if (isFolderRecord(record) && lookup.databaseAt(record.obsidianPath)) return;
|
|
24093
|
+
const folder = childFolderOf(record, lookup);
|
|
24094
|
+
if (folder !== "" && !lookup.databaseAt(folder)) this.vacatedFolders.add(folder);
|
|
21880
24095
|
}
|
|
21881
24096
|
/** 이번 pull 이 내려받은 이미지 수. */
|
|
21882
24097
|
get imageCount() {
|
|
@@ -22056,7 +24271,7 @@ var PagePuller = class {
|
|
|
22056
24271
|
if (!folderNote) return { action: "unchanged", path: record.obsidianPath };
|
|
22057
24272
|
record = folderNote;
|
|
22058
24273
|
}
|
|
22059
|
-
const remoteGone = page
|
|
24274
|
+
const remoteGone = isTrashedOrArchived(page);
|
|
22060
24275
|
if (remoteGone && !await this.vaultFs.exists(record.obsidianPath)) {
|
|
22061
24276
|
return { action: "unchanged", path: record.obsidianPath };
|
|
22062
24277
|
}
|
|
@@ -22152,6 +24367,7 @@ var PagePuller = class {
|
|
|
22152
24367
|
this.stateDb.delete(record.id);
|
|
22153
24368
|
this.stateDb.deleteWikilink(record.obsidianPath);
|
|
22154
24369
|
});
|
|
24370
|
+
if (this.config.sync.deleteSync) this.noteVacatedFolder(record);
|
|
22155
24371
|
return { action: "untracked", path: record.obsidianPath };
|
|
22156
24372
|
}
|
|
22157
24373
|
const outcome = await applyRemoteDeletion(this.stateDb, this.vaultFs, record, {
|
|
@@ -22159,6 +24375,7 @@ var PagePuller = class {
|
|
|
22159
24375
|
strategy: this.config.sync.conflictStrategy,
|
|
22160
24376
|
remoteChange: change
|
|
22161
24377
|
});
|
|
24378
|
+
if (outcome.action === "deleted" && this.config.sync.deleteSync) this.noteVacatedFolder(record);
|
|
22162
24379
|
return { ...outcome, path: record.obsidianPath };
|
|
22163
24380
|
}
|
|
22164
24381
|
/**
|
|
@@ -22314,8 +24531,7 @@ var PagePuller = class {
|
|
|
22314
24531
|
*/
|
|
22315
24532
|
async pullFolderRecord(record, page) {
|
|
22316
24533
|
const folder = record.obsidianPath;
|
|
22317
|
-
|
|
22318
|
-
if (remoteGone) return null;
|
|
24534
|
+
if (isTrashedOrArchived(page)) return null;
|
|
22319
24535
|
const { markdown } = await this.notionClient.getPageMarkdown(page.id);
|
|
22320
24536
|
if (!hasBodyBesidesChildren(markdown)) {
|
|
22321
24537
|
this.observation.record(record.id, page, remoteBodyFingerprint(markdown));
|
|
@@ -22647,6 +24863,13 @@ var PullPlanner = class {
|
|
|
22647
24863
|
const restoredModified = planned.filter(
|
|
22648
24864
|
(c) => c.type === "modified" && restoreIds.has(c.pageId)
|
|
22649
24865
|
).length;
|
|
24866
|
+
const restoredPaths = [
|
|
24867
|
+
...[
|
|
24868
|
+
...restoreChanges,
|
|
24869
|
+
...planned.filter((c) => c.type === "modified" && restoreIds.has(c.pageId))
|
|
24870
|
+
].map((change) => this.stateDb.getByNotionId(change.pageId)?.obsidianPath).filter((path) => path !== void 0),
|
|
24871
|
+
...rows.filter((row) => row.operation === "update" && row.restore).map((row) => row.path)
|
|
24872
|
+
];
|
|
22650
24873
|
const rowCount = (operation, restore = false) => rows.filter((row) => row.operation === operation && (row.restore ?? false) === restore).length;
|
|
22651
24874
|
return {
|
|
22652
24875
|
created: count("created") + rowCount("create"),
|
|
@@ -22655,6 +24878,7 @@ var PullPlanner = class {
|
|
|
22655
24878
|
// 복원은 updated 에 섞지 않는다 — dry-run 이 "수정 N건" 이라고만 말하면
|
|
22656
24879
|
// 사용자가 사라진 파일이 되살아난다는 사실을 미리 알 수 없다.
|
|
22657
24880
|
restored: restoreChanges.length + restoredModified + rowCount("update", true),
|
|
24881
|
+
...restoredPaths.length > 0 ? { restoredPaths } : {},
|
|
22658
24882
|
conflicts: [],
|
|
22659
24883
|
writtenPaths: [],
|
|
22660
24884
|
failed,
|
|
@@ -22729,6 +24953,42 @@ var PullPlanner = class {
|
|
|
22729
24953
|
}
|
|
22730
24954
|
};
|
|
22731
24955
|
|
|
24956
|
+
// src/sync/deletion-sweep.ts
|
|
24957
|
+
var DELETION_SWEEP_CURSOR_META_KEY = "deletion_sweep_cursor";
|
|
24958
|
+
var DELETION_SWEEP_BUDGET = 15;
|
|
24959
|
+
function sweepable(records, skip) {
|
|
24960
|
+
return records.filter((r) => r.notionPageId !== null && !skip.has(idOf(r))).sort((a, b) => idOf(a) < idOf(b) ? -1 : idOf(a) > idOf(b) ? 1 : 0);
|
|
24961
|
+
}
|
|
24962
|
+
function sweepBatch(records, cursor, skip, budget = DELETION_SWEEP_BUDGET) {
|
|
24963
|
+
const ordered = sweepable(records, skip);
|
|
24964
|
+
if (ordered.length === 0) return { batch: [], next: null, complete: true };
|
|
24965
|
+
if (ordered.length <= budget) {
|
|
24966
|
+
return { batch: ordered, next: idOf(ordered[ordered.length - 1]), complete: true };
|
|
24967
|
+
}
|
|
24968
|
+
const start = Math.max(0, cursor === null ? 0 : ordered.findIndex((r) => idOf(r) > cursor));
|
|
24969
|
+
const batch = Array.from({ length: budget }, (_, i) => ordered[(start + i) % ordered.length]);
|
|
24970
|
+
return { batch, next: idOf(batch[batch.length - 1]), complete: false };
|
|
24971
|
+
}
|
|
24972
|
+
function trackedDescendants(records, goneIds) {
|
|
24973
|
+
const gone = new Set(goneIds);
|
|
24974
|
+
const out = [];
|
|
24975
|
+
for (let grew = true; grew; ) {
|
|
24976
|
+
grew = false;
|
|
24977
|
+
for (const record of records) {
|
|
24978
|
+
if (record.notionPageId === null || record.notionParentId === null) continue;
|
|
24979
|
+
const id = idOf(record);
|
|
24980
|
+
if (gone.has(id) || !gone.has(compactNotionId(record.notionParentId))) continue;
|
|
24981
|
+
gone.add(id);
|
|
24982
|
+
out.push(record);
|
|
24983
|
+
grew = true;
|
|
24984
|
+
}
|
|
24985
|
+
}
|
|
24986
|
+
return out;
|
|
24987
|
+
}
|
|
24988
|
+
function idOf(record) {
|
|
24989
|
+
return compactNotionId(record.notionPageId);
|
|
24990
|
+
}
|
|
24991
|
+
|
|
22732
24992
|
// src/sync/remote-detector.ts
|
|
22733
24993
|
var RemoteDetector = class _RemoteDetector {
|
|
22734
24994
|
constructor(config, stateDb, notionClient, observation) {
|
|
@@ -22764,7 +25024,23 @@ var RemoteDetector = class _RemoteDetector {
|
|
|
22764
25024
|
// Notion 에서 부모가 바뀐 추적 페이지(정규화 id → 새 부모 페이지 id) — pull 이 볼트에서도 옮긴다(ADR-039).
|
|
22765
25025
|
// 옮겨도 수정 시각이 바뀌지 않아(실측) 변경으로는 보이지 않는다. 감지마다 다시 채운다.
|
|
22766
25026
|
_moved = /* @__PURE__ */ new Map();
|
|
25027
|
+
// 증분 감지가 search 로 살아 있다고 본 페이지(압축 id) — 삭제 확인이 다시 묻지 않는다.
|
|
25028
|
+
_seenAlive = /* @__PURE__ */ new Set();
|
|
25029
|
+
// 이번 감지에서 루트의 자식이 바뀌었을 수 있나 — {@link rootChanged}.
|
|
25030
|
+
_rootChanged = false;
|
|
25031
|
+
// 이번 감지가 삭제를 물은 뒤의 커서 — pull 이 마칠 때 적는다({@link saveDeletionSweep}). undefined 면 묻지 않았다.
|
|
25032
|
+
_sweepCursor;
|
|
25033
|
+
// 이번 감지가 추적 중인 것을 모두 물었나 — 그러면 증분이어도 미룬 삭제가 없다.
|
|
25034
|
+
_sweptAll = false;
|
|
22767
25035
|
/** 이번 감지에서 자식 페이지를 가진 것으로 본 페이지인가 — 폴더노트 판정의 1차 근거. */
|
|
25036
|
+
/**
|
|
25037
|
+
* 루트 바로 아래에 새 DB 가 생겼을 수 있나 — 증분 search 에 루트가 들었거나 전체 대조다. 루트는 받는 노트가 아니라
|
|
25038
|
+
* 본문을 읽지 않아, 루트 바로 아래 새 DB 는 `pull --force` 만 찾았다(2026-10-29 실측 — DB 를 만들면 루트의 수정
|
|
25039
|
+
* 시각이 바뀐다). 이때 DB 발견이 루트의 자식을 한 번 훑는다.
|
|
25040
|
+
*/
|
|
25041
|
+
rootChanged() {
|
|
25042
|
+
return this._rootChanged;
|
|
25043
|
+
}
|
|
22768
25044
|
hasChildPages(pageId) {
|
|
22769
25045
|
return this._childParentIds.has(normalizeNotionId(pageId));
|
|
22770
25046
|
}
|
|
@@ -22805,7 +25081,7 @@ var RemoteDetector = class _RemoteDetector {
|
|
|
22805
25081
|
throwIfAborted(signal);
|
|
22806
25082
|
try {
|
|
22807
25083
|
const page = await this.notionClient.getPage(childId);
|
|
22808
|
-
if (page
|
|
25084
|
+
if (isTrashedOrArchived(page)) continue;
|
|
22809
25085
|
const actualParent = await extractParentId(this.notionClient, page);
|
|
22810
25086
|
if (!actualParent || !notionIdsEqual(actualParent, parentId)) continue;
|
|
22811
25087
|
this._childParentIds.add(normalizeNotionId(parentId));
|
|
@@ -22868,10 +25144,84 @@ var RemoteDetector = class _RemoteDetector {
|
|
|
22868
25144
|
* @param options.databases 바뀐 DB 도 찾는다(pull · dry-run). 상태 확인은 DB 를 조회하지 않아 찾지 않는다.
|
|
22869
25145
|
*/
|
|
22870
25146
|
async detectRemote(scan, options) {
|
|
25147
|
+
this._seenAlive.clear();
|
|
25148
|
+
this._rootChanged = scan.kind === "full";
|
|
25149
|
+
this._sweepCursor = void 0;
|
|
25150
|
+
this._sweptAll = false;
|
|
22871
25151
|
const detection = scan.kind === "full" ? { changes: await this.detectRemoteChanges(options.signal), databaseIds: null } : await this.detectRemoteChangesIncremental(scan.since, options);
|
|
25152
|
+
if (options.databases) {
|
|
25153
|
+
detection.changes.push(
|
|
25154
|
+
...await this.sweepDeletions(scan, detection.changes, options.signal)
|
|
25155
|
+
);
|
|
25156
|
+
}
|
|
22872
25157
|
this.beginDetection(detection.changes);
|
|
22873
25158
|
return detection;
|
|
22874
25159
|
}
|
|
25160
|
+
/**
|
|
25161
|
+
* 이번 감지가 보지 못하는 삭제를 차례로 몇 개씩 원격에 묻는다({@link sweepBatch}). Notion 은 지운 것을
|
|
25162
|
+
* 목록으로 주지 않는다 — search · DB 조회 모두 휴지통을 돌려주지 않고 거르지도 못한다(ADR-044).
|
|
25163
|
+
*
|
|
25164
|
+
* - 증분 → 추적 중인 페이지 · 행 모두가 차례에 든다. search 가 살아 있다고 보인 것은 뺀다.
|
|
25165
|
+
* - 전체 대조 → 조회 필터를 단 DB 의 행만. 나머지는 순회 · DB 조회가 가른다. 필터를 단 DB 는 조회에 없는
|
|
25166
|
+
* 행이 필터 밖인지 지워진 것인지 몰라 가르지 않는다(DatabaseSyncer) — 여기서 하나씩 묻는다.
|
|
25167
|
+
*
|
|
25168
|
+
* 지워진 페이지 아래의 추적 페이지는 차례와 상관없이 함께 묻는다 — 부모를 휴지통에 넣으면 그 아래도 모두
|
|
25169
|
+
* 휴지통이다(실측 2026-10-28). 삭제는 deleteSync 일 때만, 첫 pull 이 아닐 때만 가른다 — 전체 대조와 같다.
|
|
25170
|
+
*/
|
|
25171
|
+
async sweepDeletions(scan, found2, signal) {
|
|
25172
|
+
if (!this.config.sync.deleteSync || !this.stateDb.getMeta(LAST_PULL_META_KEY)) return [];
|
|
25173
|
+
const records = this.stateDb.getAll();
|
|
25174
|
+
const candidates = scan.kind === "incremental" ? records : records.filter((r) => this.inFilteredDatabase(r));
|
|
25175
|
+
const skip = /* @__PURE__ */ new Set([...this._seenAlive, ...found2.map((c) => compactNotionId(c.pageId))]);
|
|
25176
|
+
const { batch, next, complete: complete2 } = sweepBatch(
|
|
25177
|
+
candidates,
|
|
25178
|
+
this.stateDb.getMeta(DELETION_SWEEP_CURSOR_META_KEY) || null,
|
|
25179
|
+
skip
|
|
25180
|
+
);
|
|
25181
|
+
this._sweepCursor = next;
|
|
25182
|
+
this._sweptAll = scan.kind === "incremental" && complete2;
|
|
25183
|
+
const deletions = [];
|
|
25184
|
+
const gone = /* @__PURE__ */ new Set();
|
|
25185
|
+
const ask = async (record) => {
|
|
25186
|
+
throwIfAborted(signal);
|
|
25187
|
+
const id = compactNotionId(record.notionPageId);
|
|
25188
|
+
try {
|
|
25189
|
+
if ((await remotePresence(this.notionClient, record.notionPageId)).kind !== "gone") return;
|
|
25190
|
+
} catch (error) {
|
|
25191
|
+
getLogger().warn(
|
|
25192
|
+
`[Im-Nobsidian] ${record.obsidianPath} \u2014 \uC6D0\uACA9\uC744 \uD655\uC778\uD558\uC9C0 \uBABB\uD574 \uC774\uBC88\uC5D0\uB294 \uC0AD\uC81C\uB97C \uAC00\uB974\uC9C0 \uC54A\uC74C:`,
|
|
25193
|
+
error
|
|
25194
|
+
);
|
|
25195
|
+
return;
|
|
25196
|
+
}
|
|
25197
|
+
gone.add(id);
|
|
25198
|
+
deletions.push(remoteDeletionChange(record));
|
|
25199
|
+
};
|
|
25200
|
+
for (const record of batch) await ask(record);
|
|
25201
|
+
const asked = new Set(batch.map((r) => compactNotionId(r.notionPageId)));
|
|
25202
|
+
for (const record of trackedDescendants(records, gone)) {
|
|
25203
|
+
const id = compactNotionId(record.notionPageId);
|
|
25204
|
+
if (!asked.has(id) && !skip.has(id)) await ask(record);
|
|
25205
|
+
}
|
|
25206
|
+
return deletions;
|
|
25207
|
+
}
|
|
25208
|
+
/** 조회 필터(`pullFilter`)를 단 설정 DB 의 행인가. */
|
|
25209
|
+
inFilteredDatabase(record) {
|
|
25210
|
+
if (record.fileType !== "db-row" || record.notionParentId === null) return false;
|
|
25211
|
+
return (this.config.notion.databases ?? []).some(
|
|
25212
|
+
(db) => db.pullFilter !== void 0 && notionIdsEqual(db.databaseId, record.notionParentId)
|
|
25213
|
+
);
|
|
25214
|
+
}
|
|
25215
|
+
/** 이번 감지가 추적 중인 페이지 · 행을 모두 원격에 물었나 — 결과의 `deletionsDeferred` 를 가른다. */
|
|
25216
|
+
deletionsChecked() {
|
|
25217
|
+
return this._sweptAll;
|
|
25218
|
+
}
|
|
25219
|
+
/** 삭제를 물은 자리를 적는다 — 다음 pull 이 이어서 묻는다. 경로를 좁힌 pull · dry-run 은 부르지 않는다. */
|
|
25220
|
+
saveDeletionSweep() {
|
|
25221
|
+
if (this._sweepCursor !== void 0) {
|
|
25222
|
+
this.stateDb.setMeta(DELETION_SWEEP_CURSOR_META_KEY, this._sweepCursor ?? "");
|
|
25223
|
+
}
|
|
25224
|
+
}
|
|
22875
25225
|
/**
|
|
22876
25226
|
* 전체 대조 — 루트 아래 페이지를 모두 훑고 추적 중인데 목록에 없는 페이지를 원격에 물어 삭제를
|
|
22877
25227
|
* 가른다. 취소하면 {@link OperationAbortedError} 를 던진다 — 다 훑지 못한 목록으로 삭제를 가르지
|
|
@@ -23012,9 +25362,8 @@ var RemoteDetector = class _RemoteDetector {
|
|
|
23012
25362
|
}
|
|
23013
25363
|
/**
|
|
23014
25364
|
* 증분 원격 변경 감지 — `since` 이후 수정된 페이지만 search 로 받아 created/modified 만
|
|
23015
|
-
* 만든다.
|
|
23016
|
-
*
|
|
23017
|
-
* 전체 대조는 주기마다 돈다(ADR-027 — 예전에는 deleteSync 가 켜져 있으면 매번 돌았다. I10).
|
|
25365
|
+
* 만든다. search API 는 in_trash/archived 페이지를 반환하지 않아 삭제는 여기서 보이지 않는다 —
|
|
25366
|
+
* 차례로 묻는 삭제 확인({@link sweepDeletions})과 주기마다 도는 전체 대조(ADR-027)가 가른다.
|
|
23018
25367
|
*
|
|
23019
25368
|
* 바뀐 행 · 새 행 · 스키마를 고친 DB 도 모은다 — pull 은 그 DB 만 조회한다. 새 행은 페이지로 받지
|
|
23020
25369
|
* 않는다: DB 조회가 행 속성과 함께 받는다. 그래서 부모를 묻지 않는다(행마다 요청 1회 이상 아낀다).
|
|
@@ -23032,13 +25381,17 @@ var RemoteDetector = class _RemoteDetector {
|
|
|
23032
25381
|
const recentPages = await this.notionClient.searchRecentPages(searchSince, options.signal);
|
|
23033
25382
|
const untracked = [];
|
|
23034
25383
|
for (const page of recentPages) {
|
|
25384
|
+
this._seenAlive.add(compactNotionId(page.id));
|
|
23035
25385
|
const record = this.stateDb.getByNotionId(page.id);
|
|
23036
25386
|
if (!record) {
|
|
23037
25387
|
if (page.parentDatabaseId) {
|
|
23038
25388
|
databaseIds.add(compactNotionId(page.parentDatabaseId));
|
|
23039
25389
|
continue;
|
|
23040
25390
|
}
|
|
23041
|
-
if (notionIdsEqual(page.id, this.config.notion.rootPageId))
|
|
25391
|
+
if (notionIdsEqual(page.id, this.config.notion.rootPageId)) {
|
|
25392
|
+
this._rootChanged = true;
|
|
25393
|
+
continue;
|
|
25394
|
+
}
|
|
23042
25395
|
throwIfAborted(options.signal);
|
|
23043
25396
|
try {
|
|
23044
25397
|
const parentId = await extractParentId(this.notionClient, page);
|
|
@@ -23185,6 +25538,9 @@ function isRerenderable(record) {
|
|
|
23185
25538
|
}
|
|
23186
25539
|
|
|
23187
25540
|
// src/sync/missing-local-files.ts
|
|
25541
|
+
function restoredWhenMissing(record) {
|
|
25542
|
+
return record.notionPageId !== null && !isFolderRecord(record) && (record.fileType === "file" || record.fileType === "folder-note" || record.fileType === "db-row");
|
|
25543
|
+
}
|
|
23188
25544
|
async function detectMissingLocalFiles(config, stateDb, vaultFs, paths) {
|
|
23189
25545
|
if (config.sync.deleteSync) return [];
|
|
23190
25546
|
const records = stateDb.getAll();
|
|
@@ -23288,7 +25644,7 @@ function draftFolderDatabase(input) {
|
|
|
23288
25644
|
let index = 0;
|
|
23289
25645
|
for (const [name, list] of values) {
|
|
23290
25646
|
if (NON_PROPERTY_KEYS.has(name)) continue;
|
|
23291
|
-
if (
|
|
25647
|
+
if (isRangeEnd2(name, values)) continue;
|
|
23292
25648
|
if (name === DEFAULT_TITLE_PROPERTY) {
|
|
23293
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" });
|
|
23294
25650
|
continue;
|
|
@@ -23405,7 +25761,7 @@ function collectValues(notes) {
|
|
|
23405
25761
|
}
|
|
23406
25762
|
return out;
|
|
23407
25763
|
}
|
|
23408
|
-
function
|
|
25764
|
+
function isRangeEnd2(name, values) {
|
|
23409
25765
|
if (!name.endsWith(RANGE_END_SUFFIX)) return false;
|
|
23410
25766
|
const start = values.get(name.slice(0, -RANGE_END_SUFFIX.length));
|
|
23411
25767
|
return start !== void 0 && start.length > 0 && start.every(isFrontmatterDate);
|
|
@@ -23463,11 +25819,11 @@ ${text}
|
|
|
23463
25819
|
skipped.push({ what: "\uBCF4\uAE30", reason: ".base \uB97C \uC77D\uC9C0 \uBABB\uD588\uB2E4" });
|
|
23464
25820
|
return [];
|
|
23465
25821
|
}
|
|
23466
|
-
const
|
|
23467
|
-
for (const p of properties)
|
|
25822
|
+
const idOf2 = /* @__PURE__ */ new Map();
|
|
25823
|
+
for (const p of properties) idOf2.set(p.type === "title" ? BASES_TITLE : p.name, p.id);
|
|
23468
25824
|
const columnId = (raw) => {
|
|
23469
25825
|
if (typeof raw !== "string") return void 0;
|
|
23470
|
-
return
|
|
25826
|
+
return idOf2.get(raw) ?? idOf2.get(raw.replace(/^note\./, ""));
|
|
23471
25827
|
};
|
|
23472
25828
|
const views = [];
|
|
23473
25829
|
const list = Array.isArray(parsed.views) ? parsed.views : [];
|
|
@@ -23590,11 +25946,26 @@ var SyncOrchestrator = class {
|
|
|
23590
25946
|
propertyMapper.setWikilinkResolver(wikilinkResolver);
|
|
23591
25947
|
this.notionClient.setWikilinkResolver(wikilinkResolver);
|
|
23592
25948
|
this.rowSchemas = new RowSchemaCache(
|
|
23593
|
-
(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
|
+
},
|
|
23594
25958
|
wikilinkResolver,
|
|
23595
25959
|
() => PropertyMapper.fromConfig(config),
|
|
23596
|
-
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
|
+
}
|
|
23597
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);
|
|
23598
25969
|
this.drift = new RemoteDriftChecker(
|
|
23599
25970
|
config,
|
|
23600
25971
|
stateDb,
|
|
@@ -23692,6 +26063,12 @@ var SyncOrchestrator = class {
|
|
|
23692
26063
|
databaseSyncer;
|
|
23693
26064
|
/** DB 행 push 용 — DB 마다 스키마를 읽은 매퍼. 실행마다 비운다(S-01). */
|
|
23694
26065
|
rowSchemas;
|
|
26066
|
+
/** 볼트에서 바꾼 DB 속성 이름을 Notion 의 열 이름으로(ADR-045). */
|
|
26067
|
+
vaultRenames;
|
|
26068
|
+
/** 볼트에서 고친 사이드카의 속성을 Notion DB 의 열로(ADR-045). */
|
|
26069
|
+
sidecarEdits;
|
|
26070
|
+
/** 볼트에서 고친 `.base` 의 보기를 Notion 보기로(ADR-046). */
|
|
26071
|
+
viewEdits;
|
|
23695
26072
|
// 이번 실행이 원격을 보는 기준(N-05) — push · pull · status 가 시작할 때 정한다
|
|
23696
26073
|
// ({@link RunObservation.begin}). 원격 판정과 관측 기록이 같은 값을 쓴다.
|
|
23697
26074
|
observation;
|
|
@@ -23762,6 +26139,7 @@ var SyncOrchestrator = class {
|
|
|
23762
26139
|
this.rowSchemas.clear();
|
|
23763
26140
|
this.placement.resetUnpreparedFolders();
|
|
23764
26141
|
this.databaseCreator.reset();
|
|
26142
|
+
this.pusher.takeReadOnlySkips();
|
|
23765
26143
|
if (!options?.dryRun) await this.recovery.recoverInterruptedPushOps();
|
|
23766
26144
|
const plan = await this.planner.planLocalChanges(await this.vaultFs.listMarkdownFileStats());
|
|
23767
26145
|
const view = this.planner.localView(plan);
|
|
@@ -23772,20 +26150,31 @@ var SyncOrchestrator = class {
|
|
|
23772
26150
|
const excludeSet = options?.excludePaths ? new Set(options.excludePaths) : null;
|
|
23773
26151
|
const filtered = (options?.paths ? eligible.filter((c) => inAnyPathScope(c.path, options.paths)) : eligible).filter((c) => !excludeSet || !excludeSet.has(c.path));
|
|
23774
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));
|
|
23775
26158
|
if (filtered.length === 0 && folderMoves.length === 0) {
|
|
23776
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);
|
|
23777
26162
|
return {
|
|
23778
26163
|
created: 0,
|
|
23779
26164
|
updated: 0,
|
|
23780
26165
|
deleted: 0,
|
|
23781
26166
|
moved: 0,
|
|
23782
|
-
failed: [],
|
|
23783
|
-
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 } : {}
|
|
23784
26171
|
};
|
|
23785
26172
|
}
|
|
23786
26173
|
const refusedCreates = this.placement.refusedCreates(filtered, view.lookup);
|
|
23787
26174
|
const refusedMoves = this.placement.refusedMoves(filtered, view);
|
|
23788
26175
|
const refusedFailures = [
|
|
26176
|
+
...sidecarPlan.refused,
|
|
26177
|
+
...viewPlan.refused,
|
|
23789
26178
|
...[...refusedCreates].map(([path, error]) => ({
|
|
23790
26179
|
path,
|
|
23791
26180
|
operation: "create",
|
|
@@ -23799,9 +26188,14 @@ var SyncOrchestrator = class {
|
|
|
23799
26188
|
if (error) refusedFailures.push({ path: move.to, operation: "move", error });
|
|
23800
26189
|
else movableFolders.push(move);
|
|
23801
26190
|
}
|
|
23802
|
-
const
|
|
26191
|
+
const placeable2 = filtered.filter(
|
|
23803
26192
|
(c) => !refusedCreates.has(c.path) && !refusedMoves.has(c.path)
|
|
23804
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 }));
|
|
23805
26199
|
if (options?.dryRun) {
|
|
23806
26200
|
const planned = this.config.sync.deleteSync ? applicable : applicable.filter((c) => c.type !== "deleted");
|
|
23807
26201
|
const items = [
|
|
@@ -23815,6 +26209,8 @@ var SyncOrchestrator = class {
|
|
|
23815
26209
|
view.lookup,
|
|
23816
26210
|
!options?.paths
|
|
23817
26211
|
);
|
|
26212
|
+
const unsentDeletions2 = applicable.filter((change) => this.unsentDeletion(change)).map((change) => change.path);
|
|
26213
|
+
const readOnlySkipped = await this.pusher.previewReadOnlySkips(applicable);
|
|
23818
26214
|
return {
|
|
23819
26215
|
createdDatabases: newDatabases.size,
|
|
23820
26216
|
created: count("create"),
|
|
@@ -23822,15 +26218,32 @@ var SyncOrchestrator = class {
|
|
|
23822
26218
|
deleted: count("delete"),
|
|
23823
26219
|
moved: count("move"),
|
|
23824
26220
|
failed: refusedFailures,
|
|
23825
|
-
duration: Date.now() - startTime
|
|
26221
|
+
duration: Date.now() - startTime,
|
|
26222
|
+
...unsentDeletions2.length > 0 ? { unsentDeletions: unsentDeletions2 } : {},
|
|
26223
|
+
...readOnlySkipped.length > 0 ? { readOnlySkipped } : {},
|
|
26224
|
+
...plannedRenames.length > 0 ? { renamedProperties: plannedRenames } : {},
|
|
26225
|
+
...plannedEdits.length > 0 ? { schemaEdits: plannedEdits } : {},
|
|
26226
|
+
...plannedViews.length > 0 ? { viewEdits: plannedViews } : {}
|
|
23826
26227
|
};
|
|
23827
26228
|
}
|
|
23828
26229
|
this.stateDb.setMeta(PUSH_IN_PROGRESS_META_KEY, "true");
|
|
23829
26230
|
await this.observation.resolveBotUserId();
|
|
23830
26231
|
const counts = { created: 0, updated: 0, deleted: 0, moved: 0 };
|
|
23831
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
|
+
);
|
|
26244
|
+
const unsentDeletions = [];
|
|
23832
26245
|
let completed = 0;
|
|
23833
|
-
const total = movableFolders.length +
|
|
26246
|
+
const total = movableFolders.length + pushing.length;
|
|
23834
26247
|
counts.moved += await this.pusher.pushFolderMoves(
|
|
23835
26248
|
movableFolders,
|
|
23836
26249
|
failed,
|
|
@@ -23856,6 +26269,7 @@ var SyncOrchestrator = class {
|
|
|
23856
26269
|
case "deleted": {
|
|
23857
26270
|
const propagated = await this.pusher.pushDelete(change.path);
|
|
23858
26271
|
if (propagated) counts.deleted++;
|
|
26272
|
+
else if (this.unsentDeletion(change)) unsentDeletions.push(change.path);
|
|
23859
26273
|
break;
|
|
23860
26274
|
}
|
|
23861
26275
|
}
|
|
@@ -23877,14 +26291,10 @@ var SyncOrchestrator = class {
|
|
|
23877
26291
|
retryQueue.push(change);
|
|
23878
26292
|
}
|
|
23879
26293
|
};
|
|
23880
|
-
const pushedEarly = await this.placement.prepareFolders(
|
|
23881
|
-
applicable,
|
|
23882
|
-
firstAttempt,
|
|
23883
|
-
!options?.paths
|
|
23884
|
-
);
|
|
26294
|
+
const pushedEarly = await this.placement.prepareFolders(pushing, firstAttempt, !options?.paths);
|
|
23885
26295
|
await this.completeDatabaseSchemas();
|
|
23886
26296
|
await runPool(
|
|
23887
|
-
|
|
26297
|
+
pushing.filter((c) => !pushedEarly.has(c.path)),
|
|
23888
26298
|
firstAttempt,
|
|
23889
26299
|
{ concurrency: this.config.advanced.concurrency, signal: options?.signal }
|
|
23890
26300
|
);
|
|
@@ -24011,9 +26421,23 @@ var SyncOrchestrator = class {
|
|
|
24011
26421
|
deleted: counts.deleted,
|
|
24012
26422
|
moved: counts.moved,
|
|
24013
26423
|
failed,
|
|
24014
|
-
duration: Date.now() - startTime
|
|
26424
|
+
duration: Date.now() - startTime,
|
|
26425
|
+
readOnlySkipped: this.pusher.takeReadOnlySkips(),
|
|
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 } : {}
|
|
24015
26430
|
};
|
|
24016
26431
|
}
|
|
26432
|
+
/**
|
|
26433
|
+
* 볼트에서 지운 이 노트를 Notion 에서 지우지 않아 다음 pull 이 되살리나 — deleteSync 가 꺼져 있고 pull 이
|
|
26434
|
+
* 되살리는 레코드일 때. 예전에는 대기로 적기만 하고 알리지 않아, 지운 파일이 말없이 돌아왔다(D8).
|
|
26435
|
+
*/
|
|
26436
|
+
unsentDeletion(change) {
|
|
26437
|
+
if (change.type !== "deleted" || this.config.sync.deleteSync) return false;
|
|
26438
|
+
const record = this.stateDb.getByPath(change.path);
|
|
26439
|
+
return !!record && restoredWhenMissing(record);
|
|
26440
|
+
}
|
|
24017
26441
|
/**
|
|
24018
26442
|
* 만든 DB 에 relation · rollup · formula 를 붙이고, 행 스키마를 다시 읽게 한 뒤 보기를 만든다. 실패는
|
|
24019
26443
|
* 다음 push 가 이어 붙인다 — 행은 그대로 올린다.
|
|
@@ -24086,6 +26510,10 @@ var SyncOrchestrator = class {
|
|
|
24086
26510
|
this.assertConnectionScope();
|
|
24087
26511
|
this.puller.beginPull();
|
|
24088
26512
|
this.rowSchemas.clear();
|
|
26513
|
+
this.databaseSyncer.takeRestoredRows();
|
|
26514
|
+
this.databaseSyncer.takeSchemaFollowed();
|
|
26515
|
+
this.databaseSyncer.takeHeldSidecars();
|
|
26516
|
+
this.databaseSyncer.takeHeldBases();
|
|
24089
26517
|
const emptyResult = {
|
|
24090
26518
|
created: 0,
|
|
24091
26519
|
updated: 0,
|
|
@@ -24118,6 +26546,7 @@ var SyncOrchestrator = class {
|
|
|
24118
26546
|
this.discovery.clearInlineRefs();
|
|
24119
26547
|
const conflicts = [];
|
|
24120
26548
|
const writtenPaths = [];
|
|
26549
|
+
const restoredPaths = [];
|
|
24121
26550
|
const excluded = [];
|
|
24122
26551
|
const failed = [];
|
|
24123
26552
|
let detected = [];
|
|
@@ -24161,7 +26590,12 @@ var SyncOrchestrator = class {
|
|
|
24161
26590
|
pendingDatabases
|
|
24162
26591
|
);
|
|
24163
26592
|
const finalize = async (created, updated, deleted, restored = 0) => {
|
|
26593
|
+
await this.puller.removeVacatedFolders();
|
|
24164
26594
|
const renamedRows = this.databaseSyncer.takeRenamedRows();
|
|
26595
|
+
restoredPaths.push(...this.databaseSyncer.takeRestoredRows());
|
|
26596
|
+
const schemaFollowed = this.databaseSyncer.takeSchemaFollowed();
|
|
26597
|
+
const heldSidecars = this.databaseSyncer.takeHeldSidecars();
|
|
26598
|
+
const heldBases = this.databaseSyncer.takeHeldBases();
|
|
24165
26599
|
const relinked = await this.linkUpdater.apply([...relocated.applied, ...renamedRows.applied]);
|
|
24166
26600
|
const linkCount = writtenPaths.length > 0 ? await this.resolveNotionLinks(writtenPaths) : 0;
|
|
24167
26601
|
if (!options?.paths) {
|
|
@@ -24177,6 +26611,7 @@ var SyncOrchestrator = class {
|
|
|
24177
26611
|
this.stateDb.setMeta(LAST_FULL_PULL_META_KEY, startedAt);
|
|
24178
26612
|
}
|
|
24179
26613
|
this.detector.savePendingDatabases(ledger);
|
|
26614
|
+
this.detector.saveDeletionSweep();
|
|
24180
26615
|
}
|
|
24181
26616
|
this.stateDb.setMeta(LAST_SYNC_META_KEY, (/* @__PURE__ */ new Date()).toISOString());
|
|
24182
26617
|
this.stateDb.setMeta(PULL_IN_PROGRESS_META_KEY, "");
|
|
@@ -24185,6 +26620,10 @@ var SyncOrchestrator = class {
|
|
|
24185
26620
|
updated,
|
|
24186
26621
|
deleted,
|
|
24187
26622
|
restored,
|
|
26623
|
+
...restoredPaths.length > 0 ? { restoredPaths } : {},
|
|
26624
|
+
...schemaFollowed.length > 0 ? { schemaFollowed } : {},
|
|
26625
|
+
...heldSidecars.length > 0 ? { heldSidecars } : {},
|
|
26626
|
+
...heldBases.length > 0 ? { heldBases } : {},
|
|
24188
26627
|
conflicts,
|
|
24189
26628
|
writtenPaths,
|
|
24190
26629
|
failed,
|
|
@@ -24196,7 +26635,8 @@ var SyncOrchestrator = class {
|
|
|
24196
26635
|
...remoteScanInfo(
|
|
24197
26636
|
scan,
|
|
24198
26637
|
this.stateDb.getMeta(LAST_FULL_PULL_META_KEY),
|
|
24199
|
-
this.config.sync.deleteSync
|
|
26638
|
+
this.config.sync.deleteSync,
|
|
26639
|
+
this.detector.deletionsChecked()
|
|
24200
26640
|
),
|
|
24201
26641
|
skippedDatabases: ledger.skipped
|
|
24202
26642
|
},
|
|
@@ -24247,6 +26687,10 @@ var SyncOrchestrator = class {
|
|
|
24247
26687
|
);
|
|
24248
26688
|
return {
|
|
24249
26689
|
...plan,
|
|
26690
|
+
remoteScan: plan.remoteScan && {
|
|
26691
|
+
...plan.remoteScan,
|
|
26692
|
+
deletionsDeferred: plan.remoteScan.deletionsDeferred && !this.detector.deletionsChecked()
|
|
26693
|
+
},
|
|
24250
26694
|
moved: relocated.moved,
|
|
24251
26695
|
moveSkipped: relocated.skipped,
|
|
24252
26696
|
excluded: [...exclusions.values()]
|
|
@@ -24294,6 +26738,7 @@ var SyncOrchestrator = class {
|
|
|
24294
26738
|
conflicts,
|
|
24295
26739
|
{
|
|
24296
26740
|
forceRediscovery: options?.force === true,
|
|
26741
|
+
rescanRoot: this.detector.rootChanged(),
|
|
24297
26742
|
paths: options?.paths,
|
|
24298
26743
|
progress: rowProgress,
|
|
24299
26744
|
ledger,
|
|
@@ -24340,8 +26785,10 @@ var SyncOrchestrator = class {
|
|
|
24340
26785
|
conflicts.push(outcome.conflict);
|
|
24341
26786
|
} else if (outcome.action === "written") {
|
|
24342
26787
|
writtenPaths.push(outcome.path);
|
|
24343
|
-
if (restoreIds.has(change.pageId))
|
|
24344
|
-
|
|
26788
|
+
if (restoreIds.has(change.pageId)) {
|
|
26789
|
+
counts.restored++;
|
|
26790
|
+
restoredPaths.push(outcome.path);
|
|
26791
|
+
} else counts.updated++;
|
|
24345
26792
|
}
|
|
24346
26793
|
return {
|
|
24347
26794
|
path: outcome.path,
|
|
@@ -24569,7 +27016,12 @@ var SyncOrchestrator = class {
|
|
|
24569
27016
|
this.stateDb,
|
|
24570
27017
|
this.config.notion.rootPageId
|
|
24571
27018
|
);
|
|
24572
|
-
return {
|
|
27019
|
+
return {
|
|
27020
|
+
databases,
|
|
27021
|
+
pages,
|
|
27022
|
+
nestedFolderPages: findNestedFolderPages(this.stateDb.getAll()),
|
|
27023
|
+
complete: databases.complete && (pages?.complete ?? true)
|
|
27024
|
+
};
|
|
24573
27025
|
}
|
|
24574
27026
|
async fetch() {
|
|
24575
27027
|
return this.gate.run("fetch", () => this.executeFetch());
|
|
@@ -24713,14 +27165,14 @@ function toVaultPath(relativePath2, separator) {
|
|
|
24713
27165
|
function compileAll(patterns) {
|
|
24714
27166
|
return patterns.flatMap((raw) => {
|
|
24715
27167
|
const pattern = normalizePattern(raw);
|
|
24716
|
-
return pattern === null ? [] : [
|
|
27168
|
+
return pattern === null ? [] : [compile2(pattern)];
|
|
24717
27169
|
});
|
|
24718
27170
|
}
|
|
24719
27171
|
function normalizePattern(raw) {
|
|
24720
27172
|
const pattern = raw.trim().replace(/\\/g, "/").replace(/^(?:\.\/|\/)+/, "").replace(/\/+$/, "");
|
|
24721
27173
|
return pattern === "" ? null : pattern;
|
|
24722
27174
|
}
|
|
24723
|
-
function
|
|
27175
|
+
function compile2(pattern) {
|
|
24724
27176
|
if (pattern === "**" || pattern === "**/*") return { kind: "all" };
|
|
24725
27177
|
if (!/[*?]/.test(pattern)) return { kind: "prefix", prefix: pattern };
|
|
24726
27178
|
if (pattern.endsWith("/**") && !/[*?]/.test(pattern.slice(0, -3))) {
|
|
@@ -24814,6 +27266,19 @@ var NodeVaultFS = class {
|
|
|
24814
27266
|
const fullPath = join(this.rootPath, path);
|
|
24815
27267
|
await unlink(fullPath);
|
|
24816
27268
|
}
|
|
27269
|
+
/** `rmdir` 는 비지 않은 폴더를 지우지 않는다(ENOTEMPTY) — 숨김 파일도 그대로 둔다. */
|
|
27270
|
+
async removeEmptyFolder(path) {
|
|
27271
|
+
try {
|
|
27272
|
+
await rmdir(join(this.rootPath, path));
|
|
27273
|
+
return true;
|
|
27274
|
+
} catch (error) {
|
|
27275
|
+
const code = error.code;
|
|
27276
|
+
if (code === "ENOTEMPTY" || code === "EEXIST" || code === "ENOENT" || code === "ENOTDIR") {
|
|
27277
|
+
return false;
|
|
27278
|
+
}
|
|
27279
|
+
throw error;
|
|
27280
|
+
}
|
|
27281
|
+
}
|
|
24817
27282
|
async moveFile(from, to) {
|
|
24818
27283
|
const fromPath = join(this.rootPath, from);
|
|
24819
27284
|
const toPath = join(this.rootPath, to);
|
|
@@ -25772,6 +28237,9 @@ var FilteredVaultFS = class {
|
|
|
25772
28237
|
ensureFolder(path) {
|
|
25773
28238
|
return this.inner.ensureFolder(path);
|
|
25774
28239
|
}
|
|
28240
|
+
async removeEmptyFolder(path) {
|
|
28241
|
+
return await this.inner.removeEmptyFolder?.(path) ?? false;
|
|
28242
|
+
}
|
|
25775
28243
|
getFileStat(path) {
|
|
25776
28244
|
return this.inner.getFileStat(path);
|
|
25777
28245
|
}
|
|
@@ -25940,7 +28408,7 @@ var SEP_RE = new RegExp(`^[\\t ]*${COLUMN_SEP_SOURCE}[\\t ]*$`);
|
|
|
25940
28408
|
var FENCE_RE = /^[\t ]*(`{3,}|~{3,})/;
|
|
25941
28409
|
function readColumnLayout(lines) {
|
|
25942
28410
|
const places = new Array(lines.length);
|
|
25943
|
-
let
|
|
28411
|
+
let group2 = -1;
|
|
25944
28412
|
let fence = null;
|
|
25945
28413
|
let open = null;
|
|
25946
28414
|
const close = (end) => {
|
|
@@ -25950,7 +28418,7 @@ function readColumnLayout(lines) {
|
|
|
25950
28418
|
const until = columns[index + 1]?.line ?? end;
|
|
25951
28419
|
for (let line = column.line; line < until; line++) {
|
|
25952
28420
|
places[line] = {
|
|
25953
|
-
group,
|
|
28421
|
+
group: group2,
|
|
25954
28422
|
index,
|
|
25955
28423
|
count: columns.length,
|
|
25956
28424
|
...column.ratio === void 0 ? {} : { ratio: column.ratio },
|
|
@@ -25972,7 +28440,7 @@ function readColumnLayout(lines) {
|
|
|
25972
28440
|
}
|
|
25973
28441
|
if (START_RE.test(line)) {
|
|
25974
28442
|
close(i);
|
|
25975
|
-
|
|
28443
|
+
group2++;
|
|
25976
28444
|
open = { start: i, columns: [] };
|
|
25977
28445
|
return;
|
|
25978
28446
|
}
|
|
@@ -25998,6 +28466,6 @@ function formatRatio(ratio) {
|
|
|
25998
28466
|
return String(Math.round(ratio * 100) / 100);
|
|
25999
28467
|
}
|
|
26000
28468
|
|
|
26001
|
-
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, formatUnifiedDiff, generateColorCSS, generateId, getLogger, getNotionBgColor, getNotionColor, getVisibleProperties, groupEntries, hasColumnLayout, hasDesignMarker, inAnyPathScope, isInternalPath, isMarkdownPath, isRemoteDeletion, lineDiff, lintRenderedMarkdown, matchesPathScope, normalizeNotionId, normalizeVaultPath, noteNameFromPath, notionIdFromInput, notionIdsEqual, operationLabel, parseDesignColor, parseIgnorePatterns, pendingWalBytes, readColumnLayout, readDesignLine, resolvePropertyName, sanitizeFileName, 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 };
|
|
26002
28470
|
//# sourceMappingURL=index.js.map
|
|
26003
28471
|
//# sourceMappingURL=index.js.map
|