@carlonicora/nextjs-jsonapi 6.24.0 → 6.26.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/{BlockNoteEditor-MONT74RQ.js → BlockNoteEditor-JHXDOCS5.js} +28 -25
- package/dist/BlockNoteEditor-JHXDOCS5.js.map +1 -0
- package/dist/{BlockNoteEditor-ENDOVTAW.mjs → BlockNoteEditor-RVDSB6TF.mjs} +13 -10
- package/dist/{BlockNoteEditor-ENDOVTAW.mjs.map → BlockNoteEditor-RVDSB6TF.mjs.map} +1 -1
- package/dist/{BlockNoteViewer-2TJZ6JET.mjs → BlockNoteViewer-76CKL6W5.mjs} +8 -6
- package/dist/{BlockNoteViewer-2TJZ6JET.mjs.map → BlockNoteViewer-76CKL6W5.mjs.map} +1 -1
- package/dist/{BlockNoteViewer-K2HBF3IP.js → BlockNoteViewer-B25WKYQP.js} +9 -7
- package/dist/BlockNoteViewer-B25WKYQP.js.map +1 -0
- package/dist/JsonApiRequest-OKNQZLS6.js +26 -0
- package/dist/{JsonApiRequest-KC6DOHKT.js.map → JsonApiRequest-OKNQZLS6.js.map} +1 -1
- package/dist/{JsonApiRequest-ADLBJ5RX.mjs → JsonApiRequest-WNPDS356.mjs} +2 -2
- package/dist/billing/index.js +504 -517
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +20 -33
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-VNUT2D27.mjs → chunk-2PWYG2WJ.mjs} +2 -2
- package/dist/{chunk-W4ML6D2X.mjs → chunk-3U7YJ73T.mjs} +7 -7
- package/dist/{chunk-BQ2WAG4K.js → chunk-3VNDFKT7.js} +19 -19
- package/dist/{chunk-BQ2WAG4K.js.map → chunk-3VNDFKT7.js.map} +1 -1
- package/dist/{chunk-BE6UMM5X.mjs → chunk-47HNJQ7U.mjs} +3 -3
- package/dist/chunk-5T2AQMQ2.mjs +90 -0
- package/dist/chunk-5T2AQMQ2.mjs.map +1 -0
- package/dist/{chunk-A5M3MORY.js → chunk-5WM72MLE.js} +3 -3
- package/dist/{chunk-A5M3MORY.js.map → chunk-5WM72MLE.js.map} +1 -1
- package/dist/{chunk-OZADFYE7.mjs → chunk-64REWDSK.mjs} +308 -241
- package/dist/chunk-64REWDSK.mjs.map +1 -0
- package/dist/{chunk-S7SPAC6X.mjs → chunk-73NKUBMP.mjs} +3 -3
- package/dist/chunk-743M4TIB.mjs +76 -0
- package/dist/chunk-743M4TIB.mjs.map +1 -0
- package/dist/{chunk-HN7L6RRW.js → chunk-77JC2UCF.js} +903 -955
- package/dist/chunk-77JC2UCF.js.map +1 -0
- package/dist/{chunk-7OIWOPM5.mjs → chunk-7MGXQIEP.mjs} +4 -3
- package/dist/chunk-7MGXQIEP.mjs.map +1 -0
- package/dist/{chunk-YTDGYI62.mjs → chunk-DWPHHUMV.mjs} +3 -3
- package/dist/{chunk-SO3YUCL6.js → chunk-F3EVYBMA.js} +6 -5
- package/dist/chunk-F3EVYBMA.js.map +1 -0
- package/dist/{chunk-53SD4YRF.js → chunk-FBHUUPRV.js} +5 -5
- package/dist/{chunk-53SD4YRF.js.map → chunk-FBHUUPRV.js.map} +1 -1
- package/dist/{chunk-T6G2F2BK.mjs → chunk-FJUKLVVX.mjs} +16 -68
- package/dist/chunk-FJUKLVVX.mjs.map +1 -0
- package/dist/chunk-HROKKUBQ.js +156 -0
- package/dist/chunk-HROKKUBQ.js.map +1 -0
- package/dist/{chunk-BDWV4KYR.js → chunk-JE6QWCNE.js} +1386 -1319
- package/dist/chunk-JE6QWCNE.js.map +1 -0
- package/dist/{chunk-RCOKQ3R4.js → chunk-LNO2JLU3.js} +2 -2
- package/dist/{chunk-RCOKQ3R4.js.map → chunk-LNO2JLU3.js.map} +1 -1
- package/dist/{chunk-TMV7TL7O.mjs → chunk-M7YK5CCE.mjs} +8 -6
- package/dist/{chunk-TMV7TL7O.mjs.map → chunk-M7YK5CCE.mjs.map} +1 -1
- package/dist/{chunk-GMMDXINC.js → chunk-N3NQS7X6.js} +12 -12
- package/dist/{chunk-GMMDXINC.js.map → chunk-N3NQS7X6.js.map} +1 -1
- package/dist/chunk-NP3AFULU.js +76 -0
- package/dist/chunk-NP3AFULU.js.map +1 -0
- package/dist/{chunk-TNTCLVUR.js → chunk-ONU4WCDG.js} +17 -15
- package/dist/chunk-ONU4WCDG.js.map +1 -0
- package/dist/{chunk-YSE2Z4RB.js → chunk-PQQFOONG.js} +12 -12
- package/dist/{chunk-YSE2Z4RB.js.map → chunk-PQQFOONG.js.map} +1 -1
- package/dist/chunk-RW6XZGZG.mjs +156 -0
- package/dist/chunk-RW6XZGZG.mjs.map +1 -0
- package/dist/chunk-SYMI4IDW.js +90 -0
- package/dist/chunk-SYMI4IDW.js.map +1 -0
- package/dist/{chunk-N2BFT7KI.mjs → chunk-W7U6O3OE.mjs} +2 -71
- package/dist/chunk-W7U6O3OE.mjs.map +1 -0
- package/dist/{chunk-YMCL3JXZ.mjs → chunk-YPBIPE2B.mjs} +2 -2
- package/dist/{chunk-P7L6OFX5.js → chunk-Z6T2575A.js} +12 -81
- package/dist/chunk-Z6T2575A.js.map +1 -0
- package/dist/client/index.js +13 -10
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +17 -14
- package/dist/components/forms/DatePickerPopover.d.ts.map +1 -1
- package/dist/components/forms/DateRangeSelector.d.ts.map +1 -1
- package/dist/components/forms/FormDate.d.ts.map +1 -1
- package/dist/components/forms/FormDateTime.d.ts.map +1 -1
- package/dist/components/forms/time-cycle.d.ts +14 -0
- package/dist/components/forms/time-cycle.d.ts.map +1 -0
- package/dist/components/index.js +11 -8
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +10 -7
- package/dist/components/tables/cells/cell.date.d.ts.map +1 -1
- package/dist/components/tables/cells/cell.dateTime.d.ts.map +1 -1
- package/dist/contexts/DateLocaleContext.d.ts +56 -0
- package/dist/contexts/DateLocaleContext.d.ts.map +1 -0
- package/dist/contexts/index.d.ts +1 -0
- package/dist/contexts/index.d.ts.map +1 -1
- package/dist/contexts/index.js +24 -8
- package/dist/contexts/index.js.map +1 -1
- package/dist/contexts/index.mjs +23 -7
- package/dist/core/index.js +17 -6
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +19 -8
- package/dist/date-locale/index.d.ts +2 -0
- package/dist/date-locale/index.d.ts.map +1 -0
- package/dist/date-locale/index.js +21 -0
- package/dist/date-locale/index.js.map +1 -0
- package/dist/date-locale/index.mjs +21 -0
- package/dist/email-automations/index.js +131 -123
- package/dist/email-automations/index.js.map +1 -1
- package/dist/email-automations/index.mjs +23 -15
- package/dist/email-automations/index.mjs.map +1 -1
- package/dist/features/account-deletion/components/AccountDeletionAdminTrigger.d.ts.map +1 -1
- package/dist/features/analytics/components/AnalyticsJourneyContainer.d.ts.map +1 -1
- package/dist/features/analytics/components/AnalyticsSessionsTable.d.ts.map +1 -1
- package/dist/features/analytics/index.js +154 -147
- package/dist/features/analytics/index.js.map +1 -1
- package/dist/features/analytics/index.mjs +32 -25
- package/dist/features/analytics/index.mjs.map +1 -1
- package/dist/features/analytics/server.js +11 -9
- package/dist/features/analytics/server.js.map +1 -1
- package/dist/features/analytics/server.mjs +9 -7
- package/dist/features/analytics/server.mjs.map +1 -1
- package/dist/features/billing/components/utils/date.d.ts +3 -3
- package/dist/features/billing/components/utils/format-options.d.ts +6 -4
- package/dist/features/billing/components/utils/format-options.d.ts.map +1 -1
- package/dist/features/billing/components/utils/useBillingFormatOptions.d.ts +5 -1
- package/dist/features/billing/components/utils/useBillingFormatOptions.d.ts.map +1 -1
- package/dist/features/billing/stripe-usage/components/details/UsageSummaryCard.d.ts.map +1 -1
- package/dist/features/billing/stripe-usage/components/lists/UsageHistoryTable.d.ts.map +1 -1
- package/dist/features/blog/components/admin/BlogTranslationStatus.d.ts.map +1 -1
- package/dist/features/blog/components/public/BlogPostMeta.d.ts.map +1 -1
- package/dist/features/blog/hooks/useBlogPostTableStructure.d.ts.map +1 -1
- package/dist/features/blog/index.js +168 -162
- package/dist/features/blog/index.js.map +1 -1
- package/dist/features/blog/index.mjs +25 -19
- package/dist/features/blog/index.mjs.map +1 -1
- package/dist/features/blog/server.js +29 -27
- package/dist/features/blog/server.js.map +1 -1
- package/dist/features/blog/server.mjs +8 -6
- package/dist/features/blog/server.mjs.map +1 -1
- package/dist/features/email-automation/hooks/useEmailAutomationEnrollmentTableStructure.d.ts.map +1 -1
- package/dist/features/handbook/components/parts/HandbookIndexStatus.d.ts.map +1 -1
- package/dist/features/help/components/HelpArticleBody.d.ts.map +1 -1
- package/dist/features/help/index.js +48 -43
- package/dist/features/help/index.js.map +1 -1
- package/dist/features/help/index.mjs +15 -10
- package/dist/features/help/index.mjs.map +1 -1
- package/dist/features/notification/components/notifications/Notification.d.ts +2 -1
- package/dist/features/notification/components/notifications/Notification.d.ts.map +1 -1
- package/dist/features/notification/contexts/NotificationContext.d.ts.map +1 -1
- package/dist/features/notification/hooks/useNotificationTableStructure.d.ts.map +1 -1
- package/dist/features/oauth/components/OAuthClientCard.d.ts.map +1 -1
- package/dist/features/oauth/components/OAuthClientDetail.d.ts.map +1 -1
- package/dist/features/tokenusage/index.js +133 -130
- package/dist/features/tokenusage/index.js.map +1 -1
- package/dist/features/tokenusage/index.mjs +10 -7
- package/dist/features/tokenusage/index.mjs.map +1 -1
- package/dist/features/tokenusage/lib/formatters.d.ts +6 -3
- package/dist/features/tokenusage/lib/formatters.d.ts.map +1 -1
- package/dist/index.js +19 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +23 -11
- package/dist/local-transport-AT4TE264.js +15 -0
- package/dist/{local-transport-MJPOKPTU.js.map → local-transport-AT4TE264.js.map} +1 -1
- package/dist/{local-transport-RVD4FZGA.mjs → local-transport-KWEUZ4GX.mjs} +5 -5
- package/dist/offline/index.js +56 -54
- package/dist/offline/index.js.map +1 -1
- package/dist/offline/index.mjs +23 -21
- package/dist/offline/index.mjs.map +1 -1
- package/dist/{request-4AWGPIGH.mjs → request-6GAIWWTG.mjs} +2 -2
- package/dist/{request-H5FZBZA2.js → request-BWO4Y434.js} +3 -3
- package/dist/{request-H5FZBZA2.js.map → request-BWO4Y434.js.map} +1 -1
- package/dist/server/ServerSession.d.ts +2 -0
- package/dist/server/ServerSession.d.ts.map +1 -1
- package/dist/server/index.js +10 -8
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +9 -7
- package/dist/utils/date-formatter.d.ts +45 -4
- package/dist/utils/date-formatter.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/toast.d.ts +2 -0
- package/dist/utils/toast.d.ts.map +1 -1
- package/dist/write-through-KNRI5L4V.mjs +15 -0
- package/dist/write-through-KNRI5L4V.mjs.map +1 -0
- package/dist/write-through-MYHQ6GJU.js +15 -0
- package/dist/{write-through-HE2QSOVI.js.map → write-through-MYHQ6GJU.js.map} +1 -1
- package/messages/en.json +1 -0
- package/messages/it.json +1 -0
- package/package.json +6 -1
- package/src/components/forms/DatePickerPopover.tsx +5 -3
- package/src/components/forms/DateRangeSelector.tsx +9 -7
- package/src/components/forms/FormDate.tsx +5 -3
- package/src/components/forms/FormDateTime.tsx +75 -39
- package/src/components/forms/__tests__/time-cycle.spec.ts +37 -0
- package/src/components/forms/time-cycle.ts +27 -0
- package/src/components/tables/cells/cell.date.tsx +3 -5
- package/src/components/tables/cells/cell.dateTime.tsx +3 -7
- package/src/contexts/DateLocaleContext.tsx +106 -0
- package/src/contexts/__tests__/DateLocaleContext.spec.tsx +175 -0
- package/src/contexts/index.ts +1 -0
- package/src/date-locale/index.ts +12 -0
- package/src/features/account-deletion/components/AccountDeletionAdminTrigger.tsx +3 -4
- package/src/features/analytics/components/AnalyticsJourneyContainer.tsx +8 -8
- package/src/features/analytics/components/AnalyticsSessionsTable.tsx +4 -6
- package/src/features/auth/components/two-factor/PasskeyList.tsx +3 -3
- package/src/features/auth/components/two-factor/TotpAuthenticatorList.tsx +3 -3
- package/src/features/billing/components/utils/__tests__/billing-formatters.spec.ts +14 -6
- package/src/features/billing/components/utils/date.ts +5 -5
- package/src/features/billing/components/utils/format-options.ts +6 -4
- package/src/features/billing/components/utils/useBillingFormatOptions.ts +8 -2
- package/src/features/billing/stripe-usage/components/details/UsageSummaryCard.tsx +4 -12
- package/src/features/billing/stripe-usage/components/lists/UsageHistoryTable.tsx +4 -17
- package/src/features/blog/components/admin/BlogTranslationStatus.tsx +3 -2
- package/src/features/blog/components/public/BlogPostMeta.tsx +5 -4
- package/src/features/blog/components/public/__tests__/BlogList.spec.tsx +1 -2
- package/src/features/blog/components/public/__tests__/BlogPostView.spec.tsx +2 -3
- package/src/features/blog/hooks/useBlogPostTableStructure.tsx +4 -8
- package/src/features/email-automation/hooks/useEmailAutomationEnrollmentTableStructure.tsx +11 -5
- package/src/features/email-blast/components/containers/EmailBlastContainer.tsx +3 -3
- package/src/features/email-blast/hooks/useEmailBlastTableStructure.tsx +4 -4
- package/src/features/handbook/components/parts/HandbookIndexStatus.tsx +4 -3
- package/src/features/help/components/HelpArticleBody.tsx +4 -3
- package/src/features/notification/components/notifications/Notification.tsx +38 -7
- package/src/features/notification/contexts/NotificationContext.tsx +6 -3
- package/src/features/notification/hooks/useNotificationTableStructure.tsx +6 -5
- package/src/features/oauth/components/OAuthClientCard.tsx +8 -5
- package/src/features/oauth/components/OAuthClientDetail.tsx +3 -6
- package/src/features/tokenusage/lib/__tests__/formatters.spec.ts +19 -0
- package/src/features/tokenusage/lib/formatters.ts +11 -6
- package/src/features/waitlist/hooks/useWaitlistTableStructure.tsx +5 -5
- package/src/server/ServerSession.ts +3 -0
- package/src/server/__tests__/ServerSession.read.spec.ts +18 -1
- package/src/utils/__tests__/date-formatter.spec.ts +134 -10
- package/src/utils/__tests__/toast.spec.ts +39 -0
- package/src/utils/date-formatter.ts +200 -44
- package/src/utils/index.ts +11 -1
- package/src/utils/toast.ts +2 -0
- package/dist/BlockNoteEditor-MONT74RQ.js.map +0 -1
- package/dist/BlockNoteViewer-K2HBF3IP.js.map +0 -1
- package/dist/JsonApiRequest-KC6DOHKT.js +0 -26
- package/dist/chunk-7OIWOPM5.mjs.map +0 -1
- package/dist/chunk-BDWV4KYR.js.map +0 -1
- package/dist/chunk-HN7L6RRW.js.map +0 -1
- package/dist/chunk-N2BFT7KI.mjs.map +0 -1
- package/dist/chunk-OZADFYE7.mjs.map +0 -1
- package/dist/chunk-P7L6OFX5.js.map +0 -1
- package/dist/chunk-SO3YUCL6.js.map +0 -1
- package/dist/chunk-T6G2F2BK.mjs.map +0 -1
- package/dist/chunk-TNTCLVUR.js.map +0 -1
- package/dist/local-transport-MJPOKPTU.js +0 -15
- package/dist/write-through-7I3LA2WN.mjs +0 -15
- package/dist/write-through-HE2QSOVI.js +0 -15
- /package/dist/{JsonApiRequest-ADLBJ5RX.mjs.map → JsonApiRequest-WNPDS356.mjs.map} +0 -0
- /package/dist/{chunk-VNUT2D27.mjs.map → chunk-2PWYG2WJ.mjs.map} +0 -0
- /package/dist/{chunk-W4ML6D2X.mjs.map → chunk-3U7YJ73T.mjs.map} +0 -0
- /package/dist/{chunk-BE6UMM5X.mjs.map → chunk-47HNJQ7U.mjs.map} +0 -0
- /package/dist/{chunk-S7SPAC6X.mjs.map → chunk-73NKUBMP.mjs.map} +0 -0
- /package/dist/{chunk-YTDGYI62.mjs.map → chunk-DWPHHUMV.mjs.map} +0 -0
- /package/dist/{chunk-YMCL3JXZ.mjs.map → chunk-YPBIPE2B.mjs.map} +0 -0
- /package/dist/{local-transport-RVD4FZGA.mjs.map → date-locale/index.mjs.map} +0 -0
- /package/dist/{request-4AWGPIGH.mjs.map → local-transport-KWEUZ4GX.mjs.map} +0 -0
- /package/dist/{write-through-7I3LA2WN.mjs.map → request-6GAIWWTG.mjs.map} +0 -0
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
VIEWPORT_COOKIE_NAME,
|
|
6
6
|
checkPermissionsFromServer
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-FJUKLVVX.mjs";
|
|
8
8
|
import {
|
|
9
9
|
__name
|
|
10
10
|
} from "./chunk-PAWJFY3S.mjs";
|
|
@@ -46,7 +46,8 @@ var ServerSession = class {
|
|
|
46
46
|
isAdministrator: !!roles && roles.includes(getRoleId().Administrator),
|
|
47
47
|
token,
|
|
48
48
|
sidebarOpen: cookieStore.get("sidebar_state")?.value !== "false",
|
|
49
|
-
isMobile: viewportCookie ? viewportCookie === "1" : userAgent({ headers: headerStore }).device.type === "mobile"
|
|
49
|
+
isMobile: viewportCookie ? viewportCookie === "1" : userAgent({ headers: headerStore }).device.type === "mobile",
|
|
50
|
+
acceptLanguage: headerStore.get("accept-language") ?? void 0
|
|
50
51
|
};
|
|
51
52
|
}
|
|
52
53
|
static async isLogged() {
|
|
@@ -138,4 +139,4 @@ export {
|
|
|
138
139
|
ServerSession,
|
|
139
140
|
buildOfflineManifest
|
|
140
141
|
};
|
|
141
|
-
//# sourceMappingURL=chunk-
|
|
142
|
+
//# sourceMappingURL=chunk-7MGXQIEP.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/cache.ts","../src/server/ServerSession.ts","../src/offline/server/build-offline-manifest.ts"],"sourcesContent":["// Server-only utilities (NOT server actions - these cannot be called from client components)\n\nimport { revalidateTag, updateTag } from \"next/cache\";\n\nimport type { CacheProfile } from \"./cacheProfiles\";\n\nexport { CACHE_PROFILES, type CacheProfile } from \"./cacheProfiles\";\n\n/**\n * Revalidate a cache tag to invalidate cached data.\n * Next.js 16+ requires a profile parameter.\n *\n * With `\"max\"` the tagged entries are marked stale and served stale-while-revalidate\n * (a one-year window): the next request still gets the old data while fresh data loads\n * in the background. It is not an immediate invalidation. A Server Action that must\n * read its own write uses `updateCacheTag` instead.\n *\n * @param tag - The cache tag to invalidate\n * @param profile - The cache profile whose `expire` bounds how long stale data may be served (defaults to \"max\")\n */\nexport async function invalidateCacheTag(tag: string, profile: CacheProfile = \"max\"): Promise<void> {\n revalidateTag(tag, profile);\n}\n\n/**\n * Revalidate multiple cache tags.\n *\n * @param tags - Array of cache tags to invalidate\n * @param profile - The cache profile, see `invalidateCacheTag` (defaults to \"max\", stale-while-revalidate)\n */\nexport async function invalidateCacheTags(tags: string[], profile: CacheProfile = \"max\"): Promise<void> {\n for (const tag of tags) {\n revalidateTag(tag, profile);\n }\n}\n\n/**\n * Expire a cache tag immediately, so the same request reads fresh data (read-your-own-writes).\n * Wraps `updateTag`, which works in Server Actions only. Route Handlers use `invalidateCacheTag`.\n *\n * @param tag - The cache tag to expire\n */\nexport async function updateCacheTag(tag: string): Promise<void> {\n updateTag(tag);\n}\n","import { cookies, headers } from \"next/headers\";\nimport { redirect } from \"next/navigation\";\nimport { userAgent } from \"next/server\";\nimport { ungzip } from \"pako\";\nimport { Action, checkPermissionsFromServer, ModuleWithPermissions } from \"../permissions\";\nimport { getRoleId } from \"../roles/config\";\nimport { VIEWPORT_COOKIE_NAME } from \"../utils/use-mobile\";\n\nexport interface SessionSnapshot {\n isLogged: boolean;\n isAdministrator: boolean;\n token?: string;\n sidebarOpen: boolean;\n isMobile: boolean;\n /** The request's raw `Accept-Language` header; the date locale is resolved from it on the client. */\n acceptLanguage?: string;\n}\n\nexport class ServerSession {\n static async read(): Promise<SessionSnapshot> {\n const cookieStore = await cookies();\n const headerStore = await headers();\n\n const token = cookieStore.get(\"token\")?.value || undefined;\n const roles = cookieStore.get(\"roles\")?.value;\n const viewportCookie = cookieStore.get(VIEWPORT_COOKIE_NAME)?.value;\n\n return {\n isLogged: !!token,\n isAdministrator: !!roles && roles.includes(getRoleId().Administrator),\n token,\n sidebarOpen: cookieStore.get(\"sidebar_state\")?.value !== \"false\",\n isMobile: viewportCookie ? viewportCookie === \"1\" : userAgent({ headers: headerStore }).device.type === \"mobile\",\n acceptLanguage: headerStore.get(\"accept-language\") ?? undefined,\n };\n }\n\n static async isLogged() {\n const cookieStore = await cookies();\n const token = cookieStore.get(\"token\");\n\n if (!token || !token.value) return false;\n return true;\n }\n\n static async companyId() {\n const cookieStore = await cookies();\n return cookieStore.get(\"companyId\")?.value ?? \"\";\n }\n\n static async userId() {\n const cookieStore = await cookies();\n return cookieStore.get(\"userId\")?.value ?? \"\";\n }\n\n static async checkPermission<M extends ModuleWithPermissions>(params: { module: M; action: Action; data?: any }) {\n if (!(await this.hasPermissionToModule(params))) redirect(`/401`);\n }\n\n static async hasRole(roleId: string): Promise<boolean> {\n const cookieStore = await cookies();\n const roles = cookieStore.get(\"roles\")?.value;\n\n if (!roles || !roles.includes(roleId)) return false;\n\n return true;\n }\n\n static async hasFeature(featureId: string): Promise<boolean> {\n const cookieStore = await cookies();\n const features = cookieStore.get(\"features\")?.value;\n\n if (!features || !features.includes(featureId)) return false;\n\n return true;\n }\n\n static async hasPermissionToModule<M extends ModuleWithPermissions>(params: {\n module: M;\n action: Action;\n data?: any;\n }): Promise<boolean> {\n const cookieStore = await cookies();\n\n if (params.module.feature) {\n const features = cookieStore.get(\"features\")?.value;\n\n if (features && !features.includes(params.module.feature)) return false;\n }\n\n const rawModules = cookieStore.get(\"modules\")?.value;\n if (!rawModules) return false;\n\n const modules: {\n id: string;\n permissions: {\n create: boolean | string;\n read: boolean | string;\n update: boolean | string;\n delete: boolean | string;\n };\n }[] = JSON.parse(ungzip(Buffer.from(rawModules, \"base64\"), { toText: true }));\n\n const selectedModule = modules.find((module) => module.id === params.module.moduleId);\n\n return checkPermissionsFromServer({\n module: params.module,\n action: params.action,\n data: params.data,\n userId: await this.userId(),\n selectedModule: selectedModule,\n });\n\n // if (!selectedModule) return false;\n\n // if (!selectedModule.permissions[params.action]) return false;\n // if (typeof selectedModule.permissions[params.action] === \"boolean\")\n // return selectedModule.permissions[params.action] as boolean;\n\n // if (!params.data) return false;\n\n // return params.data[selectedModule.permissions[params.action] as string] === this.userId;\n }\n}\n","import { createHash } from \"node:crypto\";\nimport { readdir, readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nexport type OfflineManifest = { buildId: string; assets: string[] };\n\nasync function listFiles(dir: string): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n const files: string[] = [];\n for (const entry of entries) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) files.push(...(await listFiles(full)));\n else if (entry.isFile()) files.push(full);\n }\n return files;\n}\n\n/**\n * Describes the running build for the service worker's offline shell precache\n * (`/offline-manifest.json`): the build id from `<distDir>/BUILD_ID` and every\n * file under `<distDir>/static`, as the `/_next/static/...` URL Next serves it at.\n * `next dev` writes no BUILD_ID (its folder is `.next/dev`): the id is then `dev-` plus a hash\n * of the asset list. Dev chunk names do NOT carry content hashes (an edit rewrites a chunk under\n * the same name), so the id changes only when files appear or disappear; the service worker\n * therefore serves `/_next/static/` network first for a `dev-` build (pwa/sw.js).\n * `distDir` must be the value the app's next.config uses. `extraAssets` are further same-origin\n * URLs the shell needs with no network (the SQLite files the device worker loads).\n */\nexport async function buildOfflineManifest(params: {\n distDir: string;\n extraAssets?: string[];\n}): Promise<OfflineManifest> {\n const staticDir = path.join(params.distDir, \"static\");\n const files = await listFiles(staticDir);\n const assets = files\n .map((file) => `/_next/static/${path.relative(staticDir, file).split(path.sep).join(\"/\")}`)\n .concat(params.extraAssets ?? [])\n .sort();\n const buildId = await readBuildId(params.distDir, assets);\n return { buildId, assets };\n}\n\nasync function readBuildId(distDir: string, assets: string[]): Promise<string> {\n try {\n return (await readFile(path.join(distDir, \"BUILD_ID\"), \"utf8\")).trim();\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n return `dev-${createHash(\"sha256\").update(assets.join(\"\\n\")).digest(\"hex\").slice(0, 16)}`;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CURRENT_USER_STORAGE_KEY,
|
|
3
3
|
getClientApiUrl
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-47HNJQ7U.mjs";
|
|
5
5
|
import {
|
|
6
6
|
NeedsConnectionError,
|
|
7
7
|
getOfflineConfig,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "./chunk-4ZJIUTIV.mjs";
|
|
13
13
|
import {
|
|
14
14
|
getApiUrl
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-YPBIPE2B.mjs";
|
|
16
16
|
import {
|
|
17
17
|
__name
|
|
18
18
|
} from "./chunk-PAWJFY3S.mjs";
|
|
@@ -725,4 +725,4 @@ export {
|
|
|
725
725
|
splitApiUrl,
|
|
726
726
|
localTransport
|
|
727
727
|
};
|
|
728
|
-
//# sourceMappingURL=chunk-
|
|
728
|
+
//# sourceMappingURL=chunk-DWPHHUMV.mjs.map
|
|
@@ -4,7 +4,7 @@ var _chunkG4JR3SFVjs = require('./chunk-G4JR3SFV.js');
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
var
|
|
7
|
+
var _chunk77JC2UCFjs = require('./chunk-77JC2UCF.js');
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
var _chunk7QVYU63Ejs = require('./chunk-7QVYU63E.js');
|
|
@@ -40,13 +40,14 @@ var ServerSession = class {
|
|
|
40
40
|
const headerStore = await _headers.headers.call(void 0, );
|
|
41
41
|
const token = _optionalChain([cookieStore, 'access', _ => _.get, 'call', _2 => _2("token"), 'optionalAccess', _3 => _3.value]) || void 0;
|
|
42
42
|
const roles = _optionalChain([cookieStore, 'access', _4 => _4.get, 'call', _5 => _5("roles"), 'optionalAccess', _6 => _6.value]);
|
|
43
|
-
const viewportCookie = _optionalChain([cookieStore, 'access', _7 => _7.get, 'call', _8 => _8(
|
|
43
|
+
const viewportCookie = _optionalChain([cookieStore, 'access', _7 => _7.get, 'call', _8 => _8(_chunk77JC2UCFjs.VIEWPORT_COOKIE_NAME), 'optionalAccess', _9 => _9.value]);
|
|
44
44
|
return {
|
|
45
45
|
isLogged: !!token,
|
|
46
46
|
isAdministrator: !!roles && roles.includes(_chunkG4JR3SFVjs.getRoleId.call(void 0, ).Administrator),
|
|
47
47
|
token,
|
|
48
48
|
sidebarOpen: _optionalChain([cookieStore, 'access', _10 => _10.get, 'call', _11 => _11("sidebar_state"), 'optionalAccess', _12 => _12.value]) !== "false",
|
|
49
|
-
isMobile: viewportCookie ? viewportCookie === "1" : _server.userAgent.call(void 0, { headers: headerStore }).device.type === "mobile"
|
|
49
|
+
isMobile: viewportCookie ? viewportCookie === "1" : _server.userAgent.call(void 0, { headers: headerStore }).device.type === "mobile",
|
|
50
|
+
acceptLanguage: _nullishCoalesce(headerStore.get("accept-language"), () => ( void 0))
|
|
50
51
|
};
|
|
51
52
|
}
|
|
52
53
|
static async isLogged() {
|
|
@@ -88,7 +89,7 @@ var ServerSession = class {
|
|
|
88
89
|
if (!rawModules) return false;
|
|
89
90
|
const modules = JSON.parse(_pako.ungzip.call(void 0, Buffer.from(rawModules, "base64"), { toText: true }));
|
|
90
91
|
const selectedModule = modules.find((module) => module.id === params.module.moduleId);
|
|
91
|
-
return
|
|
92
|
+
return _chunk77JC2UCFjs.checkPermissionsFromServer.call(void 0, {
|
|
92
93
|
module: params.module,
|
|
93
94
|
action: params.action,
|
|
94
95
|
data: params.data,
|
|
@@ -138,4 +139,4 @@ _chunk7QVYU63Ejs.__name.call(void 0, readBuildId, "readBuildId");
|
|
|
138
139
|
|
|
139
140
|
|
|
140
141
|
exports.invalidateCacheTag = invalidateCacheTag; exports.invalidateCacheTags = invalidateCacheTags; exports.updateCacheTag = updateCacheTag; exports.ServerSession = ServerSession; exports.buildOfflineManifest = buildOfflineManifest;
|
|
141
|
-
//# sourceMappingURL=chunk-
|
|
142
|
+
//# sourceMappingURL=chunk-F3EVYBMA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/chunk-F3EVYBMA.js","../src/server/cache.ts","../src/server/ServerSession.ts","../src/offline/server/build-offline-manifest.ts"],"names":[],"mappings":"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","file":"/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/chunk-F3EVYBMA.js","sourcesContent":[null,"// Server-only utilities (NOT server actions - these cannot be called from client components)\n\nimport { revalidateTag, updateTag } from \"next/cache\";\n\nimport type { CacheProfile } from \"./cacheProfiles\";\n\nexport { CACHE_PROFILES, type CacheProfile } from \"./cacheProfiles\";\n\n/**\n * Revalidate a cache tag to invalidate cached data.\n * Next.js 16+ requires a profile parameter.\n *\n * With `\"max\"` the tagged entries are marked stale and served stale-while-revalidate\n * (a one-year window): the next request still gets the old data while fresh data loads\n * in the background. It is not an immediate invalidation. A Server Action that must\n * read its own write uses `updateCacheTag` instead.\n *\n * @param tag - The cache tag to invalidate\n * @param profile - The cache profile whose `expire` bounds how long stale data may be served (defaults to \"max\")\n */\nexport async function invalidateCacheTag(tag: string, profile: CacheProfile = \"max\"): Promise<void> {\n revalidateTag(tag, profile);\n}\n\n/**\n * Revalidate multiple cache tags.\n *\n * @param tags - Array of cache tags to invalidate\n * @param profile - The cache profile, see `invalidateCacheTag` (defaults to \"max\", stale-while-revalidate)\n */\nexport async function invalidateCacheTags(tags: string[], profile: CacheProfile = \"max\"): Promise<void> {\n for (const tag of tags) {\n revalidateTag(tag, profile);\n }\n}\n\n/**\n * Expire a cache tag immediately, so the same request reads fresh data (read-your-own-writes).\n * Wraps `updateTag`, which works in Server Actions only. Route Handlers use `invalidateCacheTag`.\n *\n * @param tag - The cache tag to expire\n */\nexport async function updateCacheTag(tag: string): Promise<void> {\n updateTag(tag);\n}\n","import { cookies, headers } from \"next/headers\";\nimport { redirect } from \"next/navigation\";\nimport { userAgent } from \"next/server\";\nimport { ungzip } from \"pako\";\nimport { Action, checkPermissionsFromServer, ModuleWithPermissions } from \"../permissions\";\nimport { getRoleId } from \"../roles/config\";\nimport { VIEWPORT_COOKIE_NAME } from \"../utils/use-mobile\";\n\nexport interface SessionSnapshot {\n isLogged: boolean;\n isAdministrator: boolean;\n token?: string;\n sidebarOpen: boolean;\n isMobile: boolean;\n /** The request's raw `Accept-Language` header; the date locale is resolved from it on the client. */\n acceptLanguage?: string;\n}\n\nexport class ServerSession {\n static async read(): Promise<SessionSnapshot> {\n const cookieStore = await cookies();\n const headerStore = await headers();\n\n const token = cookieStore.get(\"token\")?.value || undefined;\n const roles = cookieStore.get(\"roles\")?.value;\n const viewportCookie = cookieStore.get(VIEWPORT_COOKIE_NAME)?.value;\n\n return {\n isLogged: !!token,\n isAdministrator: !!roles && roles.includes(getRoleId().Administrator),\n token,\n sidebarOpen: cookieStore.get(\"sidebar_state\")?.value !== \"false\",\n isMobile: viewportCookie ? viewportCookie === \"1\" : userAgent({ headers: headerStore }).device.type === \"mobile\",\n acceptLanguage: headerStore.get(\"accept-language\") ?? undefined,\n };\n }\n\n static async isLogged() {\n const cookieStore = await cookies();\n const token = cookieStore.get(\"token\");\n\n if (!token || !token.value) return false;\n return true;\n }\n\n static async companyId() {\n const cookieStore = await cookies();\n return cookieStore.get(\"companyId\")?.value ?? \"\";\n }\n\n static async userId() {\n const cookieStore = await cookies();\n return cookieStore.get(\"userId\")?.value ?? \"\";\n }\n\n static async checkPermission<M extends ModuleWithPermissions>(params: { module: M; action: Action; data?: any }) {\n if (!(await this.hasPermissionToModule(params))) redirect(`/401`);\n }\n\n static async hasRole(roleId: string): Promise<boolean> {\n const cookieStore = await cookies();\n const roles = cookieStore.get(\"roles\")?.value;\n\n if (!roles || !roles.includes(roleId)) return false;\n\n return true;\n }\n\n static async hasFeature(featureId: string): Promise<boolean> {\n const cookieStore = await cookies();\n const features = cookieStore.get(\"features\")?.value;\n\n if (!features || !features.includes(featureId)) return false;\n\n return true;\n }\n\n static async hasPermissionToModule<M extends ModuleWithPermissions>(params: {\n module: M;\n action: Action;\n data?: any;\n }): Promise<boolean> {\n const cookieStore = await cookies();\n\n if (params.module.feature) {\n const features = cookieStore.get(\"features\")?.value;\n\n if (features && !features.includes(params.module.feature)) return false;\n }\n\n const rawModules = cookieStore.get(\"modules\")?.value;\n if (!rawModules) return false;\n\n const modules: {\n id: string;\n permissions: {\n create: boolean | string;\n read: boolean | string;\n update: boolean | string;\n delete: boolean | string;\n };\n }[] = JSON.parse(ungzip(Buffer.from(rawModules, \"base64\"), { toText: true }));\n\n const selectedModule = modules.find((module) => module.id === params.module.moduleId);\n\n return checkPermissionsFromServer({\n module: params.module,\n action: params.action,\n data: params.data,\n userId: await this.userId(),\n selectedModule: selectedModule,\n });\n\n // if (!selectedModule) return false;\n\n // if (!selectedModule.permissions[params.action]) return false;\n // if (typeof selectedModule.permissions[params.action] === \"boolean\")\n // return selectedModule.permissions[params.action] as boolean;\n\n // if (!params.data) return false;\n\n // return params.data[selectedModule.permissions[params.action] as string] === this.userId;\n }\n}\n","import { createHash } from \"node:crypto\";\nimport { readdir, readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nexport type OfflineManifest = { buildId: string; assets: string[] };\n\nasync function listFiles(dir: string): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n const files: string[] = [];\n for (const entry of entries) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) files.push(...(await listFiles(full)));\n else if (entry.isFile()) files.push(full);\n }\n return files;\n}\n\n/**\n * Describes the running build for the service worker's offline shell precache\n * (`/offline-manifest.json`): the build id from `<distDir>/BUILD_ID` and every\n * file under `<distDir>/static`, as the `/_next/static/...` URL Next serves it at.\n * `next dev` writes no BUILD_ID (its folder is `.next/dev`): the id is then `dev-` plus a hash\n * of the asset list. Dev chunk names do NOT carry content hashes (an edit rewrites a chunk under\n * the same name), so the id changes only when files appear or disappear; the service worker\n * therefore serves `/_next/static/` network first for a `dev-` build (pwa/sw.js).\n * `distDir` must be the value the app's next.config uses. `extraAssets` are further same-origin\n * URLs the shell needs with no network (the SQLite files the device worker loads).\n */\nexport async function buildOfflineManifest(params: {\n distDir: string;\n extraAssets?: string[];\n}): Promise<OfflineManifest> {\n const staticDir = path.join(params.distDir, \"static\");\n const files = await listFiles(staticDir);\n const assets = files\n .map((file) => `/_next/static/${path.relative(staticDir, file).split(path.sep).join(\"/\")}`)\n .concat(params.extraAssets ?? [])\n .sort();\n const buildId = await readBuildId(params.distDir, assets);\n return { buildId, assets };\n}\n\nasync function readBuildId(distDir: string, assets: string[]): Promise<string> {\n try {\n return (await readFile(path.join(distDir, \"BUILD_ID\"), \"utf8\")).trim();\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n return `dev-${createHash(\"sha256\").update(assets.join(\"\\n\")).digest(\"hex\").slice(0, 16)}`;\n }\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
var
|
|
4
|
+
var _chunk3VNDFKT7js = require('./chunk-3VNDFKT7.js');
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
@@ -12,7 +12,7 @@ var _chunkBQ2WAG4Kjs = require('./chunk-BQ2WAG4K.js');
|
|
|
12
12
|
var _chunkWUNSG3INjs = require('./chunk-WUNSG3IN.js');
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
var
|
|
15
|
+
var _chunkLNO2JLU3js = require('./chunk-LNO2JLU3.js');
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
var _chunk7QVYU63Ejs = require('./chunk-7QVYU63E.js');
|
|
@@ -92,7 +92,7 @@ _chunk7QVYU63Ejs.__name.call(void 0, includedFor, "includedFor");
|
|
|
92
92
|
function signedInUserResource(target) {
|
|
93
93
|
try {
|
|
94
94
|
if (target.id !== _chunkWUNSG3INjs.getOfflineConfig.call(void 0, ).getUserId()) return void 0;
|
|
95
|
-
const stored = JSON.parse(_nullishCoalesce(window.localStorage.getItem(
|
|
95
|
+
const stored = JSON.parse(_nullishCoalesce(window.localStorage.getItem(_chunk3VNDFKT7js.CURRENT_USER_STORAGE_KEY), () => ( "null")));
|
|
96
96
|
const user = _optionalChain([stored, 'optionalAccess', _7 => _7.jsonApi]);
|
|
97
97
|
if (_optionalChain([user, 'optionalAccess', _8 => _8.type]) !== target.type || _optionalChain([user, 'optionalAccess', _9 => _9.id]) !== target.id) return void 0;
|
|
98
98
|
return { type: user.type, id: user.id, attributes: _nullishCoalesce(user.attributes, () => ( {})), meta: user.meta };
|
|
@@ -571,7 +571,7 @@ _chunk7QVYU63Ejs.__name.call(void 0, matchRoute, "matchRoute");
|
|
|
571
571
|
var METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
572
572
|
function apiBases() {
|
|
573
573
|
const bases = [];
|
|
574
|
-
for (const read of [
|
|
574
|
+
for (const read of [_chunkLNO2JLU3js.getApiUrl, _chunk3VNDFKT7js.getClientApiUrl]) {
|
|
575
575
|
try {
|
|
576
576
|
const base = read();
|
|
577
577
|
if (base) bases.push(base);
|
|
@@ -725,4 +725,4 @@ _chunk7QVYU63Ejs.__name.call(void 0, localTransport, "localTransport");
|
|
|
725
725
|
|
|
726
726
|
|
|
727
727
|
exports.errorAnswer = errorAnswer; exports.needsConnectionAnswer = needsConnectionAnswer; exports.notFoundAnswer = notFoundAnswer; exports.singleAnswer = singleAnswer; exports.includedFor = includedFor; exports.SERVER_DEFAULT_ORDER_BY = SERVER_DEFAULT_ORDER_BY; exports.searchRecords = searchRecords; exports.orderRecords = orderRecords; exports.listAnswer = listAnswer; exports.orderedListAnswer = orderedListAnswer; exports.resourceListAnswer = resourceListAnswer; exports.linkKindFor = linkKindFor; exports.linkKindsOf = linkKindsOf; exports.linksOfKind = linksOfKind; exports.rootIdForResource = rootIdForResource; exports.resourceFromDocument = resourceFromDocument; exports.mergeResource = mergeResource; exports.matchPattern = matchPattern; exports.matchRoute = matchRoute; exports.splitApiUrl = splitApiUrl; exports.localTransport = localTransport;
|
|
728
|
-
//# sourceMappingURL=chunk-
|
|
728
|
+
//# sourceMappingURL=chunk-FBHUUPRV.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/chunk-53SD4YRF.js","../src/offline/generic-answers.ts","../src/offline/router.ts","../src/offline/local-transport.ts"],"names":[],"mappings":"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","file":"/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/chunk-53SD4YRF.js","sourcesContent":[null,"import { CURRENT_USER_STORAGE_KEY } from \"../features/user/data/user.storage\";\nimport { getOfflineConfig, rootKinds } from \"./config\";\nimport { getOfflineState } from \"./state\";\nimport type { DeviceStore, StoredLink, StoredRecord } from \"./store/DeviceStore\";\nimport type {\n LocalAnswerContext,\n OfflineAnswer,\n OfflineConfig,\n OfflineMethod,\n OfflineRootKind,\n OfflineRoute,\n OfflineTypeConfig,\n} from \"./types\";\n\nconst DEFAULT_PAGE_SIZE = 25;\n\n// ---------------------------------------------------------------------------------------------\n// Answers shared by every route\n// ---------------------------------------------------------------------------------------------\n\n/** A JSON:API errors document with one error, answered with `status`. */\nexport function errorAnswer(status: number, title: string, detail: string, code?: string): OfflineAnswer {\n return {\n status,\n body: { errors: [{ status: String(status), ...(code ? { code } : {}), title, detail }] },\n };\n}\n\nexport function needsConnectionAnswer(detail: string = \"Needs a connection\"): OfflineAnswer {\n return errorAnswer(503, \"Needs a connection\", detail, \"needs-connection\");\n}\n\nexport function notFoundAnswer(detail: string): OfflineAnswer {\n return errorAnswer(404, \"Not found\", detail);\n}\n\n/** A single document of one stored record, with `included` (`includedFor`). */\nexport async function singleAnswer(\n store: DeviceStore,\n record: StoredRecord,\n status: number = 200,\n): Promise<OfflineAnswer> {\n return { status, body: { data: record.resource, included: await includedFor(store, [record]) } };\n}\n\nexport function isRootWriteLocked(rootId: string | undefined): boolean {\n return !!rootId && getOfflineState().lockedRootIds.includes(rootId);\n}\n\n// ---------------------------------------------------------------------------------------------\n// Resource helpers\n// ---------------------------------------------------------------------------------------------\n\ntype ResourceIdentifier = { type: string; id: string };\n\nfunction identifiersOf(relationship: any): ResourceIdentifier[] {\n const data = relationship?.data;\n if (!data) return [];\n return (Array.isArray(data) ? data : [data]).filter((item: any) => item && item.type && item.id);\n}\n\nfunction linkedIds(links: StoredLink[], id: string): ResourceIdentifier[] {\n return links.map((link) =>\n link.fromId === id ? { type: link.toType, id: link.toId } : { type: link.fromType, id: link.fromId },\n );\n}\n\n/** Every stored resource linked to one of `ids`, in either direction, without duplicates or the records themselves. */\n/** The `{ type, id }` every relationship of a JSON:API resource points to. */\nfunction relationshipTargets(resource: any): { type: string; id: string }[] {\n const targets: { type: string; id: string }[] = [];\n for (const relationship of Object.values<any>(resource?.relationships ?? {})) {\n const data = relationship?.data;\n for (const ref of Array.isArray(data) ? data : data ? [data] : []) {\n if (ref?.type && ref?.id) targets.push({ type: ref.type, id: ref.id });\n }\n }\n return targets;\n}\n\n/**\n * The `included` of a local answer: the stored records linked to `records` (the synced links),\n * then the stored records their relationships point to, and one level further the stored records\n * the relationships of those included records point to (an NPC's root record, then the root record's\n * owner), so a model reads the same nested relationships it reads from the server.\n *\n * Stored resources point at each other (a root record at its owner, the owner at the root record's\n * company, the company back at the owner); `RehydrationFactory` skips a resource met again inside\n * its own chain, so those circles never recurse.\n */\nexport async function includedFor(store: DeviceStore, records: StoredRecord[]): Promise<any[]> {\n // 0: the answer's own records; 1: what they link to; 2: what those link to.\n const level = new Map<string, number>(records.map((record) => [`${record.type}:${record.id}`, 0]));\n const included: { resource: any; level: number }[] = [];\n\n const add = async (target: { type: string; id: string }, atLevel: number): Promise<void> => {\n const key = `${target.type}:${target.id}`;\n if (level.has(key)) return;\n level.set(key, atLevel);\n const stored = await store.getRecord(target.type, target.id);\n const resource = stored?.resource ?? signedInUserResource(target);\n if (resource) included.push({ resource, level: atLevel });\n };\n\n for (const record of records) {\n const links = [...(await store.linksFrom(record.id)), ...(await store.linksTo(record.id))];\n for (const target of linkedIds(links, record.id)) await add(target, 1);\n }\n for (const record of records) {\n for (const target of relationshipTargets(record.resource)) await add(target, 1);\n }\n for (const entry of included.filter((candidate) => candidate.level === 1)) {\n for (const target of relationshipTargets(entry.resource)) await add(target, 2);\n }\n return included.map((entry) => entry.resource);\n}\n\n/**\n * The signed-in user as a resource, when `target` is that user. Users are not kept on the device,\n * but a relationship to the signed-in user (every downloaded root record's owner) must still resolve:\n * the app tells a GM from a player by comparing the root record's owner with the signed-in user. The\n * resource comes from the copy CurrentUserProvider keeps in localStorage, without relationships.\n */\nfunction signedInUserResource(target: { type: string; id: string }): any | undefined {\n try {\n if (target.id !== getOfflineConfig().getUserId()) return undefined;\n const stored = JSON.parse(window.localStorage.getItem(CURRENT_USER_STORAGE_KEY) ?? \"null\");\n const user = stored?.jsonApi;\n if (user?.type !== target.type || user?.id !== target.id) return undefined;\n return { type: user.type, id: user.id, attributes: user.attributes ?? {}, meta: user.meta };\n } catch {\n return undefined;\n }\n}\n\nfunction attributeOf(record: StoredRecord, name: string): unknown {\n const value = record.resource?.attributes?.[name];\n if (value !== undefined && value !== null) return value;\n // The server value the sync keeps beside the resource.\n if (name === \"updatedAt\") return record.updatedAt ?? null;\n return value;\n}\n\n/** The server's default list order (`defineEntity` sets `defaultOrderBy: \"updatedAt DESC\"`). */\nexport const SERVER_DEFAULT_ORDER_BY = \"updatedAt DESC\";\n\n/** The only `orderBy` the server accepts (`buildLegacyOrderByClause`): one property, optional ASC/DESC. */\nconst ORDER_BY = /^([A-Za-z_][A-Za-z0-9_]*)(?:\\s+(ASC|DESC))?$/i;\n\nconst ISO_DATE = /^\\d{4}-\\d{2}-\\d{2}(T|$)/;\n\nfunction comparable(value: unknown): unknown {\n if (value instanceof Date) return value.getTime();\n if (typeof value === \"string\" && ISO_DATE.test(value)) {\n const time = Date.parse(value);\n if (!Number.isNaN(time)) return time;\n }\n return value;\n}\n\n/** Cypher ORDER BY on one property, ascending: null sorts after every value. */\nfunction compareAscending(left: unknown, right: unknown): number {\n const a = comparable(left);\n const b = comparable(right);\n const aNull = a === undefined || a === null;\n const bNull = b === undefined || b === null;\n if (aNull || bNull) return aNull === bNull ? 0 : aNull ? 1 : -1;\n if (typeof a === \"number\" && typeof b === \"number\") return a - b;\n if (typeof a === \"boolean\" && typeof b === \"boolean\") return Number(a) - Number(b);\n const x = String(a);\n const y = String(b);\n return x < y ? -1 : x > y ? 1 : 0;\n}\n\nexport type ListOptions = {\n /** The string attributes the server's fulltext index covers (default: every string attribute). */\n searchFields?: string[];\n /** The order with no `orderBy` (default `updatedAt DESC`, the server's). */\n defaultOrderBy?: string;\n};\n\n/**\n * `search` as the server runs it: the term is split into letter/digit tokens and a record\n * matches when every token appears, case-insensitive, inside one of its indexed string\n * attributes (the fulltext query `*token* AND ...`).\n */\nexport function searchRecords(\n records: StoredRecord[],\n query: URLSearchParams,\n options: ListOptions = {},\n): StoredRecord[] {\n const term = query.get(\"search\");\n if (term === null || term === undefined || term.trim() === \"\") return records;\n const tokens = term\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean);\n if (tokens.length === 0) return records;\n\n return records.filter((record) => {\n const attributes = record.resource?.attributes ?? {};\n const fields = options.searchFields ?? Object.keys(attributes);\n const texts = fields\n .map((field) => attributes[field])\n .filter((value): value is string => typeof value === \"string\")\n .map((value) => value.toLowerCase());\n return tokens.every((token) => texts.some((text) => text.includes(token)));\n });\n}\n\n/**\n * Orders records as the server's `ORDER BY` does for `orderBy` (`field` or `field ASC|DESC`),\n * or for the default order when there is none. `null` when the server would refuse the\n * `orderBy` (400).\n */\nexport function orderRecords(\n records: StoredRecord[],\n query: URLSearchParams,\n options: ListOptions = {},\n): StoredRecord[] | null {\n const requested = query.get(\"orderBy\")?.trim();\n const value = requested || options.defaultOrderBy?.trim() || SERVER_DEFAULT_ORDER_BY;\n const match = ORDER_BY.exec(value);\n if (!match) return null;\n const field = match[1];\n const desc = match[2]?.toUpperCase() === \"DESC\";\n return [...records].sort((left, right) => {\n const order = compareAscending(attributeOf(left, field), attributeOf(right, field));\n return desc ? -order : order;\n });\n}\n\nfunction linkUrl(base: string, query: URLSearchParams, changes: Record<string, string | undefined>): string {\n const params = new URLSearchParams(query);\n for (const [key, value] of Object.entries(changes)) {\n if (value === undefined) params.delete(key);\n else params.set(key, value);\n }\n const search = params.toString().replace(/%5B/g, \"[\").replace(/%5D/g, \"]\").replace(/%2C/g, \",\");\n return search ? `${base}?${search}` : base;\n}\n\n/**\n * One list answer for a set of stored records, as the server answers a list: `search`, then\n * `orderBy` (or the default order), then paged the way `JsonApiPaginator` pages (`fetchAll`,\n * `page[size]`, `page[offset]`), with `included`. App answers pass the records they selected.\n */\nexport async function listAnswer(\n ctx: LocalAnswerContext,\n records: StoredRecord[],\n options: ListOptions = {},\n): Promise<OfflineAnswer> {\n const { request, store } = ctx;\n const base = request.url ? request.url.split(\"?\")[0] : request.path;\n const sorted = orderRecords(searchRecords(records, request.query, options), request.query, options);\n if (!sorted) {\n return errorAnswer(400, \"Bad Request\", \"Invalid orderBy\");\n }\n\n if (request.query.get(\"fetchAll\") === \"true\") {\n return {\n status: 200,\n body: {\n data: sorted.map((record) => record.resource),\n included: await includedFor(store, sorted),\n links: { self: linkUrl(base, request.query, {}) },\n meta: { total: sorted.length },\n },\n };\n }\n\n const size = Number(request.query.get(\"page[size]\")) || DEFAULT_PAGE_SIZE;\n const offset = Number(request.query.get(\"page[offset]\")) || 0;\n const page = sorted.slice(offset, offset + size);\n\n const links: Record<string, string> = {\n self: linkUrl(base, request.query, { \"page[size]\": String(size), \"page[offset]\": undefined }),\n };\n if (offset + size < sorted.length) {\n links.next = linkUrl(base, request.query, { \"page[size]\": String(size), \"page[offset]\": String(offset + size) });\n }\n if (offset > 0) {\n links.prev = linkUrl(base, request.query, {\n \"page[size]\": String(size),\n \"page[offset]\": String(Math.max(0, offset - size)),\n });\n // The server names it `previous`; the client reads `prev`.\n links.previous = links.prev;\n }\n\n return {\n status: 200,\n body: {\n data: page.map((record) => record.resource),\n included: await includedFor(store, page),\n links,\n meta: { total: sorted.length },\n },\n };\n}\n\n/**\n * A list document of stored records in the order the caller settled, every record in one page,\n * with `included`: for an app's custom list query that has its own `ORDER BY` and no paging.\n */\nexport async function orderedListAnswer(store: DeviceStore, records: StoredRecord[]): Promise<OfflineAnswer> {\n return {\n status: 200,\n body: {\n data: records.map((record) => record.resource),\n included: await includedFor(store, records),\n meta: { total: records.length },\n },\n };\n}\n\n/**\n * A list document of resources the device computed rather than stored (a query-result\n * projection such as a search hit, built by its model), answered as they are, nothing included.\n */\nexport function resourceListAnswer(resources: any[]): OfflineAnswer {\n return { status: 200, body: { data: resources, included: [] } };\n}\n\nfunction listOptionsOf(typeConfig: OfflineTypeConfig): ListOptions {\n return { searchFields: typeConfig.searchFields, defaultOrderBy: typeConfig.defaultOrderBy };\n}\n\n// ---------------------------------------------------------------------------------------------\n// Link kinds\n// ---------------------------------------------------------------------------------------------\n\n/**\n * The kind a link written on the device carries for relationship `name`: the Neo4j type the\n * type config maps it to (the kind synced links carry), otherwise the name itself.\n */\nexport function linkKindFor(typeConfig: OfflineTypeConfig | undefined, name: string): string {\n return typeConfig?.relationshipKinds?.[name] ?? name;\n}\n\n/**\n * Every kind a link of relationship `name` may carry on the device: the name (written on the\n * device before a mapping existed) and the mapped Neo4j type (synced). `undefined` when the\n * Neo4j type is not known: then a link of any kind may be the one.\n */\nexport function linkKindsOf(typeConfig: OfflineTypeConfig | undefined, name: string): string[] | undefined {\n const mapped = typeConfig?.relationshipKinds?.[name];\n return mapped ? [name, mapped] : undefined;\n}\n\n/**\n * The stored links of `id` whose kind is one of `kinds` (Neo4j types and/or relationship\n * names). `out`: links from `id`; `in`: links to `id`; `both`: either.\n */\nexport async function linksOfKind(\n store: DeviceStore,\n id: string,\n kinds: string[],\n direction: \"out\" | \"in\" | \"both\" = \"both\",\n): Promise<StoredLink[]> {\n const result: StoredLink[] = [];\n const seen = new Set<string>();\n for (const kind of new Set(kinds)) {\n const links = [\n ...(direction !== \"in\" ? await store.linksFrom(id, kind) : []),\n ...(direction !== \"out\" ? await store.linksTo(id, kind) : []),\n ];\n for (const link of links) {\n const key = `${link.fromId}|${link.toId}|${link.kind}`;\n if (seen.has(key)) continue;\n seen.add(key);\n result.push(link);\n }\n }\n return result;\n}\n\nfunction typeConfigOf(config: OfflineConfig, type: string): OfflineTypeConfig | undefined {\n return config.types.find((typeConfig) => typeConfig.module.name === type);\n}\n\n/** The relationship names of `resource` that point to a record of `type`. */\nfunction relationshipNamesToward(resource: any, type: string): string[] {\n return Object.entries<any>(resource?.relationships ?? {})\n .filter(([, relationship]) => identifiersOf(relationship).some((identifier) => identifier.type === type))\n .map(([name]) => name);\n}\n\n// ---------------------------------------------------------------------------------------------\n// Reads\n// ---------------------------------------------------------------------------------------------\n\n/** A record that is not on the device: only the server can answer for it. */\nfunction notOnDeviceAnswer(type: string, id: string): OfflineAnswer {\n return needsConnectionAnswer(`${type} ${id} is not on this device`);\n}\n\nasync function getOne(ctx: LocalAnswerContext, type: string): Promise<OfflineAnswer> {\n const record = await ctx.store.getRecord(type, ctx.params.id);\n if (!record) return notOnDeviceAnswer(type, ctx.params.id);\n return singleAnswer(ctx.store, record);\n}\n\nasync function rootList(\n ctx: LocalAnswerContext,\n kind: OfflineRootKind,\n typeConfig: OfflineTypeConfig,\n): Promise<OfflineAnswer> {\n const rootType = kind.module.name;\n const rootId = ctx.params.rootId;\n if (!(await ctx.store.getRecord(rootType, rootId))) return notOnDeviceAnswer(rootType, rootId);\n const records = await ctx.store.listRecords({ type: typeConfig.module.name, rootId });\n return listAnswer(ctx, records, listOptionsOf(typeConfig));\n}\n\n/**\n * `GET <parent>/:parentId/<type>`: the stored records of `type` linked to the parent by the\n * relationship between the two types. That relationship is the one whose name, on either\n * record, points to the other type; its links are matched by name and by mapped Neo4j type.\n * When its Neo4j type is not known, links of any kind count.\n */\nasync function relatedList(\n ctx: LocalAnswerContext,\n config: OfflineConfig,\n typeConfig: OfflineTypeConfig,\n): Promise<OfflineAnswer> {\n const type = typeConfig.module.name;\n const { parentType, parentId } = ctx.params;\n const parent = await ctx.store.getRecord(parentType, parentId);\n if (!parent) return notOnDeviceAnswer(parentType, parentId);\n\n const links = [...(await ctx.store.linksFrom(parentId)), ...(await ctx.store.linksTo(parentId))].filter((link) =>\n link.fromId === parentId ? link.toType === type : link.fromType === type,\n );\n const candidates = await ctx.store.listRecords({\n type,\n ids: [...new Set(links.map((link) => (link.fromId === parentId ? link.toId : link.fromId)))],\n });\n\n const kinds = new Set<string>();\n let anyKind = false;\n const collect = (owner: OfflineTypeConfig | undefined, names: string[]) => {\n for (const name of names) {\n const known = linkKindsOf(owner, name);\n if (known) known.forEach((kind) => kinds.add(kind));\n else anyKind = true;\n }\n };\n collect(typeConfig, [\n ...new Set(candidates.flatMap((record) => relationshipNamesToward(record.resource, parentType))),\n ]);\n collect(typeConfigOf(config, parentType), relationshipNamesToward(parent.resource, type));\n\n const accepted = anyKind || kinds.size === 0 ? links : links.filter((link) => kinds.has(link.kind));\n const ids = new Set(accepted.map((link) => (link.fromId === parentId ? link.toId : link.fromId)));\n return listAnswer(\n ctx,\n candidates.filter((record) => ids.has(record.id)),\n listOptionsOf(typeConfig),\n );\n}\n\n// ---------------------------------------------------------------------------------------------\n// Writes\n// ---------------------------------------------------------------------------------------------\n\n/**\n * The root record a record belongs to: itself (the type of any root kind), a relationship to a\n * root of any kind, or a related stored record.\n */\nexport async function rootIdForResource(\n store: DeviceStore,\n config: OfflineConfig,\n resource: any,\n): Promise<string | undefined> {\n const rootTypes = new Set(rootKinds(config).map((kind) => kind.module.name));\n if (rootTypes.has(resource?.type) && resource.id) return resource.id;\n\n const identifiers = Object.values(resource?.relationships ?? {}).flatMap(identifiersOf);\n const root = identifiers.find((identifier) => rootTypes.has(identifier.type));\n if (root) return root.id;\n\n for (const identifier of identifiers) {\n const stored = await store.getRecord(identifier.type, identifier.id);\n if (stored?.rootId) return stored.rootId;\n }\n return undefined;\n}\n\nasync function linkRelationships(\n store: DeviceStore,\n typeConfig: OfflineTypeConfig | undefined,\n record: { type: string; id: string },\n before: Record<string, any>,\n after: Record<string, any>,\n): Promise<void> {\n for (const [name, relationship] of Object.entries(after)) {\n const previous = new Map(identifiersOf(before[name]).map((identifier) => [identifier.id, identifier]));\n const next = new Map(identifiersOf(relationship).map((identifier) => [identifier.id, identifier]));\n const kinds = linkKindsOf(typeConfig, name);\n\n for (const [id] of previous) {\n if (next.has(id)) continue;\n // Only the links of this relationship (by name or Neo4j type); any kind when it is not known.\n const links = kinds\n ? await linksOfKind(store, record.id, kinds, \"both\")\n : [...(await store.linksFrom(record.id)), ...(await store.linksTo(record.id))];\n for (const link of links) {\n if (link.fromId === id || link.toId === id) {\n await store.deleteLink({ fromId: link.fromId, toId: link.toId, kind: link.kind });\n }\n }\n }\n for (const [id, identifier] of next) {\n if (previous.has(id)) continue;\n await store.putLink({\n fromType: record.type,\n fromId: record.id,\n toType: identifier.type,\n toId: id,\n kind: linkKindFor(typeConfig, name),\n props: {},\n });\n }\n }\n}\n\n/** The `data` of a JSON:API document a model built (`createJsonApi()` or a dedicated method). */\nexport type ResourceDocumentData = {\n attributes?: Record<string, unknown>;\n relationships?: Record<string, unknown>;\n meta?: Record<string, unknown>;\n};\n\n/**\n * The resource a create document stands for, under `identity`: the document's attributes,\n * relationships and meta, as the device stores a record it creates.\n */\nexport function resourceFromDocument(document: any, identity: { type: string; id: string }): any {\n const data: ResourceDocumentData = document?.data ?? {};\n const resource: any = { type: identity.type, id: identity.id };\n if (data.attributes) resource.attributes = { ...data.attributes };\n if (data.relationships) resource.relationships = { ...data.relationships };\n if (data.meta) resource.meta = { ...data.meta };\n return resource;\n}\n\n/**\n * A stored resource with a document's `data` written over it: attributes and relationships merged\n * by name, and meta merged when the document carries one, as the device applies an edit.\n */\nexport function mergeResource(before: any, identity: { type: string; id: string }, data: ResourceDocumentData): any {\n const previous = before ?? identity;\n const resource: any = {\n ...previous,\n type: identity.type,\n id: identity.id,\n attributes: { ...(previous.attributes ?? {}), ...(data?.attributes ?? {}) },\n relationships: { ...(previous.relationships ?? {}), ...(data?.relationships ?? {}) },\n };\n if (data?.meta) resource.meta = { ...(previous.meta ?? {}), ...data.meta };\n return resource;\n}\n\nfunction newId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") return crypto.randomUUID();\n return `local-${Date.now()}-${Math.random().toString(16).slice(2)}`;\n}\n\nasync function create(ctx: LocalAnswerContext, config: OfflineConfig, type: string): Promise<OfflineAnswer> {\n const typeConfig = typeConfigOf(config, type);\n const id: string = ctx.request.body?.data?.id ?? newId();\n const resource = resourceFromDocument(ctx.request.body, { type, id });\n const rootId = await rootIdForResource(ctx.store, config, resource);\n if (!rootId) return needsConnectionAnswer(`A new ${type} that belongs to no root record needs a connection`);\n if (isRootWriteLocked(rootId)) return needsConnectionAnswer(\"The root record is locked\");\n\n const record: StoredRecord = { type, id, rootId, resource, updatedAt: null, base: null, localOnly: true };\n await ctx.store.transaction(async (store) => {\n await store.putRecord(record);\n await linkRelationships(store, typeConfig, { type, id }, {}, resource.relationships ?? {});\n await store.enqueue({\n rootId,\n method: ctx.request.method,\n path: queuePath(ctx),\n body: ctx.request.body,\n base: null,\n });\n // The type's hook, inside the write's transaction: a throw rolls the write and the enqueue back.\n if (typeConfig?.afterLocalWrite) {\n await typeConfig.afterLocalWrite({\n store,\n module: typeConfig.module,\n id,\n method: \"POST\",\n resource,\n previous: null,\n });\n }\n });\n\n return singleAnswer(ctx.store, record, 201);\n}\n\nasync function update(ctx: LocalAnswerContext, config: OfflineConfig, type: string): Promise<OfflineAnswer> {\n const id = ctx.params.id;\n const existing = await ctx.store.getRecord(type, id);\n if (!existing) return notOnDeviceAnswer(type, id);\n if (isRootWriteLocked(existing.rootId)) return needsConnectionAnswer(\"The root record is locked\");\n\n const data = ctx.request.body?.data ?? {};\n const before = existing.resource ?? { type, id };\n const resource = mergeResource(before, { type, id }, data);\n // A record created on this device has no server version to merge against.\n const base = existing.localOnly ? null : (existing.base ?? before);\n\n const typeConfig = typeConfigOf(config, type);\n const method = ctx.request.method === \"PATCH\" ? \"PATCH\" : \"PUT\";\n const record: StoredRecord = { ...existing, resource, base, localOnly: existing.localOnly };\n await ctx.store.transaction(async (store) => {\n await store.putRecord(record);\n await linkRelationships(store, typeConfig, { type, id }, before.relationships ?? {}, data.relationships ?? {});\n await store.enqueue({\n rootId: existing.rootId,\n method: ctx.request.method,\n path: queuePath(ctx),\n body: ctx.request.body,\n recordType: type,\n recordId: id,\n base,\n });\n if (typeConfig?.afterLocalWrite) {\n await typeConfig.afterLocalWrite({ store, module: typeConfig.module, id, method, resource, previous: before });\n }\n });\n\n return singleAnswer(ctx.store, record);\n}\n\nasync function remove(ctx: LocalAnswerContext, config: OfflineConfig, type: string): Promise<OfflineAnswer> {\n const id = ctx.params.id;\n const existing = await ctx.store.getRecord(type, id);\n if (!existing) return notOnDeviceAnswer(type, id);\n if (isRootWriteLocked(existing.rootId)) return needsConnectionAnswer(\"The root record is locked\");\n\n const base = existing.localOnly ? null : (existing.base ?? existing.resource);\n\n await ctx.store.transaction(async (store) => {\n const links = [...(await store.linksFrom(id)), ...(await store.linksTo(id))];\n for (const link of links) await store.deleteLink({ fromId: link.fromId, toId: link.toId, kind: link.kind });\n await store.deleteRecord(type, id);\n await store.enqueue({\n rootId: existing.rootId,\n method: \"DELETE\",\n path: queuePath(ctx),\n recordType: type,\n recordId: id,\n base,\n });\n const typeConfig = typeConfigOf(config, type);\n if (typeConfig?.afterLocalWrite) {\n await typeConfig.afterLocalWrite({\n store,\n module: typeConfig.module,\n id,\n method: \"DELETE\",\n resource: null,\n previous: existing.resource,\n });\n }\n });\n\n return { status: 204, body: undefined };\n}\n\n/** The request path as `EndpointCreator` built it, query string included. */\nexport function queuePath(ctx: Pick<LocalAnswerContext, \"request\">): string {\n const query = ctx.request.query.toString();\n return query ? `${ctx.request.path}?${query}` : ctx.request.path;\n}\n\n// ---------------------------------------------------------------------------------------------\n// The generic route list\n// ---------------------------------------------------------------------------------------------\n\nfunction readOnlyGuard(typeConfig: OfflineTypeConfig, answer: (ctx: LocalAnswerContext) => Promise<OfflineAnswer>) {\n return async (ctx: LocalAnswerContext): Promise<OfflineAnswer> =>\n typeConfig.readOnly ? needsConnectionAnswer(`${typeConfig.module.name} is read-only offline`) : answer(ctx);\n}\n\n/**\n * One set of routes per configured type, in matching order: the root record list before the\n * related list (both are `x/:id/<type>`), the related list last. These generic routes are not\n * queued by the layer: each write answer queues itself, with its base.\n */\nexport function buildGenericRoutes(config: OfflineConfig): OfflineRoute[] {\n const routes: OfflineRoute[] = [];\n const route = (method: OfflineMethod, pattern: OfflineRoute[\"pattern\"], answer: OfflineRoute[\"answer\"]) =>\n routes.push({ method, pattern, answer, queue: false });\n\n for (const typeConfig of config.types) {\n const type = typeConfig.module.name;\n route(\"GET\", [typeConfig.module, \":id\"], (ctx) => getOne(ctx, type));\n for (const kind of rootKinds(config)) {\n route(\"GET\", [kind.module, \":rootId\", typeConfig.module], (ctx) => rootList(ctx, kind, typeConfig));\n }\n route(\n \"POST\",\n [typeConfig.module],\n readOnlyGuard(typeConfig, (ctx) => create(ctx, config, type)),\n );\n route(\n \"PUT\",\n [typeConfig.module, \":id\"],\n readOnlyGuard(typeConfig, (ctx) => update(ctx, config, type)),\n );\n route(\n \"PATCH\",\n [typeConfig.module, \":id\"],\n readOnlyGuard(typeConfig, (ctx) => update(ctx, config, type)),\n );\n route(\n \"DELETE\",\n [typeConfig.module, \":id\"],\n readOnlyGuard(typeConfig, (ctx) => remove(ctx, config, type)),\n );\n }\n for (const typeConfig of config.types) {\n route(\"GET\", [\":parentType\", \":parentId\", typeConfig.module], (ctx) => relatedList(ctx, config, typeConfig));\n }\n\n return routes;\n}\n","import type { OfflineMethod, OfflineRoute, PatternSegment } from \"./types\";\n\nexport type RouteMatch = { route: OfflineRoute; params: Record<string, string> };\n\nfunction splitPath(path: string): string[] {\n const withoutQuery = path.split(\"?\")[0].split(\"#\")[0];\n return withoutQuery.split(\"/\").filter((segment) => segment.length > 0);\n}\n\n/** A Module segment may itself hold slashes (\"roots/played\"), so each one is expanded. */\nfunction expandPattern(pattern: PatternSegment[]): string[] {\n return pattern.flatMap((segment) => splitPath(typeof segment === \"string\" ? segment : segment.name));\n}\n\nexport function matchPattern(pattern: PatternSegment[], path: string): Record<string, string> | null {\n const expected = expandPattern(pattern);\n const actual = splitPath(path);\n if (expected.length !== actual.length) return null;\n\n const params: Record<string, string> = {};\n for (let index = 0; index < expected.length; index++) {\n const segment = expected[index];\n const value = decodeURIComponent(actual[index]);\n if (segment.startsWith(\":\")) {\n params[segment.slice(1)] = value;\n } else if (segment !== value) {\n return null;\n }\n }\n return params;\n}\n\n/** The first route, in the order given, whose method and pattern match. The query string is ignored. */\nexport function matchRoute(routes: OfflineRoute[], method: OfflineMethod, path: string): RouteMatch | null {\n for (const route of routes) {\n if (route.method !== method) continue;\n const params = matchPattern(route.pattern, path);\n if (params) return { route, params };\n }\n return null;\n}\n","import { getClientApiUrl } from \"../client/JsonApiClient\";\nimport type { ApiData } from \"../core/interfaces/ApiData\";\nimport { getApiUrl } from \"../unified/JsonApiRequest\";\nimport { getOfflineConfig, getOfflineStore, rootKinds } from \"./config\";\nimport {\n buildGenericRoutes,\n rootIdForResource,\n isRootWriteLocked,\n needsConnectionAnswer,\n queuePath,\n} from \"./generic-answers\";\nimport { matchRoute } from \"./router\";\nimport { setOfflineState } from \"./state\";\nimport type { DeviceStore } from \"./store/DeviceStore\";\nimport {\n NeedsConnectionError,\n type OfflineAnswer,\n type OfflineMethod,\n type OfflineRequest,\n type OfflineRoute,\n} from \"./types\";\n\nexport type LocalTransportRequest = {\n method: string;\n url: string;\n body?: any;\n files?: unknown;\n};\n\nconst METHODS: OfflineMethod[] = [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"];\n\nfunction apiBases(): string[] {\n const bases: string[] = [];\n for (const read of [getApiUrl, getClientApiUrl]) {\n try {\n const base = read();\n if (base) bases.push(base);\n } catch {\n // not configured on this side\n }\n }\n return bases;\n}\n\n/** The path relative to the API base, as `EndpointCreator` built it, and its query. */\nexport function splitApiUrl(url: string): { path: string; query: URLSearchParams } {\n let relative: string | undefined;\n for (const base of apiBases()) {\n if (url.startsWith(base)) {\n relative = url.slice(base.length);\n break;\n }\n }\n if (relative === undefined) {\n try {\n const parsed = new URL(url);\n relative = `${parsed.pathname}${parsed.search}`;\n } catch {\n relative = url;\n }\n }\n\n const [pathPart, ...queryParts] = relative.split(\"?\");\n return {\n path: pathPart.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\"),\n query: new URLSearchParams(queryParts.join(\"?\")),\n };\n}\n\nfunction toApiData(answer: OfflineAnswer): ApiData {\n const ok = answer.status >= 200 && answer.status < 300;\n return {\n ok,\n status: answer.status,\n statusText: ok ? \"OK\" : answer.status === 503 ? \"Service Unavailable\" : \"Error\",\n data: answer.body,\n };\n}\n\n/** The root record a custom write belongs to: a `<root>/:param` capture, a stored record named by a param, or the body. */\nasync function rootIdForCustomWrite(\n store: DeviceStore,\n pattern: OfflineRequest,\n params: Record<string, string>,\n rootSegmentParam: string | undefined,\n): Promise<string | undefined> {\n const config = getOfflineConfig();\n if (rootSegmentParam && params[rootSegmentParam]) return params[rootSegmentParam];\n\n for (const value of Object.values(params)) {\n for (const typeConfig of config.types) {\n const stored = await store.getRecord(typeConfig.module.name, value);\n if (stored?.rootId) return stored.rootId;\n }\n }\n return rootIdForResource(store, config, pattern.body?.data);\n}\n\nasync function refreshPendingCount(store: DeviceStore): Promise<void> {\n const entries = await store.queue();\n setOfflineState({ pending: entries.filter((entry) => entry.status !== \"conflict\").length });\n}\n\n/**\n * Answers a request from the device copy: the app's custom routes first, then the generic ones.\n * The result has the `ApiData` shape `directFetch` returns, so `translateResponse` and\n * `rehydrate()` run unchanged.\n */\nexport async function localTransport(request: LocalTransportRequest): Promise<ApiData> {\n try {\n const method = request.method.toUpperCase() as OfflineMethod;\n if (!METHODS.includes(method)) return toApiData(needsConnectionAnswer());\n if (request.files) return toApiData(needsConnectionAnswer(\"Uploading files needs a connection\"));\n\n const config = getOfflineConfig();\n const { path, query } = splitApiUrl(request.url);\n const offlineRequest: OfflineRequest = { method, path, query, body: request.body, url: request.url };\n\n const custom = matchRoute(config.routes, method, path);\n const match = custom ?? matchRoute(buildGenericRoutes(config), method, path);\n if (!match) return toApiData(needsConnectionAnswer(`${method} ${path} has no offline answer`));\n\n let store: DeviceStore;\n try {\n store = await getOfflineStore();\n } catch (error) {\n // The device copy cannot be opened here (another tab holds the file, storage refused,\n // no signed-in user): only the server can answer.\n if (error instanceof NeedsConnectionError) return toApiData(needsConnectionAnswer(error.message));\n console.warn(\"[offline] the device copy could not be opened\", error);\n return toApiData(needsConnectionAnswer(\"The device copy is not available in this tab\"));\n }\n const ctx = { request: offlineRequest, params: match.params, store };\n\n // The root record of a custom write (a `<root>/:param` capture of any root kind's module, a\n // stored record, or the body).\n const rootTypes = new Set(rootKinds(config).map((kind) => kind.module.name));\n const customRootId = async (route: OfflineRoute): Promise<string | undefined> => {\n const rootIndex = route.pattern.findIndex(\n (segment) => typeof segment !== \"string\" && rootTypes.has(segment.name),\n );\n const next = rootIndex >= 0 ? route.pattern[rootIndex + 1] : undefined;\n const rootParam = typeof next === \"string\" && next.startsWith(\":\") ? next.slice(1) : undefined;\n return rootIdForCustomWrite(store, offlineRequest, match.params, rootParam);\n };\n\n if (!custom || !custom.route.queue) {\n // A custom write that is not queued is refused on a locked root record too (GETs stay allowed).\n if (custom && method !== \"GET\") {\n const rootId = await customRootId(custom.route);\n if (rootId && isRootWriteLocked(rootId)) {\n return toApiData(needsConnectionAnswer(\"The root record is locked\"));\n }\n }\n const answer = await match.route.answer(ctx);\n if (method !== \"GET\") await refreshPendingCount(store);\n return toApiData(answer);\n }\n\n // A custom write the server must run too: refused on a locked root record, otherwise the answer\n // updates the device copy and the original request is queued in the same transaction.\n const rootId = await customRootId(custom.route);\n if (!rootId) return toApiData(needsConnectionAnswer(`${method} ${path} belongs to no root record on this device`));\n if (isRootWriteLocked(rootId)) return toApiData(needsConnectionAnswer(\"The root record is locked\"));\n\n const answer = await store.transaction(async (transactionStore) => {\n const result = await custom.route.answer({ ...ctx, store: transactionStore });\n if (result.status >= 200 && result.status < 300) {\n await transactionStore.enqueue({\n rootId,\n method,\n path: queuePath(ctx),\n body: request.body,\n base: null,\n });\n }\n return result;\n });\n await refreshPendingCount(store);\n return toApiData(answer);\n } catch (error) {\n if (error instanceof NeedsConnectionError) return toApiData(needsConnectionAnswer(error.message));\n console.error(\"[offline] local answer failed\", error);\n return toApiData({\n status: 500,\n body: {\n errors: [{ status: \"500\", title: \"Offline answer failed\", detail: String((error as Error)?.message ?? error) }],\n },\n });\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/chunk-FBHUUPRV.js","../src/offline/generic-answers.ts","../src/offline/router.ts","../src/offline/local-transport.ts"],"names":[],"mappings":"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","file":"/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/chunk-FBHUUPRV.js","sourcesContent":[null,"import { CURRENT_USER_STORAGE_KEY } from \"../features/user/data/user.storage\";\nimport { getOfflineConfig, rootKinds } from \"./config\";\nimport { getOfflineState } from \"./state\";\nimport type { DeviceStore, StoredLink, StoredRecord } from \"./store/DeviceStore\";\nimport type {\n LocalAnswerContext,\n OfflineAnswer,\n OfflineConfig,\n OfflineMethod,\n OfflineRootKind,\n OfflineRoute,\n OfflineTypeConfig,\n} from \"./types\";\n\nconst DEFAULT_PAGE_SIZE = 25;\n\n// ---------------------------------------------------------------------------------------------\n// Answers shared by every route\n// ---------------------------------------------------------------------------------------------\n\n/** A JSON:API errors document with one error, answered with `status`. */\nexport function errorAnswer(status: number, title: string, detail: string, code?: string): OfflineAnswer {\n return {\n status,\n body: { errors: [{ status: String(status), ...(code ? { code } : {}), title, detail }] },\n };\n}\n\nexport function needsConnectionAnswer(detail: string = \"Needs a connection\"): OfflineAnswer {\n return errorAnswer(503, \"Needs a connection\", detail, \"needs-connection\");\n}\n\nexport function notFoundAnswer(detail: string): OfflineAnswer {\n return errorAnswer(404, \"Not found\", detail);\n}\n\n/** A single document of one stored record, with `included` (`includedFor`). */\nexport async function singleAnswer(\n store: DeviceStore,\n record: StoredRecord,\n status: number = 200,\n): Promise<OfflineAnswer> {\n return { status, body: { data: record.resource, included: await includedFor(store, [record]) } };\n}\n\nexport function isRootWriteLocked(rootId: string | undefined): boolean {\n return !!rootId && getOfflineState().lockedRootIds.includes(rootId);\n}\n\n// ---------------------------------------------------------------------------------------------\n// Resource helpers\n// ---------------------------------------------------------------------------------------------\n\ntype ResourceIdentifier = { type: string; id: string };\n\nfunction identifiersOf(relationship: any): ResourceIdentifier[] {\n const data = relationship?.data;\n if (!data) return [];\n return (Array.isArray(data) ? data : [data]).filter((item: any) => item && item.type && item.id);\n}\n\nfunction linkedIds(links: StoredLink[], id: string): ResourceIdentifier[] {\n return links.map((link) =>\n link.fromId === id ? { type: link.toType, id: link.toId } : { type: link.fromType, id: link.fromId },\n );\n}\n\n/** Every stored resource linked to one of `ids`, in either direction, without duplicates or the records themselves. */\n/** The `{ type, id }` every relationship of a JSON:API resource points to. */\nfunction relationshipTargets(resource: any): { type: string; id: string }[] {\n const targets: { type: string; id: string }[] = [];\n for (const relationship of Object.values<any>(resource?.relationships ?? {})) {\n const data = relationship?.data;\n for (const ref of Array.isArray(data) ? data : data ? [data] : []) {\n if (ref?.type && ref?.id) targets.push({ type: ref.type, id: ref.id });\n }\n }\n return targets;\n}\n\n/**\n * The `included` of a local answer: the stored records linked to `records` (the synced links),\n * then the stored records their relationships point to, and one level further the stored records\n * the relationships of those included records point to (an NPC's root record, then the root record's\n * owner), so a model reads the same nested relationships it reads from the server.\n *\n * Stored resources point at each other (a root record at its owner, the owner at the root record's\n * company, the company back at the owner); `RehydrationFactory` skips a resource met again inside\n * its own chain, so those circles never recurse.\n */\nexport async function includedFor(store: DeviceStore, records: StoredRecord[]): Promise<any[]> {\n // 0: the answer's own records; 1: what they link to; 2: what those link to.\n const level = new Map<string, number>(records.map((record) => [`${record.type}:${record.id}`, 0]));\n const included: { resource: any; level: number }[] = [];\n\n const add = async (target: { type: string; id: string }, atLevel: number): Promise<void> => {\n const key = `${target.type}:${target.id}`;\n if (level.has(key)) return;\n level.set(key, atLevel);\n const stored = await store.getRecord(target.type, target.id);\n const resource = stored?.resource ?? signedInUserResource(target);\n if (resource) included.push({ resource, level: atLevel });\n };\n\n for (const record of records) {\n const links = [...(await store.linksFrom(record.id)), ...(await store.linksTo(record.id))];\n for (const target of linkedIds(links, record.id)) await add(target, 1);\n }\n for (const record of records) {\n for (const target of relationshipTargets(record.resource)) await add(target, 1);\n }\n for (const entry of included.filter((candidate) => candidate.level === 1)) {\n for (const target of relationshipTargets(entry.resource)) await add(target, 2);\n }\n return included.map((entry) => entry.resource);\n}\n\n/**\n * The signed-in user as a resource, when `target` is that user. Users are not kept on the device,\n * but a relationship to the signed-in user (every downloaded root record's owner) must still resolve:\n * the app tells a GM from a player by comparing the root record's owner with the signed-in user. The\n * resource comes from the copy CurrentUserProvider keeps in localStorage, without relationships.\n */\nfunction signedInUserResource(target: { type: string; id: string }): any | undefined {\n try {\n if (target.id !== getOfflineConfig().getUserId()) return undefined;\n const stored = JSON.parse(window.localStorage.getItem(CURRENT_USER_STORAGE_KEY) ?? \"null\");\n const user = stored?.jsonApi;\n if (user?.type !== target.type || user?.id !== target.id) return undefined;\n return { type: user.type, id: user.id, attributes: user.attributes ?? {}, meta: user.meta };\n } catch {\n return undefined;\n }\n}\n\nfunction attributeOf(record: StoredRecord, name: string): unknown {\n const value = record.resource?.attributes?.[name];\n if (value !== undefined && value !== null) return value;\n // The server value the sync keeps beside the resource.\n if (name === \"updatedAt\") return record.updatedAt ?? null;\n return value;\n}\n\n/** The server's default list order (`defineEntity` sets `defaultOrderBy: \"updatedAt DESC\"`). */\nexport const SERVER_DEFAULT_ORDER_BY = \"updatedAt DESC\";\n\n/** The only `orderBy` the server accepts (`buildLegacyOrderByClause`): one property, optional ASC/DESC. */\nconst ORDER_BY = /^([A-Za-z_][A-Za-z0-9_]*)(?:\\s+(ASC|DESC))?$/i;\n\nconst ISO_DATE = /^\\d{4}-\\d{2}-\\d{2}(T|$)/;\n\nfunction comparable(value: unknown): unknown {\n if (value instanceof Date) return value.getTime();\n if (typeof value === \"string\" && ISO_DATE.test(value)) {\n const time = Date.parse(value);\n if (!Number.isNaN(time)) return time;\n }\n return value;\n}\n\n/** Cypher ORDER BY on one property, ascending: null sorts after every value. */\nfunction compareAscending(left: unknown, right: unknown): number {\n const a = comparable(left);\n const b = comparable(right);\n const aNull = a === undefined || a === null;\n const bNull = b === undefined || b === null;\n if (aNull || bNull) return aNull === bNull ? 0 : aNull ? 1 : -1;\n if (typeof a === \"number\" && typeof b === \"number\") return a - b;\n if (typeof a === \"boolean\" && typeof b === \"boolean\") return Number(a) - Number(b);\n const x = String(a);\n const y = String(b);\n return x < y ? -1 : x > y ? 1 : 0;\n}\n\nexport type ListOptions = {\n /** The string attributes the server's fulltext index covers (default: every string attribute). */\n searchFields?: string[];\n /** The order with no `orderBy` (default `updatedAt DESC`, the server's). */\n defaultOrderBy?: string;\n};\n\n/**\n * `search` as the server runs it: the term is split into letter/digit tokens and a record\n * matches when every token appears, case-insensitive, inside one of its indexed string\n * attributes (the fulltext query `*token* AND ...`).\n */\nexport function searchRecords(\n records: StoredRecord[],\n query: URLSearchParams,\n options: ListOptions = {},\n): StoredRecord[] {\n const term = query.get(\"search\");\n if (term === null || term === undefined || term.trim() === \"\") return records;\n const tokens = term\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean);\n if (tokens.length === 0) return records;\n\n return records.filter((record) => {\n const attributes = record.resource?.attributes ?? {};\n const fields = options.searchFields ?? Object.keys(attributes);\n const texts = fields\n .map((field) => attributes[field])\n .filter((value): value is string => typeof value === \"string\")\n .map((value) => value.toLowerCase());\n return tokens.every((token) => texts.some((text) => text.includes(token)));\n });\n}\n\n/**\n * Orders records as the server's `ORDER BY` does for `orderBy` (`field` or `field ASC|DESC`),\n * or for the default order when there is none. `null` when the server would refuse the\n * `orderBy` (400).\n */\nexport function orderRecords(\n records: StoredRecord[],\n query: URLSearchParams,\n options: ListOptions = {},\n): StoredRecord[] | null {\n const requested = query.get(\"orderBy\")?.trim();\n const value = requested || options.defaultOrderBy?.trim() || SERVER_DEFAULT_ORDER_BY;\n const match = ORDER_BY.exec(value);\n if (!match) return null;\n const field = match[1];\n const desc = match[2]?.toUpperCase() === \"DESC\";\n return [...records].sort((left, right) => {\n const order = compareAscending(attributeOf(left, field), attributeOf(right, field));\n return desc ? -order : order;\n });\n}\n\nfunction linkUrl(base: string, query: URLSearchParams, changes: Record<string, string | undefined>): string {\n const params = new URLSearchParams(query);\n for (const [key, value] of Object.entries(changes)) {\n if (value === undefined) params.delete(key);\n else params.set(key, value);\n }\n const search = params.toString().replace(/%5B/g, \"[\").replace(/%5D/g, \"]\").replace(/%2C/g, \",\");\n return search ? `${base}?${search}` : base;\n}\n\n/**\n * One list answer for a set of stored records, as the server answers a list: `search`, then\n * `orderBy` (or the default order), then paged the way `JsonApiPaginator` pages (`fetchAll`,\n * `page[size]`, `page[offset]`), with `included`. App answers pass the records they selected.\n */\nexport async function listAnswer(\n ctx: LocalAnswerContext,\n records: StoredRecord[],\n options: ListOptions = {},\n): Promise<OfflineAnswer> {\n const { request, store } = ctx;\n const base = request.url ? request.url.split(\"?\")[0] : request.path;\n const sorted = orderRecords(searchRecords(records, request.query, options), request.query, options);\n if (!sorted) {\n return errorAnswer(400, \"Bad Request\", \"Invalid orderBy\");\n }\n\n if (request.query.get(\"fetchAll\") === \"true\") {\n return {\n status: 200,\n body: {\n data: sorted.map((record) => record.resource),\n included: await includedFor(store, sorted),\n links: { self: linkUrl(base, request.query, {}) },\n meta: { total: sorted.length },\n },\n };\n }\n\n const size = Number(request.query.get(\"page[size]\")) || DEFAULT_PAGE_SIZE;\n const offset = Number(request.query.get(\"page[offset]\")) || 0;\n const page = sorted.slice(offset, offset + size);\n\n const links: Record<string, string> = {\n self: linkUrl(base, request.query, { \"page[size]\": String(size), \"page[offset]\": undefined }),\n };\n if (offset + size < sorted.length) {\n links.next = linkUrl(base, request.query, { \"page[size]\": String(size), \"page[offset]\": String(offset + size) });\n }\n if (offset > 0) {\n links.prev = linkUrl(base, request.query, {\n \"page[size]\": String(size),\n \"page[offset]\": String(Math.max(0, offset - size)),\n });\n // The server names it `previous`; the client reads `prev`.\n links.previous = links.prev;\n }\n\n return {\n status: 200,\n body: {\n data: page.map((record) => record.resource),\n included: await includedFor(store, page),\n links,\n meta: { total: sorted.length },\n },\n };\n}\n\n/**\n * A list document of stored records in the order the caller settled, every record in one page,\n * with `included`: for an app's custom list query that has its own `ORDER BY` and no paging.\n */\nexport async function orderedListAnswer(store: DeviceStore, records: StoredRecord[]): Promise<OfflineAnswer> {\n return {\n status: 200,\n body: {\n data: records.map((record) => record.resource),\n included: await includedFor(store, records),\n meta: { total: records.length },\n },\n };\n}\n\n/**\n * A list document of resources the device computed rather than stored (a query-result\n * projection such as a search hit, built by its model), answered as they are, nothing included.\n */\nexport function resourceListAnswer(resources: any[]): OfflineAnswer {\n return { status: 200, body: { data: resources, included: [] } };\n}\n\nfunction listOptionsOf(typeConfig: OfflineTypeConfig): ListOptions {\n return { searchFields: typeConfig.searchFields, defaultOrderBy: typeConfig.defaultOrderBy };\n}\n\n// ---------------------------------------------------------------------------------------------\n// Link kinds\n// ---------------------------------------------------------------------------------------------\n\n/**\n * The kind a link written on the device carries for relationship `name`: the Neo4j type the\n * type config maps it to (the kind synced links carry), otherwise the name itself.\n */\nexport function linkKindFor(typeConfig: OfflineTypeConfig | undefined, name: string): string {\n return typeConfig?.relationshipKinds?.[name] ?? name;\n}\n\n/**\n * Every kind a link of relationship `name` may carry on the device: the name (written on the\n * device before a mapping existed) and the mapped Neo4j type (synced). `undefined` when the\n * Neo4j type is not known: then a link of any kind may be the one.\n */\nexport function linkKindsOf(typeConfig: OfflineTypeConfig | undefined, name: string): string[] | undefined {\n const mapped = typeConfig?.relationshipKinds?.[name];\n return mapped ? [name, mapped] : undefined;\n}\n\n/**\n * The stored links of `id` whose kind is one of `kinds` (Neo4j types and/or relationship\n * names). `out`: links from `id`; `in`: links to `id`; `both`: either.\n */\nexport async function linksOfKind(\n store: DeviceStore,\n id: string,\n kinds: string[],\n direction: \"out\" | \"in\" | \"both\" = \"both\",\n): Promise<StoredLink[]> {\n const result: StoredLink[] = [];\n const seen = new Set<string>();\n for (const kind of new Set(kinds)) {\n const links = [\n ...(direction !== \"in\" ? await store.linksFrom(id, kind) : []),\n ...(direction !== \"out\" ? await store.linksTo(id, kind) : []),\n ];\n for (const link of links) {\n const key = `${link.fromId}|${link.toId}|${link.kind}`;\n if (seen.has(key)) continue;\n seen.add(key);\n result.push(link);\n }\n }\n return result;\n}\n\nfunction typeConfigOf(config: OfflineConfig, type: string): OfflineTypeConfig | undefined {\n return config.types.find((typeConfig) => typeConfig.module.name === type);\n}\n\n/** The relationship names of `resource` that point to a record of `type`. */\nfunction relationshipNamesToward(resource: any, type: string): string[] {\n return Object.entries<any>(resource?.relationships ?? {})\n .filter(([, relationship]) => identifiersOf(relationship).some((identifier) => identifier.type === type))\n .map(([name]) => name);\n}\n\n// ---------------------------------------------------------------------------------------------\n// Reads\n// ---------------------------------------------------------------------------------------------\n\n/** A record that is not on the device: only the server can answer for it. */\nfunction notOnDeviceAnswer(type: string, id: string): OfflineAnswer {\n return needsConnectionAnswer(`${type} ${id} is not on this device`);\n}\n\nasync function getOne(ctx: LocalAnswerContext, type: string): Promise<OfflineAnswer> {\n const record = await ctx.store.getRecord(type, ctx.params.id);\n if (!record) return notOnDeviceAnswer(type, ctx.params.id);\n return singleAnswer(ctx.store, record);\n}\n\nasync function rootList(\n ctx: LocalAnswerContext,\n kind: OfflineRootKind,\n typeConfig: OfflineTypeConfig,\n): Promise<OfflineAnswer> {\n const rootType = kind.module.name;\n const rootId = ctx.params.rootId;\n if (!(await ctx.store.getRecord(rootType, rootId))) return notOnDeviceAnswer(rootType, rootId);\n const records = await ctx.store.listRecords({ type: typeConfig.module.name, rootId });\n return listAnswer(ctx, records, listOptionsOf(typeConfig));\n}\n\n/**\n * `GET <parent>/:parentId/<type>`: the stored records of `type` linked to the parent by the\n * relationship between the two types. That relationship is the one whose name, on either\n * record, points to the other type; its links are matched by name and by mapped Neo4j type.\n * When its Neo4j type is not known, links of any kind count.\n */\nasync function relatedList(\n ctx: LocalAnswerContext,\n config: OfflineConfig,\n typeConfig: OfflineTypeConfig,\n): Promise<OfflineAnswer> {\n const type = typeConfig.module.name;\n const { parentType, parentId } = ctx.params;\n const parent = await ctx.store.getRecord(parentType, parentId);\n if (!parent) return notOnDeviceAnswer(parentType, parentId);\n\n const links = [...(await ctx.store.linksFrom(parentId)), ...(await ctx.store.linksTo(parentId))].filter((link) =>\n link.fromId === parentId ? link.toType === type : link.fromType === type,\n );\n const candidates = await ctx.store.listRecords({\n type,\n ids: [...new Set(links.map((link) => (link.fromId === parentId ? link.toId : link.fromId)))],\n });\n\n const kinds = new Set<string>();\n let anyKind = false;\n const collect = (owner: OfflineTypeConfig | undefined, names: string[]) => {\n for (const name of names) {\n const known = linkKindsOf(owner, name);\n if (known) known.forEach((kind) => kinds.add(kind));\n else anyKind = true;\n }\n };\n collect(typeConfig, [\n ...new Set(candidates.flatMap((record) => relationshipNamesToward(record.resource, parentType))),\n ]);\n collect(typeConfigOf(config, parentType), relationshipNamesToward(parent.resource, type));\n\n const accepted = anyKind || kinds.size === 0 ? links : links.filter((link) => kinds.has(link.kind));\n const ids = new Set(accepted.map((link) => (link.fromId === parentId ? link.toId : link.fromId)));\n return listAnswer(\n ctx,\n candidates.filter((record) => ids.has(record.id)),\n listOptionsOf(typeConfig),\n );\n}\n\n// ---------------------------------------------------------------------------------------------\n// Writes\n// ---------------------------------------------------------------------------------------------\n\n/**\n * The root record a record belongs to: itself (the type of any root kind), a relationship to a\n * root of any kind, or a related stored record.\n */\nexport async function rootIdForResource(\n store: DeviceStore,\n config: OfflineConfig,\n resource: any,\n): Promise<string | undefined> {\n const rootTypes = new Set(rootKinds(config).map((kind) => kind.module.name));\n if (rootTypes.has(resource?.type) && resource.id) return resource.id;\n\n const identifiers = Object.values(resource?.relationships ?? {}).flatMap(identifiersOf);\n const root = identifiers.find((identifier) => rootTypes.has(identifier.type));\n if (root) return root.id;\n\n for (const identifier of identifiers) {\n const stored = await store.getRecord(identifier.type, identifier.id);\n if (stored?.rootId) return stored.rootId;\n }\n return undefined;\n}\n\nasync function linkRelationships(\n store: DeviceStore,\n typeConfig: OfflineTypeConfig | undefined,\n record: { type: string; id: string },\n before: Record<string, any>,\n after: Record<string, any>,\n): Promise<void> {\n for (const [name, relationship] of Object.entries(after)) {\n const previous = new Map(identifiersOf(before[name]).map((identifier) => [identifier.id, identifier]));\n const next = new Map(identifiersOf(relationship).map((identifier) => [identifier.id, identifier]));\n const kinds = linkKindsOf(typeConfig, name);\n\n for (const [id] of previous) {\n if (next.has(id)) continue;\n // Only the links of this relationship (by name or Neo4j type); any kind when it is not known.\n const links = kinds\n ? await linksOfKind(store, record.id, kinds, \"both\")\n : [...(await store.linksFrom(record.id)), ...(await store.linksTo(record.id))];\n for (const link of links) {\n if (link.fromId === id || link.toId === id) {\n await store.deleteLink({ fromId: link.fromId, toId: link.toId, kind: link.kind });\n }\n }\n }\n for (const [id, identifier] of next) {\n if (previous.has(id)) continue;\n await store.putLink({\n fromType: record.type,\n fromId: record.id,\n toType: identifier.type,\n toId: id,\n kind: linkKindFor(typeConfig, name),\n props: {},\n });\n }\n }\n}\n\n/** The `data` of a JSON:API document a model built (`createJsonApi()` or a dedicated method). */\nexport type ResourceDocumentData = {\n attributes?: Record<string, unknown>;\n relationships?: Record<string, unknown>;\n meta?: Record<string, unknown>;\n};\n\n/**\n * The resource a create document stands for, under `identity`: the document's attributes,\n * relationships and meta, as the device stores a record it creates.\n */\nexport function resourceFromDocument(document: any, identity: { type: string; id: string }): any {\n const data: ResourceDocumentData = document?.data ?? {};\n const resource: any = { type: identity.type, id: identity.id };\n if (data.attributes) resource.attributes = { ...data.attributes };\n if (data.relationships) resource.relationships = { ...data.relationships };\n if (data.meta) resource.meta = { ...data.meta };\n return resource;\n}\n\n/**\n * A stored resource with a document's `data` written over it: attributes and relationships merged\n * by name, and meta merged when the document carries one, as the device applies an edit.\n */\nexport function mergeResource(before: any, identity: { type: string; id: string }, data: ResourceDocumentData): any {\n const previous = before ?? identity;\n const resource: any = {\n ...previous,\n type: identity.type,\n id: identity.id,\n attributes: { ...(previous.attributes ?? {}), ...(data?.attributes ?? {}) },\n relationships: { ...(previous.relationships ?? {}), ...(data?.relationships ?? {}) },\n };\n if (data?.meta) resource.meta = { ...(previous.meta ?? {}), ...data.meta };\n return resource;\n}\n\nfunction newId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") return crypto.randomUUID();\n return `local-${Date.now()}-${Math.random().toString(16).slice(2)}`;\n}\n\nasync function create(ctx: LocalAnswerContext, config: OfflineConfig, type: string): Promise<OfflineAnswer> {\n const typeConfig = typeConfigOf(config, type);\n const id: string = ctx.request.body?.data?.id ?? newId();\n const resource = resourceFromDocument(ctx.request.body, { type, id });\n const rootId = await rootIdForResource(ctx.store, config, resource);\n if (!rootId) return needsConnectionAnswer(`A new ${type} that belongs to no root record needs a connection`);\n if (isRootWriteLocked(rootId)) return needsConnectionAnswer(\"The root record is locked\");\n\n const record: StoredRecord = { type, id, rootId, resource, updatedAt: null, base: null, localOnly: true };\n await ctx.store.transaction(async (store) => {\n await store.putRecord(record);\n await linkRelationships(store, typeConfig, { type, id }, {}, resource.relationships ?? {});\n await store.enqueue({\n rootId,\n method: ctx.request.method,\n path: queuePath(ctx),\n body: ctx.request.body,\n base: null,\n });\n // The type's hook, inside the write's transaction: a throw rolls the write and the enqueue back.\n if (typeConfig?.afterLocalWrite) {\n await typeConfig.afterLocalWrite({\n store,\n module: typeConfig.module,\n id,\n method: \"POST\",\n resource,\n previous: null,\n });\n }\n });\n\n return singleAnswer(ctx.store, record, 201);\n}\n\nasync function update(ctx: LocalAnswerContext, config: OfflineConfig, type: string): Promise<OfflineAnswer> {\n const id = ctx.params.id;\n const existing = await ctx.store.getRecord(type, id);\n if (!existing) return notOnDeviceAnswer(type, id);\n if (isRootWriteLocked(existing.rootId)) return needsConnectionAnswer(\"The root record is locked\");\n\n const data = ctx.request.body?.data ?? {};\n const before = existing.resource ?? { type, id };\n const resource = mergeResource(before, { type, id }, data);\n // A record created on this device has no server version to merge against.\n const base = existing.localOnly ? null : (existing.base ?? before);\n\n const typeConfig = typeConfigOf(config, type);\n const method = ctx.request.method === \"PATCH\" ? \"PATCH\" : \"PUT\";\n const record: StoredRecord = { ...existing, resource, base, localOnly: existing.localOnly };\n await ctx.store.transaction(async (store) => {\n await store.putRecord(record);\n await linkRelationships(store, typeConfig, { type, id }, before.relationships ?? {}, data.relationships ?? {});\n await store.enqueue({\n rootId: existing.rootId,\n method: ctx.request.method,\n path: queuePath(ctx),\n body: ctx.request.body,\n recordType: type,\n recordId: id,\n base,\n });\n if (typeConfig?.afterLocalWrite) {\n await typeConfig.afterLocalWrite({ store, module: typeConfig.module, id, method, resource, previous: before });\n }\n });\n\n return singleAnswer(ctx.store, record);\n}\n\nasync function remove(ctx: LocalAnswerContext, config: OfflineConfig, type: string): Promise<OfflineAnswer> {\n const id = ctx.params.id;\n const existing = await ctx.store.getRecord(type, id);\n if (!existing) return notOnDeviceAnswer(type, id);\n if (isRootWriteLocked(existing.rootId)) return needsConnectionAnswer(\"The root record is locked\");\n\n const base = existing.localOnly ? null : (existing.base ?? existing.resource);\n\n await ctx.store.transaction(async (store) => {\n const links = [...(await store.linksFrom(id)), ...(await store.linksTo(id))];\n for (const link of links) await store.deleteLink({ fromId: link.fromId, toId: link.toId, kind: link.kind });\n await store.deleteRecord(type, id);\n await store.enqueue({\n rootId: existing.rootId,\n method: \"DELETE\",\n path: queuePath(ctx),\n recordType: type,\n recordId: id,\n base,\n });\n const typeConfig = typeConfigOf(config, type);\n if (typeConfig?.afterLocalWrite) {\n await typeConfig.afterLocalWrite({\n store,\n module: typeConfig.module,\n id,\n method: \"DELETE\",\n resource: null,\n previous: existing.resource,\n });\n }\n });\n\n return { status: 204, body: undefined };\n}\n\n/** The request path as `EndpointCreator` built it, query string included. */\nexport function queuePath(ctx: Pick<LocalAnswerContext, \"request\">): string {\n const query = ctx.request.query.toString();\n return query ? `${ctx.request.path}?${query}` : ctx.request.path;\n}\n\n// ---------------------------------------------------------------------------------------------\n// The generic route list\n// ---------------------------------------------------------------------------------------------\n\nfunction readOnlyGuard(typeConfig: OfflineTypeConfig, answer: (ctx: LocalAnswerContext) => Promise<OfflineAnswer>) {\n return async (ctx: LocalAnswerContext): Promise<OfflineAnswer> =>\n typeConfig.readOnly ? needsConnectionAnswer(`${typeConfig.module.name} is read-only offline`) : answer(ctx);\n}\n\n/**\n * One set of routes per configured type, in matching order: the root record list before the\n * related list (both are `x/:id/<type>`), the related list last. These generic routes are not\n * queued by the layer: each write answer queues itself, with its base.\n */\nexport function buildGenericRoutes(config: OfflineConfig): OfflineRoute[] {\n const routes: OfflineRoute[] = [];\n const route = (method: OfflineMethod, pattern: OfflineRoute[\"pattern\"], answer: OfflineRoute[\"answer\"]) =>\n routes.push({ method, pattern, answer, queue: false });\n\n for (const typeConfig of config.types) {\n const type = typeConfig.module.name;\n route(\"GET\", [typeConfig.module, \":id\"], (ctx) => getOne(ctx, type));\n for (const kind of rootKinds(config)) {\n route(\"GET\", [kind.module, \":rootId\", typeConfig.module], (ctx) => rootList(ctx, kind, typeConfig));\n }\n route(\n \"POST\",\n [typeConfig.module],\n readOnlyGuard(typeConfig, (ctx) => create(ctx, config, type)),\n );\n route(\n \"PUT\",\n [typeConfig.module, \":id\"],\n readOnlyGuard(typeConfig, (ctx) => update(ctx, config, type)),\n );\n route(\n \"PATCH\",\n [typeConfig.module, \":id\"],\n readOnlyGuard(typeConfig, (ctx) => update(ctx, config, type)),\n );\n route(\n \"DELETE\",\n [typeConfig.module, \":id\"],\n readOnlyGuard(typeConfig, (ctx) => remove(ctx, config, type)),\n );\n }\n for (const typeConfig of config.types) {\n route(\"GET\", [\":parentType\", \":parentId\", typeConfig.module], (ctx) => relatedList(ctx, config, typeConfig));\n }\n\n return routes;\n}\n","import type { OfflineMethod, OfflineRoute, PatternSegment } from \"./types\";\n\nexport type RouteMatch = { route: OfflineRoute; params: Record<string, string> };\n\nfunction splitPath(path: string): string[] {\n const withoutQuery = path.split(\"?\")[0].split(\"#\")[0];\n return withoutQuery.split(\"/\").filter((segment) => segment.length > 0);\n}\n\n/** A Module segment may itself hold slashes (\"roots/played\"), so each one is expanded. */\nfunction expandPattern(pattern: PatternSegment[]): string[] {\n return pattern.flatMap((segment) => splitPath(typeof segment === \"string\" ? segment : segment.name));\n}\n\nexport function matchPattern(pattern: PatternSegment[], path: string): Record<string, string> | null {\n const expected = expandPattern(pattern);\n const actual = splitPath(path);\n if (expected.length !== actual.length) return null;\n\n const params: Record<string, string> = {};\n for (let index = 0; index < expected.length; index++) {\n const segment = expected[index];\n const value = decodeURIComponent(actual[index]);\n if (segment.startsWith(\":\")) {\n params[segment.slice(1)] = value;\n } else if (segment !== value) {\n return null;\n }\n }\n return params;\n}\n\n/** The first route, in the order given, whose method and pattern match. The query string is ignored. */\nexport function matchRoute(routes: OfflineRoute[], method: OfflineMethod, path: string): RouteMatch | null {\n for (const route of routes) {\n if (route.method !== method) continue;\n const params = matchPattern(route.pattern, path);\n if (params) return { route, params };\n }\n return null;\n}\n","import { getClientApiUrl } from \"../client/JsonApiClient\";\nimport type { ApiData } from \"../core/interfaces/ApiData\";\nimport { getApiUrl } from \"../unified/JsonApiRequest\";\nimport { getOfflineConfig, getOfflineStore, rootKinds } from \"./config\";\nimport {\n buildGenericRoutes,\n rootIdForResource,\n isRootWriteLocked,\n needsConnectionAnswer,\n queuePath,\n} from \"./generic-answers\";\nimport { matchRoute } from \"./router\";\nimport { setOfflineState } from \"./state\";\nimport type { DeviceStore } from \"./store/DeviceStore\";\nimport {\n NeedsConnectionError,\n type OfflineAnswer,\n type OfflineMethod,\n type OfflineRequest,\n type OfflineRoute,\n} from \"./types\";\n\nexport type LocalTransportRequest = {\n method: string;\n url: string;\n body?: any;\n files?: unknown;\n};\n\nconst METHODS: OfflineMethod[] = [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"];\n\nfunction apiBases(): string[] {\n const bases: string[] = [];\n for (const read of [getApiUrl, getClientApiUrl]) {\n try {\n const base = read();\n if (base) bases.push(base);\n } catch {\n // not configured on this side\n }\n }\n return bases;\n}\n\n/** The path relative to the API base, as `EndpointCreator` built it, and its query. */\nexport function splitApiUrl(url: string): { path: string; query: URLSearchParams } {\n let relative: string | undefined;\n for (const base of apiBases()) {\n if (url.startsWith(base)) {\n relative = url.slice(base.length);\n break;\n }\n }\n if (relative === undefined) {\n try {\n const parsed = new URL(url);\n relative = `${parsed.pathname}${parsed.search}`;\n } catch {\n relative = url;\n }\n }\n\n const [pathPart, ...queryParts] = relative.split(\"?\");\n return {\n path: pathPart.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\"),\n query: new URLSearchParams(queryParts.join(\"?\")),\n };\n}\n\nfunction toApiData(answer: OfflineAnswer): ApiData {\n const ok = answer.status >= 200 && answer.status < 300;\n return {\n ok,\n status: answer.status,\n statusText: ok ? \"OK\" : answer.status === 503 ? \"Service Unavailable\" : \"Error\",\n data: answer.body,\n };\n}\n\n/** The root record a custom write belongs to: a `<root>/:param` capture, a stored record named by a param, or the body. */\nasync function rootIdForCustomWrite(\n store: DeviceStore,\n pattern: OfflineRequest,\n params: Record<string, string>,\n rootSegmentParam: string | undefined,\n): Promise<string | undefined> {\n const config = getOfflineConfig();\n if (rootSegmentParam && params[rootSegmentParam]) return params[rootSegmentParam];\n\n for (const value of Object.values(params)) {\n for (const typeConfig of config.types) {\n const stored = await store.getRecord(typeConfig.module.name, value);\n if (stored?.rootId) return stored.rootId;\n }\n }\n return rootIdForResource(store, config, pattern.body?.data);\n}\n\nasync function refreshPendingCount(store: DeviceStore): Promise<void> {\n const entries = await store.queue();\n setOfflineState({ pending: entries.filter((entry) => entry.status !== \"conflict\").length });\n}\n\n/**\n * Answers a request from the device copy: the app's custom routes first, then the generic ones.\n * The result has the `ApiData` shape `directFetch` returns, so `translateResponse` and\n * `rehydrate()` run unchanged.\n */\nexport async function localTransport(request: LocalTransportRequest): Promise<ApiData> {\n try {\n const method = request.method.toUpperCase() as OfflineMethod;\n if (!METHODS.includes(method)) return toApiData(needsConnectionAnswer());\n if (request.files) return toApiData(needsConnectionAnswer(\"Uploading files needs a connection\"));\n\n const config = getOfflineConfig();\n const { path, query } = splitApiUrl(request.url);\n const offlineRequest: OfflineRequest = { method, path, query, body: request.body, url: request.url };\n\n const custom = matchRoute(config.routes, method, path);\n const match = custom ?? matchRoute(buildGenericRoutes(config), method, path);\n if (!match) return toApiData(needsConnectionAnswer(`${method} ${path} has no offline answer`));\n\n let store: DeviceStore;\n try {\n store = await getOfflineStore();\n } catch (error) {\n // The device copy cannot be opened here (another tab holds the file, storage refused,\n // no signed-in user): only the server can answer.\n if (error instanceof NeedsConnectionError) return toApiData(needsConnectionAnswer(error.message));\n console.warn(\"[offline] the device copy could not be opened\", error);\n return toApiData(needsConnectionAnswer(\"The device copy is not available in this tab\"));\n }\n const ctx = { request: offlineRequest, params: match.params, store };\n\n // The root record of a custom write (a `<root>/:param` capture of any root kind's module, a\n // stored record, or the body).\n const rootTypes = new Set(rootKinds(config).map((kind) => kind.module.name));\n const customRootId = async (route: OfflineRoute): Promise<string | undefined> => {\n const rootIndex = route.pattern.findIndex(\n (segment) => typeof segment !== \"string\" && rootTypes.has(segment.name),\n );\n const next = rootIndex >= 0 ? route.pattern[rootIndex + 1] : undefined;\n const rootParam = typeof next === \"string\" && next.startsWith(\":\") ? next.slice(1) : undefined;\n return rootIdForCustomWrite(store, offlineRequest, match.params, rootParam);\n };\n\n if (!custom || !custom.route.queue) {\n // A custom write that is not queued is refused on a locked root record too (GETs stay allowed).\n if (custom && method !== \"GET\") {\n const rootId = await customRootId(custom.route);\n if (rootId && isRootWriteLocked(rootId)) {\n return toApiData(needsConnectionAnswer(\"The root record is locked\"));\n }\n }\n const answer = await match.route.answer(ctx);\n if (method !== \"GET\") await refreshPendingCount(store);\n return toApiData(answer);\n }\n\n // A custom write the server must run too: refused on a locked root record, otherwise the answer\n // updates the device copy and the original request is queued in the same transaction.\n const rootId = await customRootId(custom.route);\n if (!rootId) return toApiData(needsConnectionAnswer(`${method} ${path} belongs to no root record on this device`));\n if (isRootWriteLocked(rootId)) return toApiData(needsConnectionAnswer(\"The root record is locked\"));\n\n const answer = await store.transaction(async (transactionStore) => {\n const result = await custom.route.answer({ ...ctx, store: transactionStore });\n if (result.status >= 200 && result.status < 300) {\n await transactionStore.enqueue({\n rootId,\n method,\n path: queuePath(ctx),\n body: request.body,\n base: null,\n });\n }\n return result;\n });\n await refreshPendingCount(store);\n return toApiData(answer);\n } catch (error) {\n if (error instanceof NeedsConnectionError) return toApiData(needsConnectionAnswer(error.message));\n console.error(\"[offline] local answer failed\", error);\n return toApiData({\n status: 500,\n body: {\n errors: [{ status: \"500\", title: \"Offline answer failed\", detail: String((error as Error)?.message ?? error) }],\n },\n });\n }\n}\n"]}
|
|
@@ -1,27 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatLocalDate
|
|
3
|
+
} from "./chunk-RW6XZGZG.mjs";
|
|
1
4
|
import {
|
|
2
5
|
AbstractApiData,
|
|
3
6
|
EndpointCreator,
|
|
4
7
|
ModuleRegistry,
|
|
5
8
|
Modules,
|
|
6
9
|
RehydrationFactory,
|
|
7
|
-
RootSync
|
|
8
|
-
|
|
9
|
-
getI18nServerLocale,
|
|
10
|
-
getI18nSupportedLocales
|
|
11
|
-
} from "./chunk-N2BFT7KI.mjs";
|
|
10
|
+
RootSync
|
|
11
|
+
} from "./chunk-W7U6O3OE.mjs";
|
|
12
12
|
import {
|
|
13
13
|
ClientJsonApiDelete,
|
|
14
14
|
ClientJsonApiGet,
|
|
15
15
|
ClientJsonApiPatch,
|
|
16
16
|
ClientJsonApiPost,
|
|
17
17
|
ClientJsonApiPut
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-47HNJQ7U.mjs";
|
|
19
19
|
import {
|
|
20
20
|
DataClassRegistry,
|
|
21
21
|
JsonApiDelete,
|
|
22
22
|
JsonApiGet,
|
|
23
23
|
JsonApiPost
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-YPBIPE2B.mjs";
|
|
25
|
+
import {
|
|
26
|
+
getI18nDefaultLocale,
|
|
27
|
+
getI18nServerLocale,
|
|
28
|
+
getI18nSupportedLocales
|
|
29
|
+
} from "./chunk-743M4TIB.mjs";
|
|
25
30
|
import {
|
|
26
31
|
__name
|
|
27
32
|
} from "./chunk-PAWJFY3S.mjs";
|
|
@@ -119,7 +124,7 @@ var AbstractService = class {
|
|
|
119
124
|
* Make an API call with automatic environment detection and error handling.
|
|
120
125
|
*/
|
|
121
126
|
static async callApi(params) {
|
|
122
|
-
const { JsonApiGet: JsonApiGet2, JsonApiPost: JsonApiPost2, JsonApiPut, JsonApiPatch, JsonApiDelete: JsonApiDelete2 } = await import("./JsonApiRequest-
|
|
127
|
+
const { JsonApiGet: JsonApiGet2, JsonApiPost: JsonApiPost2, JsonApiPut, JsonApiPatch, JsonApiDelete: JsonApiDelete2 } = await import("./JsonApiRequest-WNPDS356.mjs");
|
|
123
128
|
let apiResponse;
|
|
124
129
|
let language = "en";
|
|
125
130
|
if (typeof window === "undefined") {
|
|
@@ -221,7 +226,7 @@ var AbstractService = class {
|
|
|
221
226
|
* Make an API call and return both data and meta from the response.
|
|
222
227
|
*/
|
|
223
228
|
static async callApiWithMeta(params) {
|
|
224
|
-
const { JsonApiGet: JsonApiGet2, JsonApiPost: JsonApiPost2, JsonApiPut, JsonApiPatch, JsonApiDelete: JsonApiDelete2 } = await import("./JsonApiRequest-
|
|
229
|
+
const { JsonApiGet: JsonApiGet2, JsonApiPost: JsonApiPost2, JsonApiPut, JsonApiPatch, JsonApiDelete: JsonApiDelete2 } = await import("./JsonApiRequest-WNPDS356.mjs");
|
|
225
230
|
let apiResponse;
|
|
226
231
|
let language = "en";
|
|
227
232
|
if (typeof window === "undefined") {
|
|
@@ -306,7 +311,7 @@ var AbstractService = class {
|
|
|
306
311
|
* Get raw JSON:API response data without deserialization.
|
|
307
312
|
*/
|
|
308
313
|
static async getRawData(params) {
|
|
309
|
-
const { JsonApiGet: JsonApiGet2 } = await import("./JsonApiRequest-
|
|
314
|
+
const { JsonApiGet: JsonApiGet2 } = await import("./JsonApiRequest-WNPDS356.mjs");
|
|
310
315
|
let language = "en";
|
|
311
316
|
if (typeof window === "undefined") {
|
|
312
317
|
language = await getI18nServerLocale();
|
|
@@ -699,60 +704,6 @@ function useIsMobile() {
|
|
|
699
704
|
}
|
|
700
705
|
__name(useIsMobile, "useIsMobile");
|
|
701
706
|
|
|
702
|
-
// src/utils/date-formatter.ts
|
|
703
|
-
var formatLocalDate = /* @__PURE__ */ __name((d) => {
|
|
704
|
-
const year = d.getFullYear();
|
|
705
|
-
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
706
|
-
const day = String(d.getDate()).padStart(2, "0");
|
|
707
|
-
return `${year}-${month}-${day}`;
|
|
708
|
-
}, "formatLocalDate");
|
|
709
|
-
var dateFormatLocale = /* @__PURE__ */ __name((appLocale) => !appLocale || appLocale.split("-")[0] === "en" ? "en-GB" : appLocale, "dateFormatLocale");
|
|
710
|
-
var formatDate = /* @__PURE__ */ __name((eventDate, formatOption, locale = "en-GB") => {
|
|
711
|
-
const formatPart = /* @__PURE__ */ __name((date, options) => new Intl.DateTimeFormat(locale, options).format(date), "formatPart");
|
|
712
|
-
const now = /* @__PURE__ */ new Date();
|
|
713
|
-
const diff = now.getTime() - eventDate.getTime();
|
|
714
|
-
if (formatOption === "default") {
|
|
715
|
-
if (diff < 24 * 60 * 60 * 1e3) formatOption = "timeSince";
|
|
716
|
-
else formatOption = "dateTime";
|
|
717
|
-
}
|
|
718
|
-
if (formatOption === "timeSince") {
|
|
719
|
-
const seconds = Math.floor(diff / 1e3);
|
|
720
|
-
const minutes = Math.floor(seconds / 60);
|
|
721
|
-
const hours = Math.floor(minutes / 60);
|
|
722
|
-
const days = Math.floor(hours / 24);
|
|
723
|
-
if (locale.split("-")[0] !== "en") {
|
|
724
|
-
const relative = new Intl.RelativeTimeFormat(locale, { numeric: "always" });
|
|
725
|
-
if (days > 0) return relative.format(-days, "day");
|
|
726
|
-
if (hours > 0) return relative.format(-hours, "hour");
|
|
727
|
-
if (minutes > 0) return relative.format(-minutes, "minute");
|
|
728
|
-
return relative.format(-seconds, "second");
|
|
729
|
-
}
|
|
730
|
-
if (days > 0) return `${days} days ago`;
|
|
731
|
-
if (hours > 0) return `${hours} hours ago`;
|
|
732
|
-
if (minutes > 0) return `${minutes} minutes ago`;
|
|
733
|
-
return `${seconds} seconds ago`;
|
|
734
|
-
}
|
|
735
|
-
const dateOptions = {
|
|
736
|
-
year: "numeric",
|
|
737
|
-
month: "2-digit",
|
|
738
|
-
day: "2-digit"
|
|
739
|
-
};
|
|
740
|
-
const timeOptions = {
|
|
741
|
-
hour: "2-digit",
|
|
742
|
-
minute: "2-digit"
|
|
743
|
-
};
|
|
744
|
-
switch (formatOption) {
|
|
745
|
-
case "date":
|
|
746
|
-
return formatPart(eventDate, dateOptions);
|
|
747
|
-
case "time":
|
|
748
|
-
return formatPart(eventDate, timeOptions);
|
|
749
|
-
case "dateTime":
|
|
750
|
-
return `${formatPart(eventDate, dateOptions)} ${formatPart(eventDate, timeOptions)}`;
|
|
751
|
-
default:
|
|
752
|
-
throw new Error("Invalid format option");
|
|
753
|
-
}
|
|
754
|
-
}, "formatDate");
|
|
755
|
-
|
|
756
707
|
// src/utils/exists.ts
|
|
757
708
|
var exists = /* @__PURE__ */ __name((itemOrArray) => {
|
|
758
709
|
if (!itemOrArray) return false;
|
|
@@ -12708,9 +12659,6 @@ export {
|
|
|
12708
12659
|
persistViewport,
|
|
12709
12660
|
subscribeViewport,
|
|
12710
12661
|
useIsMobile,
|
|
12711
|
-
formatLocalDate,
|
|
12712
|
-
dateFormatLocale,
|
|
12713
|
-
formatDate,
|
|
12714
12662
|
exists,
|
|
12715
12663
|
TableOptions,
|
|
12716
12664
|
getTableOptions,
|
|
@@ -13023,4 +12971,4 @@ export {
|
|
|
13023
12971
|
AuditLogService,
|
|
13024
12972
|
AuditLogModule
|
|
13025
12973
|
};
|
|
13026
|
-
//# sourceMappingURL=chunk-
|
|
12974
|
+
//# sourceMappingURL=chunk-FJUKLVVX.mjs.map
|