@carlonicora/nextjs-jsonapi 6.12.2 → 6.13.1
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/{ApiData-DPKNfY-9.d.mts → ApiData-Be8yxCOX.d.mts} +2 -0
- package/dist/{ApiData-DPKNfY-9.d.ts → ApiData-Be8yxCOX.d.ts} +2 -0
- package/dist/{BlockNoteEditor-7M5QW2YQ.js → BlockNoteEditor-UBVEUFNR.js} +34 -22
- package/dist/BlockNoteEditor-UBVEUFNR.js.map +1 -0
- package/dist/{BlockNoteEditor-75ED2CN2.mjs → BlockNoteEditor-VXMDSQAA.mjs} +20 -8
- package/dist/BlockNoteEditor-VXMDSQAA.mjs.map +1 -0
- package/dist/{BlockNoteViewer-WGCDB7IJ.js → BlockNoteViewer-2XYASA5Y.js} +8 -5
- package/dist/BlockNoteViewer-2XYASA5Y.js.map +1 -0
- package/dist/{BlockNoteViewer-MTGS6YJV.mjs → BlockNoteViewer-NBW2H6LC.mjs} +7 -4
- package/dist/{BlockNoteViewer-MTGS6YJV.mjs.map → BlockNoteViewer-NBW2H6LC.mjs.map} +1 -1
- package/dist/{JsonApiRequest-A66XK5WT.mjs → JsonApiRequest-F6TMJ42G.mjs} +2 -2
- package/dist/JsonApiRequest-WISJZSDE.js +26 -0
- package/dist/{JsonApiRequest-FZGPLNUF.js.map → JsonApiRequest-WISJZSDE.js.map} +1 -1
- package/dist/billing/index.js +499 -494
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +13 -8
- package/dist/billing/index.mjs.map +1 -1
- package/dist/chunk-27LQDGNP.mjs +317 -0
- package/dist/chunk-27LQDGNP.mjs.map +1 -0
- package/dist/{chunk-KSYQEKJX.js → chunk-2SV4SS6N.js} +14 -14
- package/dist/{chunk-KSYQEKJX.js.map → chunk-2SV4SS6N.js.map} +1 -1
- package/dist/{chunk-4FNGWALA.mjs → chunk-47BBY5KA.mjs} +6 -4
- package/dist/{chunk-4FNGWALA.mjs.map → chunk-47BBY5KA.mjs.map} +1 -1
- package/dist/chunk-6JUCIWDC.mjs +570 -0
- package/dist/chunk-6JUCIWDC.mjs.map +1 -0
- package/dist/chunk-7DBJTNSU.mjs +1049 -0
- package/dist/chunk-7DBJTNSU.mjs.map +1 -0
- package/dist/chunk-7JZHPMGW.mjs +704 -0
- package/dist/chunk-7JZHPMGW.mjs.map +1 -0
- package/dist/{chunk-L6PYFQTO.js → chunk-7ZHTPFFT.js} +16 -14
- package/dist/chunk-7ZHTPFFT.js.map +1 -0
- package/dist/chunk-B6JGKDP2.js +214 -0
- package/dist/chunk-B6JGKDP2.js.map +1 -0
- package/dist/{chunk-3YW35YHQ.mjs → chunk-BH6JWIKI.mjs} +5 -5
- package/dist/chunk-BZ5J2FIV.js +570 -0
- package/dist/chunk-BZ5J2FIV.js.map +1 -0
- package/dist/{chunk-V5MI7RNB.mjs → chunk-DGWOVYEC.mjs} +2 -2
- package/dist/chunk-DOCIFE6A.mjs +212 -0
- package/dist/chunk-DOCIFE6A.mjs.map +1 -0
- package/dist/chunk-GB6DBFYY.js +25 -0
- package/dist/chunk-GB6DBFYY.js.map +1 -0
- package/dist/{chunk-EWCY6QJA.mjs → chunk-JJMMT24D.mjs} +37 -3
- package/dist/chunk-JJMMT24D.mjs.map +1 -0
- package/dist/{chunk-JTPRJNT2.js → chunk-KMW7UNL2.js} +38 -3
- package/dist/chunk-KMW7UNL2.js.map +1 -0
- package/dist/chunk-MHB36MYE.js +317 -0
- package/dist/chunk-MHB36MYE.js.map +1 -0
- package/dist/{chunk-FF4EIQ6H.js → chunk-MOGEGWXW.js} +1039 -1680
- package/dist/chunk-MOGEGWXW.js.map +1 -0
- package/dist/{chunk-RXQYEEKZ.mjs → chunk-N4RZZKL5.mjs} +58 -25
- package/dist/chunk-N4RZZKL5.mjs.map +1 -0
- package/dist/{chunk-AA52ORW2.mjs → chunk-P445QGOC.mjs} +61 -702
- package/dist/chunk-P445QGOC.mjs.map +1 -0
- package/dist/chunk-PV4Z5W6V.js +212 -0
- package/dist/chunk-PV4Z5W6V.js.map +1 -0
- package/dist/chunk-PXDUJ47N.js +1049 -0
- package/dist/chunk-PXDUJ47N.js.map +1 -0
- package/dist/{chunk-V3YDVJ63.js → chunk-PYRZCKC2.js} +1209 -1176
- package/dist/chunk-PYRZCKC2.js.map +1 -0
- package/dist/{chunk-GHE4VT2R.js → chunk-QKNGZDSQ.js} +2 -2
- package/dist/{chunk-GHE4VT2R.js.map → chunk-QKNGZDSQ.js.map} +1 -1
- package/dist/chunk-W62MHDWM.js +704 -0
- package/dist/chunk-W62MHDWM.js.map +1 -0
- package/dist/chunk-XXP3TQHF.mjs +25 -0
- package/dist/chunk-XXP3TQHF.mjs.map +1 -0
- package/dist/chunk-YRKIFYOE.mjs +214 -0
- package/dist/chunk-YRKIFYOE.mjs.map +1 -0
- package/dist/{chunk-O3PDL2CV.js → chunk-Z4POQWFG.js} +40 -6
- package/dist/chunk-Z4POQWFG.js.map +1 -0
- package/dist/{chunk-PI56A4DG.mjs → chunk-ZLTOUZN2.mjs} +37 -2
- package/dist/chunk-ZLTOUZN2.mjs.map +1 -0
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +14 -9
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +24 -19
- package/dist/components/index.d.mts +7 -1
- package/dist/components/index.d.ts +7 -1
- package/dist/components/index.js +11 -7
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +10 -6
- package/dist/contexts/index.d.mts +1 -1
- package/dist/contexts/index.d.ts +1 -1
- package/dist/contexts/index.js +11 -7
- package/dist/contexts/index.js.map +1 -1
- package/dist/contexts/index.mjs +10 -6
- package/dist/core/index.d.mts +128 -2
- package/dist/core/index.d.ts +128 -2
- package/dist/core/index.js +17 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +22 -9
- package/dist/email-automations/index.js +101 -96
- package/dist/email-automations/index.js.map +1 -1
- package/dist/email-automations/index.mjs +13 -8
- package/dist/email-automations/index.mjs.map +1 -1
- package/dist/features/analytics/index.js +128 -123
- package/dist/features/analytics/index.js.map +1 -1
- package/dist/features/analytics/index.mjs +13 -8
- package/dist/features/analytics/index.mjs.map +1 -1
- package/dist/features/analytics/server.js +11 -8
- package/dist/features/analytics/server.js.map +1 -1
- package/dist/features/analytics/server.mjs +9 -6
- package/dist/features/analytics/server.mjs.map +1 -1
- package/dist/features/blog/index.js +158 -153
- package/dist/features/blog/index.js.map +1 -1
- package/dist/features/blog/index.mjs +14 -9
- package/dist/features/blog/index.mjs.map +1 -1
- package/dist/features/blog/server.js +30 -26
- package/dist/features/blog/server.js.map +1 -1
- package/dist/features/blog/server.mjs +10 -6
- package/dist/features/blog/server.mjs.map +1 -1
- package/dist/features/help/index.js +44 -40
- package/dist/features/help/index.js.map +1 -1
- package/dist/features/help/index.mjs +10 -6
- package/dist/features/help/index.mjs.map +1 -1
- package/dist/features/tokenusage/index.js +134 -129
- package/dist/features/tokenusage/index.js.map +1 -1
- package/dist/features/tokenusage/index.mjs +13 -8
- package/dist/features/tokenusage/index.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +18 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +24 -11
- package/dist/lifecycle-4GUOTB46.js +23 -0
- package/dist/lifecycle-4GUOTB46.js.map +1 -0
- package/dist/lifecycle-LKGPRBS7.mjs +23 -0
- package/dist/local-transport-7H3SPZSA.js +15 -0
- package/dist/local-transport-7H3SPZSA.js.map +1 -0
- package/dist/local-transport-QYFKA6AN.mjs +15 -0
- package/dist/local-transport-QYFKA6AN.mjs.map +1 -0
- package/dist/offline/index.d.mts +607 -0
- package/dist/offline/index.d.ts +607 -0
- package/dist/offline/index.js +1052 -0
- package/dist/offline/index.js.map +1 -0
- package/dist/offline/index.mjs +1052 -0
- package/dist/offline/index.mjs.map +1 -0
- package/dist/offline/sqlite/device-worker.mjs +62 -0
- package/dist/offline/sqlite/index.mjs +17392 -0
- package/dist/offline/sqlite/sqlite3-opfs-async-proxy.js +881 -0
- package/dist/offline/sqlite/sqlite3.wasm +0 -0
- package/dist/request-KTBV6HHH.js +10 -0
- package/dist/request-KTBV6HHH.js.map +1 -0
- package/dist/request-SQOUNWZ4.mjs +10 -0
- package/dist/request-SQOUNWZ4.mjs.map +1 -0
- package/dist/server/cachedGet.d.mts +1 -1
- package/dist/server/cachedGet.d.ts +1 -1
- package/dist/server/cachedGet.stub.d.mts +1 -1
- package/dist/server/cachedGet.stub.d.ts +1 -1
- package/dist/server/index.d.mts +28 -2
- package/dist/server/index.d.ts +28 -2
- package/dist/server/index.js +18 -8
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +18 -8
- package/dist/{useSocket-IRHtk2xc.d.ts → useSocket-Bo9M0t0J.d.ts} +3 -1
- package/dist/{useSocket-C8fqU4od.d.mts → useSocket-J5qanQPN.d.mts} +3 -1
- package/dist/write-through-FDQ7N4YI.js +15 -0
- package/dist/write-through-FDQ7N4YI.js.map +1 -0
- package/dist/write-through-R25VQMH6.mjs +15 -0
- package/dist/write-through-R25VQMH6.mjs.map +1 -0
- package/package.json +7 -1
- package/pwa/sw.js +277 -26
- package/src/client/request.ts +37 -0
- package/src/components/editors/BlockNoteEditor.tsx +14 -0
- package/src/components/editors/__tests__/BlockNoteEditor.empty-change.spec.tsx +205 -0
- package/src/contexts/SocketContext.tsx +7 -1
- package/src/contexts/__tests__/SocketContext.session.spec.tsx +52 -5
- package/src/core/factories/RehydrationFactory.ts +28 -8
- package/src/core/factories/__tests__/RehydrationFactory.spec.ts +33 -0
- package/src/core/index.ts +3 -0
- package/src/core/interfaces/ApiData.ts +2 -0
- package/src/core/registry/ModuleRegistry.ts +3 -0
- package/src/features/auth/components/forms/Logout.tsx +8 -1
- package/src/features/auth/components/forms/__tests__/Logout.spec.tsx +62 -0
- package/src/features/onboarding/contexts/OnboardingContext.spec.tsx +42 -1
- package/src/features/onboarding/contexts/OnboardingContext.tsx +11 -1
- package/src/hooks/__tests__/useSocket.pause.spec.ts +39 -0
- package/src/hooks/useSocket.ts +20 -1
- package/src/offline/__tests__/answer-builders.spec.ts +176 -0
- package/src/offline/__tests__/device-switch.spec.ts +179 -0
- package/src/offline/__tests__/generic-answers.spec.ts +380 -0
- package/src/offline/__tests__/local-transport.spec.ts +150 -0
- package/src/offline/__tests__/logout.spec.ts +122 -0
- package/src/offline/__tests__/offline-fixtures.ts +89 -0
- package/src/offline/__tests__/router.spec.ts +56 -0
- package/src/offline/__tests__/state.spec.ts +62 -0
- package/src/offline/__tests__/write-through.spec.ts +137 -0
- package/src/offline/config.ts +229 -0
- package/src/offline/configure.ts +14 -0
- package/src/offline/device-switch.ts +75 -0
- package/src/offline/generic-answers.ts +706 -0
- package/src/offline/index.ts +67 -0
- package/src/offline/local-transport.ts +189 -0
- package/src/offline/logout.ts +41 -0
- package/src/offline/router.ts +41 -0
- package/src/offline/server/__tests__/build-offline-manifest.spec.ts +76 -0
- package/src/offline/server/build-offline-manifest.ts +49 -0
- package/src/offline/state-hooks.ts +10 -0
- package/src/offline/state.ts +143 -0
- package/src/offline/store/DeviceStore.ts +112 -0
- package/src/offline/store/__tests__/device-store-lifecycle.spec.ts +343 -0
- package/src/offline/store/__tests__/shared-database.spec.ts +151 -0
- package/src/offline/store/__tests__/sql-device-store.spec.ts +399 -0
- package/src/offline/store/lifecycle.ts +315 -0
- package/src/offline/store/schema.ts +101 -0
- package/src/offline/store/shared-database.ts +321 -0
- package/src/offline/store/sql-device-store.ts +469 -0
- package/src/offline/store/worker-client.ts +71 -0
- package/src/offline/store/worker.ts +87 -0
- package/src/offline/sync/__tests__/conflicts.spec.ts +185 -0
- package/src/offline/sync/__tests__/queue-drain.spec.ts +364 -0
- package/src/offline/sync/__tests__/runtime.spec.ts +337 -0
- package/src/offline/sync/__tests__/sync-fixtures.ts +103 -0
- package/src/offline/sync/__tests__/sync-root.spec.ts +727 -0
- package/src/offline/sync/artwork-cache.ts +67 -0
- package/src/offline/sync/conflicts.ts +211 -0
- package/src/offline/sync/counts.ts +39 -0
- package/src/offline/sync/drain-queue.ts +246 -0
- package/src/offline/sync/known-roots.ts +37 -0
- package/src/offline/sync/models/OfflineChangeSet.ts +34 -0
- package/src/offline/sync/models/OfflineChangeSetInterface.ts +54 -0
- package/src/offline/sync/models/RootSync.ts +68 -0
- package/src/offline/sync/models/RootSyncInterface.ts +20 -0
- package/src/offline/sync/models/__tests__/OfflineChangeSet.spec.ts +47 -0
- package/src/offline/sync/models/__tests__/RootSync.spec.ts +54 -0
- package/src/offline/sync/models/index.ts +5 -0
- package/src/offline/sync/models/offline.modules.ts +14 -0
- package/src/offline/sync/runtime.ts +226 -0
- package/src/offline/sync/session-refresh.ts +32 -0
- package/src/offline/sync/sync-root.ts +336 -0
- package/src/offline/sync/waiting.ts +40 -0
- package/src/offline/types.ts +115 -0
- package/src/offline/write-through.ts +189 -0
- package/src/pwa/__tests__/sw.spec.ts +358 -1
- package/src/server/index.ts +5 -0
- package/src/unified/__tests__/JsonApiRequest.offline.spec.ts +243 -0
- package/dist/BlockNoteEditor-75ED2CN2.mjs.map +0 -1
- package/dist/BlockNoteEditor-7M5QW2YQ.js.map +0 -1
- package/dist/BlockNoteViewer-WGCDB7IJ.js.map +0 -1
- package/dist/JsonApiRequest-FZGPLNUF.js +0 -26
- package/dist/chunk-AA52ORW2.mjs.map +0 -1
- package/dist/chunk-EWCY6QJA.mjs.map +0 -1
- package/dist/chunk-FF4EIQ6H.js.map +0 -1
- package/dist/chunk-JTPRJNT2.js.map +0 -1
- package/dist/chunk-L6PYFQTO.js.map +0 -1
- package/dist/chunk-O3PDL2CV.js.map +0 -1
- package/dist/chunk-PI56A4DG.mjs.map +0 -1
- package/dist/chunk-RXQYEEKZ.mjs.map +0 -1
- package/dist/chunk-V3YDVJ63.js.map +0 -1
- package/dist/request-5RLK2AM3.mjs +0 -9
- package/dist/request-EUGTI3BE.js +0 -9
- package/dist/request-EUGTI3BE.js.map +0 -1
- /package/dist/{JsonApiRequest-A66XK5WT.mjs.map → JsonApiRequest-F6TMJ42G.mjs.map} +0 -0
- /package/dist/{chunk-3YW35YHQ.mjs.map → chunk-BH6JWIKI.mjs.map} +0 -0
- /package/dist/{chunk-V5MI7RNB.mjs.map → chunk-DGWOVYEC.mjs.map} +0 -0
- /package/dist/{request-5RLK2AM3.mjs.map → lifecycle-LKGPRBS7.mjs.map} +0 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CURRENT_USER_STORAGE_KEY,
|
|
3
|
+
getClientApiUrl
|
|
4
|
+
} from "./chunk-DOCIFE6A.mjs";
|
|
5
|
+
import {
|
|
6
|
+
NeedsConnectionError,
|
|
7
|
+
getOfflineConfig,
|
|
8
|
+
getOfflineState,
|
|
9
|
+
getOfflineStore,
|
|
10
|
+
setOfflineState
|
|
11
|
+
} from "./chunk-27LQDGNP.mjs";
|
|
12
|
+
import {
|
|
13
|
+
getApiUrl
|
|
14
|
+
} from "./chunk-DGWOVYEC.mjs";
|
|
15
|
+
import {
|
|
16
|
+
__name
|
|
17
|
+
} from "./chunk-PAWJFY3S.mjs";
|
|
18
|
+
|
|
19
|
+
// src/offline/generic-answers.ts
|
|
20
|
+
var DEFAULT_PAGE_SIZE = 25;
|
|
21
|
+
function errorAnswer(status, title, detail, code) {
|
|
22
|
+
return {
|
|
23
|
+
status,
|
|
24
|
+
body: { errors: [{ status: String(status), ...code ? { code } : {}, title, detail }] }
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
__name(errorAnswer, "errorAnswer");
|
|
28
|
+
function needsConnectionAnswer(detail = "Needs a connection") {
|
|
29
|
+
return errorAnswer(503, "Needs a connection", detail, "needs-connection");
|
|
30
|
+
}
|
|
31
|
+
__name(needsConnectionAnswer, "needsConnectionAnswer");
|
|
32
|
+
function notFoundAnswer(detail) {
|
|
33
|
+
return errorAnswer(404, "Not found", detail);
|
|
34
|
+
}
|
|
35
|
+
__name(notFoundAnswer, "notFoundAnswer");
|
|
36
|
+
async function singleAnswer(store, record, status = 200) {
|
|
37
|
+
return { status, body: { data: record.resource, included: await includedFor(store, [record]) } };
|
|
38
|
+
}
|
|
39
|
+
__name(singleAnswer, "singleAnswer");
|
|
40
|
+
function isRootWriteLocked(rootId) {
|
|
41
|
+
return !!rootId && getOfflineState().lockedRootIds.includes(rootId);
|
|
42
|
+
}
|
|
43
|
+
__name(isRootWriteLocked, "isRootWriteLocked");
|
|
44
|
+
function identifiersOf(relationship) {
|
|
45
|
+
const data = relationship?.data;
|
|
46
|
+
if (!data) return [];
|
|
47
|
+
return (Array.isArray(data) ? data : [data]).filter((item) => item && item.type && item.id);
|
|
48
|
+
}
|
|
49
|
+
__name(identifiersOf, "identifiersOf");
|
|
50
|
+
function linkedIds(links, id) {
|
|
51
|
+
return links.map(
|
|
52
|
+
(link) => link.fromId === id ? { type: link.toType, id: link.toId } : { type: link.fromType, id: link.fromId }
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
__name(linkedIds, "linkedIds");
|
|
56
|
+
function relationshipTargets(resource) {
|
|
57
|
+
const targets = [];
|
|
58
|
+
for (const relationship of Object.values(resource?.relationships ?? {})) {
|
|
59
|
+
const data = relationship?.data;
|
|
60
|
+
for (const ref of Array.isArray(data) ? data : data ? [data] : []) {
|
|
61
|
+
if (ref?.type && ref?.id) targets.push({ type: ref.type, id: ref.id });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return targets;
|
|
65
|
+
}
|
|
66
|
+
__name(relationshipTargets, "relationshipTargets");
|
|
67
|
+
async function includedFor(store, records) {
|
|
68
|
+
const level = new Map(records.map((record) => [`${record.type}:${record.id}`, 0]));
|
|
69
|
+
const included = [];
|
|
70
|
+
const add = /* @__PURE__ */ __name(async (target, atLevel) => {
|
|
71
|
+
const key = `${target.type}:${target.id}`;
|
|
72
|
+
if (level.has(key)) return;
|
|
73
|
+
level.set(key, atLevel);
|
|
74
|
+
const stored = await store.getRecord(target.type, target.id);
|
|
75
|
+
const resource = stored?.resource ?? signedInUserResource(target);
|
|
76
|
+
if (resource) included.push({ resource, level: atLevel });
|
|
77
|
+
}, "add");
|
|
78
|
+
for (const record of records) {
|
|
79
|
+
const links = [...await store.linksFrom(record.id), ...await store.linksTo(record.id)];
|
|
80
|
+
for (const target of linkedIds(links, record.id)) await add(target, 1);
|
|
81
|
+
}
|
|
82
|
+
for (const record of records) {
|
|
83
|
+
for (const target of relationshipTargets(record.resource)) await add(target, 1);
|
|
84
|
+
}
|
|
85
|
+
for (const entry of included.filter((candidate) => candidate.level === 1)) {
|
|
86
|
+
for (const target of relationshipTargets(entry.resource)) await add(target, 2);
|
|
87
|
+
}
|
|
88
|
+
return included.map((entry) => entry.resource);
|
|
89
|
+
}
|
|
90
|
+
__name(includedFor, "includedFor");
|
|
91
|
+
function signedInUserResource(target) {
|
|
92
|
+
try {
|
|
93
|
+
if (target.id !== getOfflineConfig().getUserId()) return void 0;
|
|
94
|
+
const stored = JSON.parse(window.localStorage.getItem(CURRENT_USER_STORAGE_KEY) ?? "null");
|
|
95
|
+
const user = stored?.jsonApi;
|
|
96
|
+
if (user?.type !== target.type || user?.id !== target.id) return void 0;
|
|
97
|
+
return { type: user.type, id: user.id, attributes: user.attributes ?? {}, meta: user.meta };
|
|
98
|
+
} catch {
|
|
99
|
+
return void 0;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
__name(signedInUserResource, "signedInUserResource");
|
|
103
|
+
function attributeOf(record, name) {
|
|
104
|
+
const value = record.resource?.attributes?.[name];
|
|
105
|
+
if (value !== void 0 && value !== null) return value;
|
|
106
|
+
if (name === "updatedAt") return record.updatedAt ?? null;
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
__name(attributeOf, "attributeOf");
|
|
110
|
+
var SERVER_DEFAULT_ORDER_BY = "updatedAt DESC";
|
|
111
|
+
var ORDER_BY = /^([A-Za-z_][A-Za-z0-9_]*)(?:\s+(ASC|DESC))?$/i;
|
|
112
|
+
var ISO_DATE = /^\d{4}-\d{2}-\d{2}(T|$)/;
|
|
113
|
+
function comparable(value) {
|
|
114
|
+
if (value instanceof Date) return value.getTime();
|
|
115
|
+
if (typeof value === "string" && ISO_DATE.test(value)) {
|
|
116
|
+
const time = Date.parse(value);
|
|
117
|
+
if (!Number.isNaN(time)) return time;
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
__name(comparable, "comparable");
|
|
122
|
+
function compareAscending(left, right) {
|
|
123
|
+
const a = comparable(left);
|
|
124
|
+
const b = comparable(right);
|
|
125
|
+
const aNull = a === void 0 || a === null;
|
|
126
|
+
const bNull = b === void 0 || b === null;
|
|
127
|
+
if (aNull || bNull) return aNull === bNull ? 0 : aNull ? 1 : -1;
|
|
128
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
129
|
+
if (typeof a === "boolean" && typeof b === "boolean") return Number(a) - Number(b);
|
|
130
|
+
const x = String(a);
|
|
131
|
+
const y = String(b);
|
|
132
|
+
return x < y ? -1 : x > y ? 1 : 0;
|
|
133
|
+
}
|
|
134
|
+
__name(compareAscending, "compareAscending");
|
|
135
|
+
function searchRecords(records, query, options = {}) {
|
|
136
|
+
const term = query.get("search");
|
|
137
|
+
if (term === null || term === void 0 || term.trim() === "") return records;
|
|
138
|
+
const tokens = term.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
139
|
+
if (tokens.length === 0) return records;
|
|
140
|
+
return records.filter((record) => {
|
|
141
|
+
const attributes = record.resource?.attributes ?? {};
|
|
142
|
+
const fields = options.searchFields ?? Object.keys(attributes);
|
|
143
|
+
const texts = fields.map((field) => attributes[field]).filter((value) => typeof value === "string").map((value) => value.toLowerCase());
|
|
144
|
+
return tokens.every((token) => texts.some((text) => text.includes(token)));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
__name(searchRecords, "searchRecords");
|
|
148
|
+
function orderRecords(records, query, options = {}) {
|
|
149
|
+
const requested = query.get("orderBy")?.trim();
|
|
150
|
+
const value = requested || options.defaultOrderBy?.trim() || SERVER_DEFAULT_ORDER_BY;
|
|
151
|
+
const match = ORDER_BY.exec(value);
|
|
152
|
+
if (!match) return null;
|
|
153
|
+
const field = match[1];
|
|
154
|
+
const desc = match[2]?.toUpperCase() === "DESC";
|
|
155
|
+
return [...records].sort((left, right) => {
|
|
156
|
+
const order = compareAscending(attributeOf(left, field), attributeOf(right, field));
|
|
157
|
+
return desc ? -order : order;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
__name(orderRecords, "orderRecords");
|
|
161
|
+
function linkUrl(base, query, changes) {
|
|
162
|
+
const params = new URLSearchParams(query);
|
|
163
|
+
for (const [key, value] of Object.entries(changes)) {
|
|
164
|
+
if (value === void 0) params.delete(key);
|
|
165
|
+
else params.set(key, value);
|
|
166
|
+
}
|
|
167
|
+
const search = params.toString().replace(/%5B/g, "[").replace(/%5D/g, "]").replace(/%2C/g, ",");
|
|
168
|
+
return search ? `${base}?${search}` : base;
|
|
169
|
+
}
|
|
170
|
+
__name(linkUrl, "linkUrl");
|
|
171
|
+
async function listAnswer(ctx, records, options = {}) {
|
|
172
|
+
const { request, store } = ctx;
|
|
173
|
+
const base = request.url ? request.url.split("?")[0] : request.path;
|
|
174
|
+
const sorted = orderRecords(searchRecords(records, request.query, options), request.query, options);
|
|
175
|
+
if (!sorted) {
|
|
176
|
+
return errorAnswer(400, "Bad Request", "Invalid orderBy");
|
|
177
|
+
}
|
|
178
|
+
if (request.query.get("fetchAll") === "true") {
|
|
179
|
+
return {
|
|
180
|
+
status: 200,
|
|
181
|
+
body: {
|
|
182
|
+
data: sorted.map((record) => record.resource),
|
|
183
|
+
included: await includedFor(store, sorted),
|
|
184
|
+
links: { self: linkUrl(base, request.query, {}) },
|
|
185
|
+
meta: { total: sorted.length }
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const size = Number(request.query.get("page[size]")) || DEFAULT_PAGE_SIZE;
|
|
190
|
+
const offset = Number(request.query.get("page[offset]")) || 0;
|
|
191
|
+
const page = sorted.slice(offset, offset + size);
|
|
192
|
+
const links = {
|
|
193
|
+
self: linkUrl(base, request.query, { "page[size]": String(size), "page[offset]": void 0 })
|
|
194
|
+
};
|
|
195
|
+
if (offset + size < sorted.length) {
|
|
196
|
+
links.next = linkUrl(base, request.query, { "page[size]": String(size), "page[offset]": String(offset + size) });
|
|
197
|
+
}
|
|
198
|
+
if (offset > 0) {
|
|
199
|
+
links.prev = linkUrl(base, request.query, {
|
|
200
|
+
"page[size]": String(size),
|
|
201
|
+
"page[offset]": String(Math.max(0, offset - size))
|
|
202
|
+
});
|
|
203
|
+
links.previous = links.prev;
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
status: 200,
|
|
207
|
+
body: {
|
|
208
|
+
data: page.map((record) => record.resource),
|
|
209
|
+
included: await includedFor(store, page),
|
|
210
|
+
links,
|
|
211
|
+
meta: { total: sorted.length }
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
__name(listAnswer, "listAnswer");
|
|
216
|
+
async function orderedListAnswer(store, records) {
|
|
217
|
+
return {
|
|
218
|
+
status: 200,
|
|
219
|
+
body: {
|
|
220
|
+
data: records.map((record) => record.resource),
|
|
221
|
+
included: await includedFor(store, records),
|
|
222
|
+
meta: { total: records.length }
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
__name(orderedListAnswer, "orderedListAnswer");
|
|
227
|
+
function resourceListAnswer(resources) {
|
|
228
|
+
return { status: 200, body: { data: resources, included: [] } };
|
|
229
|
+
}
|
|
230
|
+
__name(resourceListAnswer, "resourceListAnswer");
|
|
231
|
+
function listOptionsOf(typeConfig) {
|
|
232
|
+
return { searchFields: typeConfig.searchFields, defaultOrderBy: typeConfig.defaultOrderBy };
|
|
233
|
+
}
|
|
234
|
+
__name(listOptionsOf, "listOptionsOf");
|
|
235
|
+
function linkKindFor(typeConfig, name) {
|
|
236
|
+
return typeConfig?.relationshipKinds?.[name] ?? name;
|
|
237
|
+
}
|
|
238
|
+
__name(linkKindFor, "linkKindFor");
|
|
239
|
+
function linkKindsOf(typeConfig, name) {
|
|
240
|
+
const mapped = typeConfig?.relationshipKinds?.[name];
|
|
241
|
+
return mapped ? [name, mapped] : void 0;
|
|
242
|
+
}
|
|
243
|
+
__name(linkKindsOf, "linkKindsOf");
|
|
244
|
+
async function linksOfKind(store, id, kinds, direction = "both") {
|
|
245
|
+
const result = [];
|
|
246
|
+
const seen = /* @__PURE__ */ new Set();
|
|
247
|
+
for (const kind of new Set(kinds)) {
|
|
248
|
+
const links = [
|
|
249
|
+
...direction !== "in" ? await store.linksFrom(id, kind) : [],
|
|
250
|
+
...direction !== "out" ? await store.linksTo(id, kind) : []
|
|
251
|
+
];
|
|
252
|
+
for (const link of links) {
|
|
253
|
+
const key = `${link.fromId}|${link.toId}|${link.kind}`;
|
|
254
|
+
if (seen.has(key)) continue;
|
|
255
|
+
seen.add(key);
|
|
256
|
+
result.push(link);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
__name(linksOfKind, "linksOfKind");
|
|
262
|
+
function typeConfigOf(config, type) {
|
|
263
|
+
return config.types.find((typeConfig) => typeConfig.module.name === type);
|
|
264
|
+
}
|
|
265
|
+
__name(typeConfigOf, "typeConfigOf");
|
|
266
|
+
function relationshipNamesToward(resource, type) {
|
|
267
|
+
return Object.entries(resource?.relationships ?? {}).filter(([, relationship]) => identifiersOf(relationship).some((identifier) => identifier.type === type)).map(([name]) => name);
|
|
268
|
+
}
|
|
269
|
+
__name(relationshipNamesToward, "relationshipNamesToward");
|
|
270
|
+
function notOnDeviceAnswer(type, id) {
|
|
271
|
+
return needsConnectionAnswer(`${type} ${id} is not on this device`);
|
|
272
|
+
}
|
|
273
|
+
__name(notOnDeviceAnswer, "notOnDeviceAnswer");
|
|
274
|
+
async function getOne(ctx, type) {
|
|
275
|
+
const record = await ctx.store.getRecord(type, ctx.params.id);
|
|
276
|
+
if (!record) return notOnDeviceAnswer(type, ctx.params.id);
|
|
277
|
+
return singleAnswer(ctx.store, record);
|
|
278
|
+
}
|
|
279
|
+
__name(getOne, "getOne");
|
|
280
|
+
async function rootList(ctx, config, typeConfig) {
|
|
281
|
+
const rootType = config.rootModule.name;
|
|
282
|
+
const rootId = ctx.params.rootId;
|
|
283
|
+
if (!await ctx.store.getRecord(rootType, rootId)) return notOnDeviceAnswer(rootType, rootId);
|
|
284
|
+
const records = await ctx.store.listRecords({ type: typeConfig.module.name, rootId });
|
|
285
|
+
return listAnswer(ctx, records, listOptionsOf(typeConfig));
|
|
286
|
+
}
|
|
287
|
+
__name(rootList, "rootList");
|
|
288
|
+
async function relatedList(ctx, config, typeConfig) {
|
|
289
|
+
const type = typeConfig.module.name;
|
|
290
|
+
const { parentType, parentId } = ctx.params;
|
|
291
|
+
const parent = await ctx.store.getRecord(parentType, parentId);
|
|
292
|
+
if (!parent) return notOnDeviceAnswer(parentType, parentId);
|
|
293
|
+
const links = [...await ctx.store.linksFrom(parentId), ...await ctx.store.linksTo(parentId)].filter(
|
|
294
|
+
(link) => link.fromId === parentId ? link.toType === type : link.fromType === type
|
|
295
|
+
);
|
|
296
|
+
const candidates = await ctx.store.listRecords({
|
|
297
|
+
type,
|
|
298
|
+
ids: [...new Set(links.map((link) => link.fromId === parentId ? link.toId : link.fromId))]
|
|
299
|
+
});
|
|
300
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
301
|
+
let anyKind = false;
|
|
302
|
+
const collect = /* @__PURE__ */ __name((owner, names) => {
|
|
303
|
+
for (const name of names) {
|
|
304
|
+
const known = linkKindsOf(owner, name);
|
|
305
|
+
if (known) known.forEach((kind) => kinds.add(kind));
|
|
306
|
+
else anyKind = true;
|
|
307
|
+
}
|
|
308
|
+
}, "collect");
|
|
309
|
+
collect(typeConfig, [
|
|
310
|
+
...new Set(candidates.flatMap((record) => relationshipNamesToward(record.resource, parentType)))
|
|
311
|
+
]);
|
|
312
|
+
collect(typeConfigOf(config, parentType), relationshipNamesToward(parent.resource, type));
|
|
313
|
+
const accepted = anyKind || kinds.size === 0 ? links : links.filter((link) => kinds.has(link.kind));
|
|
314
|
+
const ids = new Set(accepted.map((link) => link.fromId === parentId ? link.toId : link.fromId));
|
|
315
|
+
return listAnswer(
|
|
316
|
+
ctx,
|
|
317
|
+
candidates.filter((record) => ids.has(record.id)),
|
|
318
|
+
listOptionsOf(typeConfig)
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
__name(relatedList, "relatedList");
|
|
322
|
+
async function rootIdForResource(store, config, resource) {
|
|
323
|
+
const rootType = config.rootModule.name;
|
|
324
|
+
if (resource?.type === rootType && resource.id) return resource.id;
|
|
325
|
+
const identifiers = Object.values(resource?.relationships ?? {}).flatMap(identifiersOf);
|
|
326
|
+
const root = identifiers.find((identifier) => identifier.type === rootType);
|
|
327
|
+
if (root) return root.id;
|
|
328
|
+
for (const identifier of identifiers) {
|
|
329
|
+
const stored = await store.getRecord(identifier.type, identifier.id);
|
|
330
|
+
if (stored?.rootId) return stored.rootId;
|
|
331
|
+
}
|
|
332
|
+
return void 0;
|
|
333
|
+
}
|
|
334
|
+
__name(rootIdForResource, "rootIdForResource");
|
|
335
|
+
async function linkRelationships(store, typeConfig, record, before, after) {
|
|
336
|
+
for (const [name, relationship] of Object.entries(after)) {
|
|
337
|
+
const previous = new Map(identifiersOf(before[name]).map((identifier) => [identifier.id, identifier]));
|
|
338
|
+
const next = new Map(identifiersOf(relationship).map((identifier) => [identifier.id, identifier]));
|
|
339
|
+
const kinds = linkKindsOf(typeConfig, name);
|
|
340
|
+
for (const [id] of previous) {
|
|
341
|
+
if (next.has(id)) continue;
|
|
342
|
+
const links = kinds ? await linksOfKind(store, record.id, kinds, "both") : [...await store.linksFrom(record.id), ...await store.linksTo(record.id)];
|
|
343
|
+
for (const link of links) {
|
|
344
|
+
if (link.fromId === id || link.toId === id) {
|
|
345
|
+
await store.deleteLink({ fromId: link.fromId, toId: link.toId, kind: link.kind });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
for (const [id, identifier] of next) {
|
|
350
|
+
if (previous.has(id)) continue;
|
|
351
|
+
await store.putLink({
|
|
352
|
+
fromType: record.type,
|
|
353
|
+
fromId: record.id,
|
|
354
|
+
toType: identifier.type,
|
|
355
|
+
toId: id,
|
|
356
|
+
kind: linkKindFor(typeConfig, name),
|
|
357
|
+
props: {}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
__name(linkRelationships, "linkRelationships");
|
|
363
|
+
function resourceFromDocument(document, identity) {
|
|
364
|
+
const data = document?.data ?? {};
|
|
365
|
+
const resource = { type: identity.type, id: identity.id };
|
|
366
|
+
if (data.attributes) resource.attributes = { ...data.attributes };
|
|
367
|
+
if (data.relationships) resource.relationships = { ...data.relationships };
|
|
368
|
+
if (data.meta) resource.meta = { ...data.meta };
|
|
369
|
+
return resource;
|
|
370
|
+
}
|
|
371
|
+
__name(resourceFromDocument, "resourceFromDocument");
|
|
372
|
+
function mergeResource(before, identity, data) {
|
|
373
|
+
const previous = before ?? identity;
|
|
374
|
+
const resource = {
|
|
375
|
+
...previous,
|
|
376
|
+
type: identity.type,
|
|
377
|
+
id: identity.id,
|
|
378
|
+
attributes: { ...previous.attributes ?? {}, ...data?.attributes ?? {} },
|
|
379
|
+
relationships: { ...previous.relationships ?? {}, ...data?.relationships ?? {} }
|
|
380
|
+
};
|
|
381
|
+
if (data?.meta) resource.meta = { ...previous.meta ?? {}, ...data.meta };
|
|
382
|
+
return resource;
|
|
383
|
+
}
|
|
384
|
+
__name(mergeResource, "mergeResource");
|
|
385
|
+
function newId() {
|
|
386
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
387
|
+
return `local-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
388
|
+
}
|
|
389
|
+
__name(newId, "newId");
|
|
390
|
+
async function create(ctx, config, type) {
|
|
391
|
+
const typeConfig = typeConfigOf(config, type);
|
|
392
|
+
const id = ctx.request.body?.data?.id ?? newId();
|
|
393
|
+
const resource = resourceFromDocument(ctx.request.body, { type, id });
|
|
394
|
+
const rootId = await rootIdForResource(ctx.store, config, resource);
|
|
395
|
+
if (!rootId) return needsConnectionAnswer(`A new ${type} that belongs to no root record needs a connection`);
|
|
396
|
+
if (isRootWriteLocked(rootId)) return needsConnectionAnswer("The root record is locked");
|
|
397
|
+
const record = { type, id, rootId, resource, updatedAt: null, base: null, localOnly: true };
|
|
398
|
+
await ctx.store.transaction(async (store) => {
|
|
399
|
+
await store.putRecord(record);
|
|
400
|
+
await linkRelationships(store, typeConfig, { type, id }, {}, resource.relationships ?? {});
|
|
401
|
+
await store.enqueue({
|
|
402
|
+
rootId,
|
|
403
|
+
method: ctx.request.method,
|
|
404
|
+
path: queuePath(ctx),
|
|
405
|
+
body: ctx.request.body,
|
|
406
|
+
base: null
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
return singleAnswer(ctx.store, record, 201);
|
|
410
|
+
}
|
|
411
|
+
__name(create, "create");
|
|
412
|
+
async function update(ctx, config, type) {
|
|
413
|
+
const id = ctx.params.id;
|
|
414
|
+
const existing = await ctx.store.getRecord(type, id);
|
|
415
|
+
if (!existing) return notOnDeviceAnswer(type, id);
|
|
416
|
+
if (isRootWriteLocked(existing.rootId)) return needsConnectionAnswer("The root record is locked");
|
|
417
|
+
const data = ctx.request.body?.data ?? {};
|
|
418
|
+
const before = existing.resource ?? { type, id };
|
|
419
|
+
const resource = mergeResource(before, { type, id }, data);
|
|
420
|
+
const base = existing.localOnly ? null : existing.base ?? before;
|
|
421
|
+
const record = { ...existing, resource, base, localOnly: existing.localOnly };
|
|
422
|
+
await ctx.store.transaction(async (store) => {
|
|
423
|
+
await store.putRecord(record);
|
|
424
|
+
await linkRelationships(
|
|
425
|
+
store,
|
|
426
|
+
typeConfigOf(config, type),
|
|
427
|
+
{ type, id },
|
|
428
|
+
before.relationships ?? {},
|
|
429
|
+
data.relationships ?? {}
|
|
430
|
+
);
|
|
431
|
+
await store.enqueue({
|
|
432
|
+
rootId: existing.rootId,
|
|
433
|
+
method: ctx.request.method,
|
|
434
|
+
path: queuePath(ctx),
|
|
435
|
+
body: ctx.request.body,
|
|
436
|
+
recordType: type,
|
|
437
|
+
recordId: id,
|
|
438
|
+
base
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
return singleAnswer(ctx.store, record);
|
|
442
|
+
}
|
|
443
|
+
__name(update, "update");
|
|
444
|
+
async function remove(ctx, type) {
|
|
445
|
+
const id = ctx.params.id;
|
|
446
|
+
const existing = await ctx.store.getRecord(type, id);
|
|
447
|
+
if (!existing) return notOnDeviceAnswer(type, id);
|
|
448
|
+
if (isRootWriteLocked(existing.rootId)) return needsConnectionAnswer("The root record is locked");
|
|
449
|
+
const base = existing.localOnly ? null : existing.base ?? existing.resource;
|
|
450
|
+
await ctx.store.transaction(async (store) => {
|
|
451
|
+
const links = [...await store.linksFrom(id), ...await store.linksTo(id)];
|
|
452
|
+
for (const link of links) await store.deleteLink({ fromId: link.fromId, toId: link.toId, kind: link.kind });
|
|
453
|
+
await store.deleteRecord(type, id);
|
|
454
|
+
await store.enqueue({
|
|
455
|
+
rootId: existing.rootId,
|
|
456
|
+
method: "DELETE",
|
|
457
|
+
path: queuePath(ctx),
|
|
458
|
+
recordType: type,
|
|
459
|
+
recordId: id,
|
|
460
|
+
base
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
return { status: 204, body: void 0 };
|
|
464
|
+
}
|
|
465
|
+
__name(remove, "remove");
|
|
466
|
+
function queuePath(ctx) {
|
|
467
|
+
const query = ctx.request.query.toString();
|
|
468
|
+
return query ? `${ctx.request.path}?${query}` : ctx.request.path;
|
|
469
|
+
}
|
|
470
|
+
__name(queuePath, "queuePath");
|
|
471
|
+
function readOnlyGuard(typeConfig, answer) {
|
|
472
|
+
return async (ctx) => typeConfig.readOnly ? needsConnectionAnswer(`${typeConfig.module.name} is read-only offline`) : answer(ctx);
|
|
473
|
+
}
|
|
474
|
+
__name(readOnlyGuard, "readOnlyGuard");
|
|
475
|
+
function buildGenericRoutes(config) {
|
|
476
|
+
const routes = [];
|
|
477
|
+
const route = /* @__PURE__ */ __name((method, pattern, answer) => routes.push({ method, pattern, answer, queue: false }), "route");
|
|
478
|
+
for (const typeConfig of config.types) {
|
|
479
|
+
const type = typeConfig.module.name;
|
|
480
|
+
route("GET", [typeConfig.module, ":id"], (ctx) => getOne(ctx, type));
|
|
481
|
+
route("GET", [config.rootModule, ":rootId", typeConfig.module], (ctx) => rootList(ctx, config, typeConfig));
|
|
482
|
+
route(
|
|
483
|
+
"POST",
|
|
484
|
+
[typeConfig.module],
|
|
485
|
+
readOnlyGuard(typeConfig, (ctx) => create(ctx, config, type))
|
|
486
|
+
);
|
|
487
|
+
route(
|
|
488
|
+
"PUT",
|
|
489
|
+
[typeConfig.module, ":id"],
|
|
490
|
+
readOnlyGuard(typeConfig, (ctx) => update(ctx, config, type))
|
|
491
|
+
);
|
|
492
|
+
route(
|
|
493
|
+
"PATCH",
|
|
494
|
+
[typeConfig.module, ":id"],
|
|
495
|
+
readOnlyGuard(typeConfig, (ctx) => update(ctx, config, type))
|
|
496
|
+
);
|
|
497
|
+
route(
|
|
498
|
+
"DELETE",
|
|
499
|
+
[typeConfig.module, ":id"],
|
|
500
|
+
readOnlyGuard(typeConfig, (ctx) => remove(ctx, type))
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
for (const typeConfig of config.types) {
|
|
504
|
+
route("GET", [":parentType", ":parentId", typeConfig.module], (ctx) => relatedList(ctx, config, typeConfig));
|
|
505
|
+
}
|
|
506
|
+
return routes;
|
|
507
|
+
}
|
|
508
|
+
__name(buildGenericRoutes, "buildGenericRoutes");
|
|
509
|
+
|
|
510
|
+
// src/offline/router.ts
|
|
511
|
+
function splitPath(path) {
|
|
512
|
+
const withoutQuery = path.split("?")[0].split("#")[0];
|
|
513
|
+
return withoutQuery.split("/").filter((segment) => segment.length > 0);
|
|
514
|
+
}
|
|
515
|
+
__name(splitPath, "splitPath");
|
|
516
|
+
function expandPattern(pattern) {
|
|
517
|
+
return pattern.flatMap((segment) => splitPath(typeof segment === "string" ? segment : segment.name));
|
|
518
|
+
}
|
|
519
|
+
__name(expandPattern, "expandPattern");
|
|
520
|
+
function matchPattern(pattern, path) {
|
|
521
|
+
const expected = expandPattern(pattern);
|
|
522
|
+
const actual = splitPath(path);
|
|
523
|
+
if (expected.length !== actual.length) return null;
|
|
524
|
+
const params = {};
|
|
525
|
+
for (let index = 0; index < expected.length; index++) {
|
|
526
|
+
const segment = expected[index];
|
|
527
|
+
const value = decodeURIComponent(actual[index]);
|
|
528
|
+
if (segment.startsWith(":")) {
|
|
529
|
+
params[segment.slice(1)] = value;
|
|
530
|
+
} else if (segment !== value) {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return params;
|
|
535
|
+
}
|
|
536
|
+
__name(matchPattern, "matchPattern");
|
|
537
|
+
function matchRoute(routes, method, path) {
|
|
538
|
+
for (const route of routes) {
|
|
539
|
+
if (route.method !== method) continue;
|
|
540
|
+
const params = matchPattern(route.pattern, path);
|
|
541
|
+
if (params) return { route, params };
|
|
542
|
+
}
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
__name(matchRoute, "matchRoute");
|
|
546
|
+
|
|
547
|
+
// src/offline/local-transport.ts
|
|
548
|
+
var METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
549
|
+
function apiBases() {
|
|
550
|
+
const bases = [];
|
|
551
|
+
for (const read of [getApiUrl, getClientApiUrl]) {
|
|
552
|
+
try {
|
|
553
|
+
const base = read();
|
|
554
|
+
if (base) bases.push(base);
|
|
555
|
+
} catch {
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return bases;
|
|
559
|
+
}
|
|
560
|
+
__name(apiBases, "apiBases");
|
|
561
|
+
function splitApiUrl(url) {
|
|
562
|
+
let relative;
|
|
563
|
+
for (const base of apiBases()) {
|
|
564
|
+
if (url.startsWith(base)) {
|
|
565
|
+
relative = url.slice(base.length);
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (relative === void 0) {
|
|
570
|
+
try {
|
|
571
|
+
const parsed = new URL(url);
|
|
572
|
+
relative = `${parsed.pathname}${parsed.search}`;
|
|
573
|
+
} catch {
|
|
574
|
+
relative = url;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const [pathPart, ...queryParts] = relative.split("?");
|
|
578
|
+
return {
|
|
579
|
+
path: pathPart.replace(/^\/+/, "").replace(/\/+$/, ""),
|
|
580
|
+
query: new URLSearchParams(queryParts.join("?"))
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
__name(splitApiUrl, "splitApiUrl");
|
|
584
|
+
function toApiData(answer) {
|
|
585
|
+
const ok = answer.status >= 200 && answer.status < 300;
|
|
586
|
+
return {
|
|
587
|
+
ok,
|
|
588
|
+
status: answer.status,
|
|
589
|
+
statusText: ok ? "OK" : answer.status === 503 ? "Service Unavailable" : "Error",
|
|
590
|
+
data: answer.body
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
__name(toApiData, "toApiData");
|
|
594
|
+
async function rootIdForCustomWrite(store, pattern, params, rootSegmentParam) {
|
|
595
|
+
const config = getOfflineConfig();
|
|
596
|
+
if (rootSegmentParam && params[rootSegmentParam]) return params[rootSegmentParam];
|
|
597
|
+
for (const value of Object.values(params)) {
|
|
598
|
+
for (const typeConfig of config.types) {
|
|
599
|
+
const stored = await store.getRecord(typeConfig.module.name, value);
|
|
600
|
+
if (stored?.rootId) return stored.rootId;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return rootIdForResource(store, config, pattern.body?.data);
|
|
604
|
+
}
|
|
605
|
+
__name(rootIdForCustomWrite, "rootIdForCustomWrite");
|
|
606
|
+
async function refreshPendingCount(store) {
|
|
607
|
+
const entries = await store.queue();
|
|
608
|
+
setOfflineState({ pending: entries.filter((entry) => entry.status !== "conflict").length });
|
|
609
|
+
}
|
|
610
|
+
__name(refreshPendingCount, "refreshPendingCount");
|
|
611
|
+
async function localTransport(request) {
|
|
612
|
+
try {
|
|
613
|
+
const method = request.method.toUpperCase();
|
|
614
|
+
if (!METHODS.includes(method)) return toApiData(needsConnectionAnswer());
|
|
615
|
+
if (request.files) return toApiData(needsConnectionAnswer("Uploading files needs a connection"));
|
|
616
|
+
const config = getOfflineConfig();
|
|
617
|
+
const { path, query } = splitApiUrl(request.url);
|
|
618
|
+
const offlineRequest = { method, path, query, body: request.body, url: request.url };
|
|
619
|
+
const custom = matchRoute(config.routes, method, path);
|
|
620
|
+
const match = custom ?? matchRoute(buildGenericRoutes(config), method, path);
|
|
621
|
+
if (!match) return toApiData(needsConnectionAnswer(`${method} ${path} has no offline answer`));
|
|
622
|
+
let store;
|
|
623
|
+
try {
|
|
624
|
+
store = await getOfflineStore();
|
|
625
|
+
} catch (error) {
|
|
626
|
+
if (error instanceof NeedsConnectionError) return toApiData(needsConnectionAnswer(error.message));
|
|
627
|
+
console.warn("[offline] the device copy could not be opened", error);
|
|
628
|
+
return toApiData(needsConnectionAnswer("The device copy is not available in this tab"));
|
|
629
|
+
}
|
|
630
|
+
const ctx = { request: offlineRequest, params: match.params, store };
|
|
631
|
+
const customRootId = /* @__PURE__ */ __name(async (route) => {
|
|
632
|
+
const rootIndex = route.pattern.findIndex(
|
|
633
|
+
(segment) => typeof segment !== "string" && segment.name === config.rootModule.name
|
|
634
|
+
);
|
|
635
|
+
const next = rootIndex >= 0 ? route.pattern[rootIndex + 1] : void 0;
|
|
636
|
+
const rootParam = typeof next === "string" && next.startsWith(":") ? next.slice(1) : void 0;
|
|
637
|
+
return rootIdForCustomWrite(store, offlineRequest, match.params, rootParam);
|
|
638
|
+
}, "customRootId");
|
|
639
|
+
if (!custom || !custom.route.queue) {
|
|
640
|
+
if (custom && method !== "GET") {
|
|
641
|
+
const rootId2 = await customRootId(custom.route);
|
|
642
|
+
if (rootId2 && isRootWriteLocked(rootId2)) {
|
|
643
|
+
return toApiData(needsConnectionAnswer("The root record is locked"));
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const answer2 = await match.route.answer(ctx);
|
|
647
|
+
if (method !== "GET") await refreshPendingCount(store);
|
|
648
|
+
return toApiData(answer2);
|
|
649
|
+
}
|
|
650
|
+
const rootId = await customRootId(custom.route);
|
|
651
|
+
if (!rootId) return toApiData(needsConnectionAnswer(`${method} ${path} belongs to no root record on this device`));
|
|
652
|
+
if (isRootWriteLocked(rootId)) return toApiData(needsConnectionAnswer("The root record is locked"));
|
|
653
|
+
const answer = await store.transaction(async (transactionStore) => {
|
|
654
|
+
const result = await custom.route.answer({ ...ctx, store: transactionStore });
|
|
655
|
+
if (result.status >= 200 && result.status < 300) {
|
|
656
|
+
await transactionStore.enqueue({
|
|
657
|
+
rootId,
|
|
658
|
+
method,
|
|
659
|
+
path: queuePath(ctx),
|
|
660
|
+
body: request.body,
|
|
661
|
+
base: null
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
return result;
|
|
665
|
+
});
|
|
666
|
+
await refreshPendingCount(store);
|
|
667
|
+
return toApiData(answer);
|
|
668
|
+
} catch (error) {
|
|
669
|
+
if (error instanceof NeedsConnectionError) return toApiData(needsConnectionAnswer(error.message));
|
|
670
|
+
console.error("[offline] local answer failed", error);
|
|
671
|
+
return toApiData({
|
|
672
|
+
status: 500,
|
|
673
|
+
body: {
|
|
674
|
+
errors: [{ status: "500", title: "Offline answer failed", detail: String(error?.message ?? error) }]
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
__name(localTransport, "localTransport");
|
|
680
|
+
|
|
681
|
+
export {
|
|
682
|
+
errorAnswer,
|
|
683
|
+
needsConnectionAnswer,
|
|
684
|
+
notFoundAnswer,
|
|
685
|
+
singleAnswer,
|
|
686
|
+
includedFor,
|
|
687
|
+
SERVER_DEFAULT_ORDER_BY,
|
|
688
|
+
searchRecords,
|
|
689
|
+
orderRecords,
|
|
690
|
+
listAnswer,
|
|
691
|
+
orderedListAnswer,
|
|
692
|
+
resourceListAnswer,
|
|
693
|
+
linkKindFor,
|
|
694
|
+
linkKindsOf,
|
|
695
|
+
linksOfKind,
|
|
696
|
+
rootIdForResource,
|
|
697
|
+
resourceFromDocument,
|
|
698
|
+
mergeResource,
|
|
699
|
+
matchPattern,
|
|
700
|
+
matchRoute,
|
|
701
|
+
splitApiUrl,
|
|
702
|
+
localTransport
|
|
703
|
+
};
|
|
704
|
+
//# sourceMappingURL=chunk-7JZHPMGW.mjs.map
|