@open-mercato/core 0.6.8-develop.6986.1.3adb0d0df6 → 0.6.8-develop.6992.1.00c90fecff
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/customers/components/detail/AssignRoleDialog.js +9 -3
- package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/DealsSection.js +2 -3
- package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
- package/dist/modules/customers/components/detail/assignableStaff.js +2 -1
- package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js +11 -12
- package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +7 -4
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
- package/dist/modules/data_sync/api/run.js +9 -1
- package/dist/modules/data_sync/api/run.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/retry.js +9 -1
- package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
- package/dist/modules/data_sync/lib/adapter-registry.js +10 -1
- package/dist/modules/data_sync/lib/adapter-registry.js.map +2 -2
- package/dist/modules/data_sync/lib/start-cursor.js +17 -0
- package/dist/modules/data_sync/lib/start-cursor.js.map +7 -0
- package/dist/modules/data_sync/lib/sync-engine.js +84 -27
- package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-run-service.js +86 -10
- package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
- package/dist/modules/data_sync/workers/sync-scheduled.js +9 -6
- package/dist/modules/data_sync/workers/sync-scheduled.js.map +2 -2
- package/dist/modules/progress/lib/progressService.js +2 -0
- package/dist/modules/progress/lib/progressService.js.map +2 -2
- package/dist/modules/progress/lib/progressServiceImpl.js +78 -34
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/dist/modules/sales/api/channels/route.js +1 -1
- package/dist/modules/sales/api/channels/route.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/components/detail/AssignRoleDialog.tsx +12 -3
- package/src/modules/customers/components/detail/DealsSection.tsx +6 -11
- package/src/modules/customers/components/detail/assignableStaff.ts +9 -1
- package/src/modules/customers/components/detail/schedule/LinkedEntitiesField.tsx +16 -13
- package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +11 -4
- package/src/modules/data_sync/AGENTS.md +6 -1
- package/src/modules/data_sync/api/run.ts +9 -1
- package/src/modules/data_sync/api/runs/[id]/retry.ts +9 -1
- package/src/modules/data_sync/lib/adapter-registry.ts +15 -0
- package/src/modules/data_sync/lib/adapter.ts +18 -0
- package/src/modules/data_sync/lib/start-cursor.ts +35 -0
- package/src/modules/data_sync/lib/sync-engine.ts +101 -27
- package/src/modules/data_sync/lib/sync-run-service.ts +118 -10
- package/src/modules/data_sync/workers/sync-scheduled.ts +9 -6
- package/src/modules/progress/AGENTS.md +2 -1
- package/src/modules/progress/lib/progressService.ts +9 -0
- package/src/modules/progress/lib/progressServiceImpl.ts +111 -37
- package/src/modules/sales/api/channels/route.ts +1 -1
|
@@ -4,6 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import { Users, X, Clock, CheckCircle2, XCircle } from "lucide-react";
|
|
5
5
|
import { cn } from "@open-mercato/shared/lib/utils";
|
|
6
6
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
7
|
+
import { hasMoreFromPage } from "@open-mercato/shared/lib/pagination/load-more";
|
|
7
8
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
8
9
|
import { Checkbox } from "@open-mercato/ui/primitives/checkbox";
|
|
9
10
|
import { IconButton } from "@open-mercato/ui/primitives/icon-button";
|
|
@@ -12,6 +13,7 @@ import { SearchInput } from "@open-mercato/ui/primitives/search-input";
|
|
|
12
13
|
import { fetchAssignableStaffMembersPage } from "../assignableStaff.js";
|
|
13
14
|
import { isVisible, getFieldLabel } from "./fieldConfig.js";
|
|
14
15
|
import { PARTICIPANT_COLORS } from "./useScheduleFormState.js";
|
|
16
|
+
const PAGE_SIZE = 20;
|
|
15
17
|
function ParticipantSearchPopover({
|
|
16
18
|
existingIds,
|
|
17
19
|
onAdd,
|
|
@@ -22,7 +24,7 @@ function ParticipantSearchPopover({
|
|
|
22
24
|
const [query, setQuery] = React.useState("");
|
|
23
25
|
const [results, setResults] = React.useState([]);
|
|
24
26
|
const [page, setPage] = React.useState(1);
|
|
25
|
-
const [
|
|
27
|
+
const [hasMore, setHasMore] = React.useState(false);
|
|
26
28
|
const [loading, setLoading] = React.useState(false);
|
|
27
29
|
const [loadError, setLoadError] = React.useState(null);
|
|
28
30
|
const selectableResults = React.useMemo(
|
|
@@ -33,7 +35,7 @@ function ParticipantSearchPopover({
|
|
|
33
35
|
if (!open) return;
|
|
34
36
|
const controller = new AbortController();
|
|
35
37
|
setLoading(true);
|
|
36
|
-
fetchAssignableStaffMembersPage(query, { page, pageSize:
|
|
38
|
+
fetchAssignableStaffMembersPage(query, { page, pageSize: PAGE_SIZE, signal: controller.signal }).then((result) => {
|
|
37
39
|
const members = result.items;
|
|
38
40
|
const nextResults = members.map((member) => ({
|
|
39
41
|
userId: member.userId,
|
|
@@ -46,10 +48,11 @@ function ParticipantSearchPopover({
|
|
|
46
48
|
nextResults.forEach((entry) => merged.set(entry.userId, entry));
|
|
47
49
|
return Array.from(merged.values());
|
|
48
50
|
});
|
|
49
|
-
|
|
51
|
+
setHasMore(hasMoreFromPage(result.servedCount, PAGE_SIZE));
|
|
50
52
|
setLoadError(null);
|
|
51
53
|
}).catch(() => {
|
|
52
54
|
setResults([]);
|
|
55
|
+
setHasMore(false);
|
|
53
56
|
setLoadError(
|
|
54
57
|
t(
|
|
55
58
|
"customers.assignableStaff.loadError",
|
|
@@ -132,7 +135,7 @@ function ParticipantSearchPopover({
|
|
|
132
135
|
r.userId
|
|
133
136
|
);
|
|
134
137
|
}),
|
|
135
|
-
!loading && !loadError &&
|
|
138
|
+
!loading && !loadError && hasMore ? /* @__PURE__ */ jsx("div", { className: "px-2 py-2", children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", className: "w-full", onClick: () => setPage((current) => current + 1), children: t("customers.schedule.loadMore", "Load more") }) }) : null
|
|
136
139
|
] })
|
|
137
140
|
] })
|
|
138
141
|
] });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/customers/components/detail/schedule/ParticipantsField.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Users, X, Clock, CheckCircle2, XCircle } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { SearchInput } from '@open-mercato/ui/primitives/search-input'\nimport { fetchAssignableStaffMembersPage } from '../assignableStaff'\nimport type { ActivityType, ScheduleFieldId } from './fieldConfig'\nimport { isVisible, getFieldLabel } from './fieldConfig'\nimport type { Participant, RsvpStatus } from './useScheduleFormState'\nimport { PARTICIPANT_COLORS } from './useScheduleFormState'\n\nfunction ParticipantSearchPopover({\n existingIds,\n onAdd,\n onAddMany,\n t,\n}: {\n existingIds: Set<string>\n onAdd: (p: Participant) => void\n onAddMany: (participants: Participant[]) => void\n t: (key: string, fallback: string) => string\n}) {\n const [open, setOpen] = React.useState(false)\n const [query, setQuery] = React.useState('')\n const [results, setResults] = React.useState<Array<{ userId: string; name: string; email: string }>>([])\n const [page, setPage] = React.useState(1)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(false)\n const [loadError, setLoadError] = React.useState<string | null>(null)\n const selectableResults = React.useMemo(\n () => results.filter((result) => !existingIds.has(result.userId)),\n [existingIds, results],\n )\n\n React.useEffect(() => {\n if (!open) return\n const controller = new AbortController()\n setLoading(true)\n fetchAssignableStaffMembersPage(query, { page, pageSize: 20, signal: controller.signal })\n .then((result) => {\n const members = result.items\n const nextResults = members.map((member) => ({\n userId: member.userId,\n name: member.displayName,\n email: member.email ?? '',\n }))\n setResults((current) => {\n if (page <= 1) return nextResults\n const merged = new Map(current.map((entry) => [entry.userId, entry]))\n nextResults.forEach((entry) => merged.set(entry.userId, entry))\n return Array.from(merged.values())\n })\n setTotalPages(result.total > 0 ? Math.max(1, Math.ceil(result.total / result.pageSize)) : 1)\n setLoadError(null)\n })\n .catch(() => {\n setResults([])\n setLoadError(\n t(\n 'customers.assignableStaff.loadError',\n 'Unable to load team members. Check your permissions and try again.',\n ),\n )\n })\n .finally(() => setLoading(false))\n return () => controller.abort()\n }, [open, page, query, t])\n\n React.useEffect(() => {\n if (!open) return\n setPage(1)\n }, [open, query])\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" className=\"h-auto inline-flex items-center gap-1.5 rounded-full border border-status-success-border bg-status-success-bg px-2.5 py-1.5 text-xs font-semibold text-foreground\">\n <Users className=\"size-3\" />\n {t('customers.schedule.addParticipant', 'Add participant')}\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-72 p-2\">\n <SearchInput\n value={query}\n onChange={setQuery}\n placeholder={t('customers.schedule.searchParticipant', 'Search team members...')}\n className=\"mb-2\"\n autoFocus\n />\n {selectableResults.length ? (\n <div className=\"mb-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"w-full\"\n onClick={() => {\n onAddMany(\n selectableResults.map((participant, index) => ({\n userId: participant.userId,\n name: participant.name,\n email: participant.email,\n color: PARTICIPANT_COLORS[(existingIds.size + index) % PARTICIPANT_COLORS.length],\n })),\n )\n setOpen(false)\n setQuery('')\n }}\n >\n {t('customers.schedule.addVisibleParticipants', 'Add all visible')}\n </Button>\n </div>\n ) : null}\n <div className=\"max-h-48 overflow-y-auto space-y-0.5\">\n {loading && <p className=\"px-2 py-3 text-xs text-muted-foreground text-center\">{t('customers.schedule.searching', 'Searching...')}</p>}\n {!loading && loadError && <p className=\"px-2 py-3 text-xs text-destructive text-center\">{loadError}</p>}\n {!loading && !loadError && results.length === 0 && <p className=\"px-2 py-3 text-xs text-muted-foreground text-center\">{t('customers.schedule.noResults', 'No results')}</p>}\n {results.map((r) => {\n const alreadyAdded = existingIds.has(r.userId)\n return (\n <Button\n key={r.userId}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={alreadyAdded}\n onClick={() => {\n onAdd({ userId: r.userId, name: r.name, email: r.email, color: PARTICIPANT_COLORS[existingIds.size % PARTICIPANT_COLORS.length] })\n setOpen(false)\n setQuery('')\n }}\n className={cn(\n 'h-auto flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors',\n alreadyAdded ? 'opacity-40 cursor-default' : 'hover:bg-accent cursor-pointer',\n )}\n >\n <span className=\"inline-flex size-6 items-center justify-center rounded-full bg-muted text-xs font-bold shrink-0\">\n {r.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"min-w-0 flex-1 truncate\">{r.name}</span>\n {r.email && <span className=\"text-xs text-muted-foreground truncate\">{r.email}</span>}\n </Button>\n )\n })}\n {!loading && !loadError && page < totalPages ? (\n <div className=\"px-2 py-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"w-full\" onClick={() => setPage((current) => current + 1)}>\n {t('customers.schedule.loadMore', 'Load more')}\n </Button>\n </div>\n ) : null}\n </div>\n </PopoverContent>\n </Popover>\n )\n}\n\ninterface ParticipantsFieldProps {\n visible: Set<ScheduleFieldId>\n activityType: ActivityType\n participants: Participant[]\n setParticipants: React.Dispatch<React.SetStateAction<Participant[]>>\n removeParticipant: (userId: string) => void\n guestPermissions: { canInviteOthers: boolean; canModify: boolean; canSeeList: boolean }\n setGuestPermissions: React.Dispatch<React.SetStateAction<{ canInviteOthers: boolean; canModify: boolean; canSeeList: boolean }>>\n}\n\nexport function ParticipantsField({\n visible,\n activityType,\n participants,\n setParticipants,\n removeParticipant,\n guestPermissions,\n setGuestPermissions,\n}: ParticipantsFieldProps) {\n const t = useT()\n\n if (!isVisible(activityType, 'participants')) return null\n\n const sectionLabel = getFieldLabel(\n activityType,\n 'participants',\n t,\n 'customers.schedule.participants',\n 'Participants',\n )\n\n return (\n <div>\n <label className=\"text-overline font-semibold uppercase text-muted-foreground tracking-wider\">\n {sectionLabel}\n </label>\n <div className=\"mt-2.5 flex flex-wrap content-center items-center gap-2 rounded-lg border border-border bg-background px-3 py-2.5\">\n {participants.map((p) => (\n <div key={p.userId} className=\"inline-flex items-center gap-1.5 rounded-full border border-border bg-muted px-2.5 py-1.5\">\n <span className={cn('inline-flex size-5 items-center justify-center rounded-full text-xs font-bold text-white', p.color ?? 'bg-primary')}>\n {p.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"text-xs text-foreground\">{p.name}</span>\n <IconButton type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => removeParticipant(p.userId)} className=\"h-auto text-muted-foreground hover:text-foreground p-0\" aria-label={t('customers.schedule.removeParticipant', 'Remove participant')}>\n <X className=\"size-3\" />\n </IconButton>\n </div>\n ))}\n <ParticipantSearchPopover\n existingIds={new Set(participants.map((p) => p.userId))}\n onAdd={(p) => setParticipants((prev) => [...prev, { ...p, status: 'pending' as RsvpStatus }])}\n onAddMany={(nextParticipants) => {\n setParticipants((prev) => [\n ...prev,\n ...nextParticipants.map((participant) => ({ ...participant, status: 'pending' as RsvpStatus })),\n ])\n }}\n t={t}\n />\n </div>\n\n {/* Guest permissions -- shown when participants exist */}\n {participants.length > 0 && (\n <div className=\"mt-3 flex flex-wrap items-center gap-x-[16px] gap-y-[6px] text-xs\">\n <span className=\"font-medium text-muted-foreground\">{t('customers.schedule.guestPermissions', 'Guest permissions:')}</span>\n <label htmlFor=\"guest-perm-invite\" className=\"flex items-center gap-1.5 cursor-pointer\">\n <Checkbox id=\"guest-perm-invite\" checked={guestPermissions.canInviteOthers} onCheckedChange={(checked) => setGuestPermissions((p) => ({ ...p, canInviteOthers: checked === true }))} />\n {t('customers.schedule.guestPerm.invite', 'Invite others')}\n </label>\n <label htmlFor=\"guest-perm-modify\" className=\"flex items-center gap-1.5 cursor-pointer\">\n <Checkbox id=\"guest-perm-modify\" checked={guestPermissions.canModify} onCheckedChange={(checked) => setGuestPermissions((p) => ({ ...p, canModify: checked === true }))} />\n {t('customers.schedule.guestPerm.modify', 'Modify')}\n </label>\n <label htmlFor=\"guest-perm-see-list\" className=\"flex items-center gap-1.5 cursor-pointer\">\n <Checkbox id=\"guest-perm-see-list\" checked={guestPermissions.canSeeList} onCheckedChange={(checked) => setGuestPermissions((p) => ({ ...p, canSeeList: checked === true }))} />\n {t('customers.schedule.guestPerm.seeList', 'See list')}\n </label>\n </div>\n )}\n\n {/* RSVP summary -- shown when participants exist */}\n {participants.length > 0 && (() => {\n const accepted = participants.filter((p) => p.status === 'accepted').length\n const pending = participants.filter((p) => !p.status || p.status === 'pending').length\n const declined = participants.filter((p) => p.status === 'declined').length\n if (accepted === 0 && pending === 0 && declined === 0) return null\n return (\n <div className=\"mt-2 flex items-center gap-3 text-xs\">\n <span className=\"text-muted-foreground\">{t('customers.schedule.rsvp.label', 'Responses:')}</span>\n {accepted > 0 && <span className=\"flex items-center gap-1 font-medium text-status-success-text\"><CheckCircle2 className=\"size-3.5\" /> {accepted} {t('customers.schedule.rsvp.accepted', 'tak')}</span>}\n {pending > 0 && <span className=\"flex items-center gap-1 font-medium text-status-warning-text\"><Clock className=\"size-3.5\" /> {pending} {t('customers.schedule.rsvp.pending', 'czeka')}</span>}\n {declined > 0 && <span className=\"flex items-center gap-1 font-medium text-status-error-text\"><XCircle className=\"size-3.5\" /> {declined} {t('customers.schedule.rsvp.declined', 'nie')}</span>}\n </div>\n )\n })()}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Users, X, Clock, CheckCircle2, XCircle } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { hasMoreFromPage } from '@open-mercato/shared/lib/pagination/load-more'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { SearchInput } from '@open-mercato/ui/primitives/search-input'\nimport { fetchAssignableStaffMembersPage } from '../assignableStaff'\nimport type { ActivityType, ScheduleFieldId } from './fieldConfig'\nimport { isVisible, getFieldLabel } from './fieldConfig'\nimport type { Participant, RsvpStatus } from './useScheduleFormState'\nimport { PARTICIPANT_COLORS } from './useScheduleFormState'\n\nconst PAGE_SIZE = 20\n\nfunction ParticipantSearchPopover({\n existingIds,\n onAdd,\n onAddMany,\n t,\n}: {\n existingIds: Set<string>\n onAdd: (p: Participant) => void\n onAddMany: (participants: Participant[]) => void\n t: (key: string, fallback: string) => string\n}) {\n const [open, setOpen] = React.useState(false)\n const [query, setQuery] = React.useState('')\n const [results, setResults] = React.useState<Array<{ userId: string; name: string; email: string }>>([])\n const [page, setPage] = React.useState(1)\n // Short-page termination instead of a `total`-derived page bound \u2014 see\n // `hasMoreFromPage`. Measured on the served count, not on `members`, which is\n // deduped by user id.\n const [hasMore, setHasMore] = React.useState(false)\n const [loading, setLoading] = React.useState(false)\n const [loadError, setLoadError] = React.useState<string | null>(null)\n const selectableResults = React.useMemo(\n () => results.filter((result) => !existingIds.has(result.userId)),\n [existingIds, results],\n )\n\n React.useEffect(() => {\n if (!open) return\n const controller = new AbortController()\n setLoading(true)\n fetchAssignableStaffMembersPage(query, { page, pageSize: PAGE_SIZE, signal: controller.signal })\n .then((result) => {\n const members = result.items\n const nextResults = members.map((member) => ({\n userId: member.userId,\n name: member.displayName,\n email: member.email ?? '',\n }))\n setResults((current) => {\n if (page <= 1) return nextResults\n const merged = new Map(current.map((entry) => [entry.userId, entry]))\n nextResults.forEach((entry) => merged.set(entry.userId, entry))\n return Array.from(merged.values())\n })\n setHasMore(hasMoreFromPage(result.servedCount, PAGE_SIZE))\n setLoadError(null)\n })\n .catch(() => {\n setResults([])\n setHasMore(false)\n setLoadError(\n t(\n 'customers.assignableStaff.loadError',\n 'Unable to load team members. Check your permissions and try again.',\n ),\n )\n })\n .finally(() => setLoading(false))\n return () => controller.abort()\n }, [open, page, query, t])\n\n React.useEffect(() => {\n if (!open) return\n setPage(1)\n }, [open, query])\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" className=\"h-auto inline-flex items-center gap-1.5 rounded-full border border-status-success-border bg-status-success-bg px-2.5 py-1.5 text-xs font-semibold text-foreground\">\n <Users className=\"size-3\" />\n {t('customers.schedule.addParticipant', 'Add participant')}\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-72 p-2\">\n <SearchInput\n value={query}\n onChange={setQuery}\n placeholder={t('customers.schedule.searchParticipant', 'Search team members...')}\n className=\"mb-2\"\n autoFocus\n />\n {selectableResults.length ? (\n <div className=\"mb-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"w-full\"\n onClick={() => {\n onAddMany(\n selectableResults.map((participant, index) => ({\n userId: participant.userId,\n name: participant.name,\n email: participant.email,\n color: PARTICIPANT_COLORS[(existingIds.size + index) % PARTICIPANT_COLORS.length],\n })),\n )\n setOpen(false)\n setQuery('')\n }}\n >\n {t('customers.schedule.addVisibleParticipants', 'Add all visible')}\n </Button>\n </div>\n ) : null}\n <div className=\"max-h-48 overflow-y-auto space-y-0.5\">\n {loading && <p className=\"px-2 py-3 text-xs text-muted-foreground text-center\">{t('customers.schedule.searching', 'Searching...')}</p>}\n {!loading && loadError && <p className=\"px-2 py-3 text-xs text-destructive text-center\">{loadError}</p>}\n {!loading && !loadError && results.length === 0 && <p className=\"px-2 py-3 text-xs text-muted-foreground text-center\">{t('customers.schedule.noResults', 'No results')}</p>}\n {results.map((r) => {\n const alreadyAdded = existingIds.has(r.userId)\n return (\n <Button\n key={r.userId}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={alreadyAdded}\n onClick={() => {\n onAdd({ userId: r.userId, name: r.name, email: r.email, color: PARTICIPANT_COLORS[existingIds.size % PARTICIPANT_COLORS.length] })\n setOpen(false)\n setQuery('')\n }}\n className={cn(\n 'h-auto flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors',\n alreadyAdded ? 'opacity-40 cursor-default' : 'hover:bg-accent cursor-pointer',\n )}\n >\n <span className=\"inline-flex size-6 items-center justify-center rounded-full bg-muted text-xs font-bold shrink-0\">\n {r.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"min-w-0 flex-1 truncate\">{r.name}</span>\n {r.email && <span className=\"text-xs text-muted-foreground truncate\">{r.email}</span>}\n </Button>\n )\n })}\n {!loading && !loadError && hasMore ? (\n <div className=\"px-2 py-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"w-full\" onClick={() => setPage((current) => current + 1)}>\n {t('customers.schedule.loadMore', 'Load more')}\n </Button>\n </div>\n ) : null}\n </div>\n </PopoverContent>\n </Popover>\n )\n}\n\ninterface ParticipantsFieldProps {\n visible: Set<ScheduleFieldId>\n activityType: ActivityType\n participants: Participant[]\n setParticipants: React.Dispatch<React.SetStateAction<Participant[]>>\n removeParticipant: (userId: string) => void\n guestPermissions: { canInviteOthers: boolean; canModify: boolean; canSeeList: boolean }\n setGuestPermissions: React.Dispatch<React.SetStateAction<{ canInviteOthers: boolean; canModify: boolean; canSeeList: boolean }>>\n}\n\nexport function ParticipantsField({\n visible,\n activityType,\n participants,\n setParticipants,\n removeParticipant,\n guestPermissions,\n setGuestPermissions,\n}: ParticipantsFieldProps) {\n const t = useT()\n\n if (!isVisible(activityType, 'participants')) return null\n\n const sectionLabel = getFieldLabel(\n activityType,\n 'participants',\n t,\n 'customers.schedule.participants',\n 'Participants',\n )\n\n return (\n <div>\n <label className=\"text-overline font-semibold uppercase text-muted-foreground tracking-wider\">\n {sectionLabel}\n </label>\n <div className=\"mt-2.5 flex flex-wrap content-center items-center gap-2 rounded-lg border border-border bg-background px-3 py-2.5\">\n {participants.map((p) => (\n <div key={p.userId} className=\"inline-flex items-center gap-1.5 rounded-full border border-border bg-muted px-2.5 py-1.5\">\n <span className={cn('inline-flex size-5 items-center justify-center rounded-full text-xs font-bold text-white', p.color ?? 'bg-primary')}>\n {p.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"text-xs text-foreground\">{p.name}</span>\n <IconButton type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => removeParticipant(p.userId)} className=\"h-auto text-muted-foreground hover:text-foreground p-0\" aria-label={t('customers.schedule.removeParticipant', 'Remove participant')}>\n <X className=\"size-3\" />\n </IconButton>\n </div>\n ))}\n <ParticipantSearchPopover\n existingIds={new Set(participants.map((p) => p.userId))}\n onAdd={(p) => setParticipants((prev) => [...prev, { ...p, status: 'pending' as RsvpStatus }])}\n onAddMany={(nextParticipants) => {\n setParticipants((prev) => [\n ...prev,\n ...nextParticipants.map((participant) => ({ ...participant, status: 'pending' as RsvpStatus })),\n ])\n }}\n t={t}\n />\n </div>\n\n {/* Guest permissions -- shown when participants exist */}\n {participants.length > 0 && (\n <div className=\"mt-3 flex flex-wrap items-center gap-x-[16px] gap-y-[6px] text-xs\">\n <span className=\"font-medium text-muted-foreground\">{t('customers.schedule.guestPermissions', 'Guest permissions:')}</span>\n <label htmlFor=\"guest-perm-invite\" className=\"flex items-center gap-1.5 cursor-pointer\">\n <Checkbox id=\"guest-perm-invite\" checked={guestPermissions.canInviteOthers} onCheckedChange={(checked) => setGuestPermissions((p) => ({ ...p, canInviteOthers: checked === true }))} />\n {t('customers.schedule.guestPerm.invite', 'Invite others')}\n </label>\n <label htmlFor=\"guest-perm-modify\" className=\"flex items-center gap-1.5 cursor-pointer\">\n <Checkbox id=\"guest-perm-modify\" checked={guestPermissions.canModify} onCheckedChange={(checked) => setGuestPermissions((p) => ({ ...p, canModify: checked === true }))} />\n {t('customers.schedule.guestPerm.modify', 'Modify')}\n </label>\n <label htmlFor=\"guest-perm-see-list\" className=\"flex items-center gap-1.5 cursor-pointer\">\n <Checkbox id=\"guest-perm-see-list\" checked={guestPermissions.canSeeList} onCheckedChange={(checked) => setGuestPermissions((p) => ({ ...p, canSeeList: checked === true }))} />\n {t('customers.schedule.guestPerm.seeList', 'See list')}\n </label>\n </div>\n )}\n\n {/* RSVP summary -- shown when participants exist */}\n {participants.length > 0 && (() => {\n const accepted = participants.filter((p) => p.status === 'accepted').length\n const pending = participants.filter((p) => !p.status || p.status === 'pending').length\n const declined = participants.filter((p) => p.status === 'declined').length\n if (accepted === 0 && pending === 0 && declined === 0) return null\n return (\n <div className=\"mt-2 flex items-center gap-3 text-xs\">\n <span className=\"text-muted-foreground\">{t('customers.schedule.rsvp.label', 'Responses:')}</span>\n {accepted > 0 && <span className=\"flex items-center gap-1 font-medium text-status-success-text\"><CheckCircle2 className=\"size-3.5\" /> {accepted} {t('customers.schedule.rsvp.accepted', 'tak')}</span>}\n {pending > 0 && <span className=\"flex items-center gap-1 font-medium text-status-warning-text\"><Clock className=\"size-3.5\" /> {pending} {t('customers.schedule.rsvp.pending', 'czeka')}</span>}\n {declined > 0 && <span className=\"flex items-center gap-1 font-medium text-status-error-text\"><XCircle className=\"size-3.5\" /> {declined} {t('customers.schedule.rsvp.declined', 'nie')}</span>}\n </div>\n )\n })()}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -7,6 +7,7 @@ import { getIntegration } from "@open-mercato/shared/modules/integrations/types"
|
|
|
7
7
|
import { runSyncSchema } from "../data/validators.js";
|
|
8
8
|
import { startDataSyncRun } from "../lib/start-run.js";
|
|
9
9
|
import { getDataSyncAdapter } from "../lib/adapter-registry.js";
|
|
10
|
+
import { resolveStartCursor } from "../lib/start-cursor.js";
|
|
10
11
|
import {
|
|
11
12
|
runCrudMutationGuardAfterSuccess,
|
|
12
13
|
validateCrudMutationGuard
|
|
@@ -90,7 +91,14 @@ async function POST(req) {
|
|
|
90
91
|
if (guardResult && !guardResult.ok) {
|
|
91
92
|
return NextResponse.json(guardResult.body, { status: guardResult.status });
|
|
92
93
|
}
|
|
93
|
-
const cursor = parsed.data.fullSync ? null : await
|
|
94
|
+
const cursor = parsed.data.fullSync ? null : await resolveStartCursor({
|
|
95
|
+
syncRunService,
|
|
96
|
+
adapter,
|
|
97
|
+
integrationId: parsed.data.integrationId,
|
|
98
|
+
entityType: parsed.data.entityType,
|
|
99
|
+
direction: parsed.data.direction,
|
|
100
|
+
scope
|
|
101
|
+
});
|
|
94
102
|
const { run, progressJob } = await startDataSyncRun({
|
|
95
103
|
syncRunService,
|
|
96
104
|
progressService,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/data_sync/api/run.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { organizationScopeRequiredResponse, resolveActiveOrganizationId } from '@open-mercato/shared/lib/auth/organizationScope'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { getIntegration } from '@open-mercato/shared/modules/integrations/types'\nimport type { ProgressService } from '../../progress/lib/progressService'\nimport type { IntegrationStateService } from '../../integrations/lib/state-service'\nimport type { SyncRunService } from '../lib/sync-run-service'\nimport { runSyncSchema } from '../data/validators'\nimport { startDataSyncRun } from '../lib/start-run'\nimport { getDataSyncAdapter } from '../lib/adapter-registry'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('data_sync').child({ component: 'run' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['data_sync.run'] },\n}\n\nexport const openApi = {\n tags: ['DataSync'],\n summary: 'Start a data sync run',\n}\n\nexport async function POST(req: Request) {\n try {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const organizationId = resolveActiveOrganizationId(auth)\n if (!organizationId) {\n return organizationScopeRequiredResponse()\n }\n\n const payload = await readJsonSafe(req)\n const parsed = runSyncSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 422 })\n }\n\n const container = await createRequestContainer()\n const syncRunService = container.resolve('dataSyncRunService') as SyncRunService\n const progressService = container.resolve('progressService') as ProgressService\n const integrationStateService = container.resolve('integrationStateService') as IntegrationStateService\n\n const scope = {\n organizationId,\n tenantId: auth.tenantId,\n }\n\n const integration = getIntegration(parsed.data.integrationId)\n if (!integration?.providerKey) {\n return NextResponse.json({ error: 'Integration not found' }, { status: 404 })\n }\n\n const adapter = getDataSyncAdapter(integration.providerKey)\n if (!adapter) {\n return NextResponse.json({ error: 'No registered sync adapter for provider' }, { status: 404 })\n }\n if (adapter.runMode === 'provider') {\n return NextResponse.json(\n {\n error: 'This integration must be started from its provider-specific import flow.',\n settingsPath: `/backend/integrations/${encodeURIComponent(parsed.data.integrationId)}`,\n },\n { status: 422 },\n )\n }\n\n if (!adapter.supportedEntities.includes(parsed.data.entityType)) {\n return NextResponse.json({ error: 'Unsupported entity type for this integration' }, { status: 422 })\n }\n\n const integrationEnabled = await integrationStateService.isEnabled(parsed.data.integrationId, scope)\n if (!integrationEnabled) {\n return NextResponse.json({ error: 'Integration is disabled' }, { status: 409 })\n }\n\n const overlap = await syncRunService.findRunningOverlap(\n parsed.data.integrationId,\n parsed.data.entityType,\n parsed.data.direction,\n scope,\n )\n if (overlap) {\n return NextResponse.json({ error: 'A sync run is already in progress for this integration and entity direction' }, { status: 409 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: scope.organizationId,\n userId: auth.sub,\n resourceKind: 'data_sync.run',\n resourceId: parsed.data.integrationId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsed.data,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const cursor = parsed.data.fullSync\n ? null\n : await syncRunService
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { organizationScopeRequiredResponse, resolveActiveOrganizationId } from '@open-mercato/shared/lib/auth/organizationScope'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { getIntegration } from '@open-mercato/shared/modules/integrations/types'\nimport type { ProgressService } from '../../progress/lib/progressService'\nimport type { IntegrationStateService } from '../../integrations/lib/state-service'\nimport type { SyncRunService } from '../lib/sync-run-service'\nimport { runSyncSchema } from '../data/validators'\nimport { startDataSyncRun } from '../lib/start-run'\nimport { getDataSyncAdapter } from '../lib/adapter-registry'\nimport { resolveStartCursor } from '../lib/start-cursor'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('data_sync').child({ component: 'run' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['data_sync.run'] },\n}\n\nexport const openApi = {\n tags: ['DataSync'],\n summary: 'Start a data sync run',\n}\n\nexport async function POST(req: Request) {\n try {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const organizationId = resolveActiveOrganizationId(auth)\n if (!organizationId) {\n return organizationScopeRequiredResponse()\n }\n\n const payload = await readJsonSafe(req)\n const parsed = runSyncSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 422 })\n }\n\n const container = await createRequestContainer()\n const syncRunService = container.resolve('dataSyncRunService') as SyncRunService\n const progressService = container.resolve('progressService') as ProgressService\n const integrationStateService = container.resolve('integrationStateService') as IntegrationStateService\n\n const scope = {\n organizationId,\n tenantId: auth.tenantId,\n }\n\n const integration = getIntegration(parsed.data.integrationId)\n if (!integration?.providerKey) {\n return NextResponse.json({ error: 'Integration not found' }, { status: 404 })\n }\n\n const adapter = getDataSyncAdapter(integration.providerKey)\n if (!adapter) {\n return NextResponse.json({ error: 'No registered sync adapter for provider' }, { status: 404 })\n }\n if (adapter.runMode === 'provider') {\n return NextResponse.json(\n {\n error: 'This integration must be started from its provider-specific import flow.',\n settingsPath: `/backend/integrations/${encodeURIComponent(parsed.data.integrationId)}`,\n },\n { status: 422 },\n )\n }\n\n if (!adapter.supportedEntities.includes(parsed.data.entityType)) {\n return NextResponse.json({ error: 'Unsupported entity type for this integration' }, { status: 422 })\n }\n\n const integrationEnabled = await integrationStateService.isEnabled(parsed.data.integrationId, scope)\n if (!integrationEnabled) {\n return NextResponse.json({ error: 'Integration is disabled' }, { status: 409 })\n }\n\n const overlap = await syncRunService.findRunningOverlap(\n parsed.data.integrationId,\n parsed.data.entityType,\n parsed.data.direction,\n scope,\n )\n if (overlap) {\n return NextResponse.json({ error: 'A sync run is already in progress for this integration and entity direction' }, { status: 409 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: scope.organizationId,\n userId: auth.sub,\n resourceKind: 'data_sync.run',\n resourceId: parsed.data.integrationId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsed.data,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const cursor = parsed.data.fullSync\n ? null\n : await resolveStartCursor({\n syncRunService,\n adapter,\n integrationId: parsed.data.integrationId,\n entityType: parsed.data.entityType,\n direction: parsed.data.direction,\n scope,\n })\n\n const { run, progressJob } = await startDataSyncRun({\n syncRunService,\n progressService,\n scope: {\n ...scope,\n userId: auth.sub,\n },\n input: {\n integrationId: parsed.data.integrationId,\n entityType: parsed.data.entityType,\n direction: parsed.data.direction,\n cursor,\n triggeredBy: parsed.data.triggeredBy ?? auth.sub,\n batchSize: parsed.data.batchSize,\n },\n })\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: scope.organizationId,\n userId: auth.sub,\n resourceKind: 'data_sync.run',\n resourceId: run.id,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ id: run.id, progressJobId: progressJob?.id ?? null }, { status: 201 })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n const stack = error instanceof Error ? error.stack : undefined\n logger.error('Unhandled error starting sync run', { message, stack })\n return NextResponse.json(\n { error: 'Failed to start data sync run.' },\n { status: 500 },\n )\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,mCAAmC,mCAAmC;AAC/E,SAAS,8BAA8B;AACvC,SAAS,oBAAoB;AAC7B,SAAS,sBAAsB;AAI/B,SAAS,qBAAqB;AAC9B,SAAS,wBAAwB;AACjC,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,WAAW,EAAE,MAAM,EAAE,WAAW,MAAM,CAAC;AAE5D,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,eAAe,EAAE;AAChE;AAEO,MAAM,UAAU;AAAA,EACrB,MAAM,CAAC,UAAU;AAAA,EACjB,SAAS;AACX;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,QAAI,CAAC,MAAM,UAAU;AACnB,aAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrE;AACA,UAAM,iBAAiB,4BAA4B,IAAI;AACvD,QAAI,CAAC,gBAAgB;AACnB,aAAO,kCAAkC;AAAA,IAC3C;AAEA,UAAM,UAAU,MAAM,aAAa,GAAG;AACtC,UAAM,SAAS,cAAc,UAAU,OAAO;AAC9C,QAAI,CAAC,OAAO,SAAS;AACnB,aAAO,aAAa,KAAK,EAAE,OAAO,mBAAmB,SAAS,OAAO,MAAM,QAAQ,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACzG;AAEA,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,iBAAiB,UAAU,QAAQ,oBAAoB;AAC7D,UAAM,kBAAkB,UAAU,QAAQ,iBAAiB;AAC3D,UAAM,0BAA0B,UAAU,QAAQ,yBAAyB;AAE3E,UAAM,QAAQ;AAAA,MACZ;AAAA,MACA,UAAU,KAAK;AAAA,IACjB;AAEA,UAAM,cAAc,eAAe,OAAO,KAAK,aAAa;AAC5D,QAAI,CAAC,aAAa,aAAa;AAC7B,aAAO,aAAa,KAAK,EAAE,OAAO,wBAAwB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC9E;AAEA,UAAM,UAAU,mBAAmB,YAAY,WAAW;AAC1D,QAAI,CAAC,SAAS;AACZ,aAAO,aAAa,KAAK,EAAE,OAAO,0CAA0C,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAChG;AACA,QAAI,QAAQ,YAAY,YAAY;AAClC,aAAO,aAAa;AAAA,QAClB;AAAA,UACE,OAAO;AAAA,UACP,cAAc,yBAAyB,mBAAmB,OAAO,KAAK,aAAa,CAAC;AAAA,QACtF;AAAA,QACA,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAEA,QAAI,CAAC,QAAQ,kBAAkB,SAAS,OAAO,KAAK,UAAU,GAAG;AAC/D,aAAO,aAAa,KAAK,EAAE,OAAO,+CAA+C,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrG;AAEA,UAAM,qBAAqB,MAAM,wBAAwB,UAAU,OAAO,KAAK,eAAe,KAAK;AACnG,QAAI,CAAC,oBAAoB;AACvB,aAAO,aAAa,KAAK,EAAE,OAAO,0BAA0B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAChF;AAEA,UAAM,UAAU,MAAM,eAAe;AAAA,MACnC,OAAO,KAAK;AAAA,MACZ,OAAO,KAAK;AAAA,MACZ,OAAO,KAAK;AAAA,MACZ;AAAA,IACF;AACA,QAAI,SAAS;AACX,aAAO,aAAa,KAAK,EAAE,OAAO,8EAA8E,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACpI;AAEA,UAAM,cAAc,MAAM,0BAA0B,WAAW;AAAA,MAC7D,UAAU,KAAK;AAAA,MACf,gBAAgB,MAAM;AAAA,MACtB,QAAQ,KAAK;AAAA,MACb,cAAc;AAAA,MACd,YAAY,OAAO,KAAK;AAAA,MACxB,WAAW;AAAA,MACX,eAAe,IAAI;AAAA,MACnB,gBAAgB,IAAI;AAAA,MACpB,iBAAiB,OAAO;AAAA,IAC1B,CAAC;AACD,QAAI,eAAe,CAAC,YAAY,IAAI;AAClC,aAAO,aAAa,KAAK,YAAY,MAAM,EAAE,QAAQ,YAAY,OAAO,CAAC;AAAA,IAC3E;AAEA,UAAM,SAAS,OAAO,KAAK,WACvB,OACA,MAAM,mBAAmB;AAAA,MACzB;AAAA,MACA;AAAA,MACA,eAAe,OAAO,KAAK;AAAA,MAC3B,YAAY,OAAO,KAAK;AAAA,MACxB,WAAW,OAAO,KAAK;AAAA,MACvB;AAAA,IACF,CAAC;AAEH,UAAM,EAAE,KAAK,YAAY,IAAI,MAAM,iBAAiB;AAAA,MAClD;AAAA,MACA;AAAA,MACA,OAAO;AAAA,QACL,GAAG;AAAA,QACH,QAAQ,KAAK;AAAA,MACf;AAAA,MACA,OAAO;AAAA,QACL,eAAe,OAAO,KAAK;AAAA,QAC3B,YAAY,OAAO,KAAK;AAAA,QACxB,WAAW,OAAO,KAAK;AAAA,QACvB;AAAA,QACA,aAAa,OAAO,KAAK,eAAe,KAAK;AAAA,QAC7C,WAAW,OAAO,KAAK;AAAA,MACzB;AAAA,IACF,CAAC;AAED,QAAI,aAAa,MAAM,YAAY,uBAAuB;AACxD,YAAM,iCAAiC,WAAW;AAAA,QAChD,UAAU,KAAK;AAAA,QACf,gBAAgB,MAAM;AAAA,QACtB,QAAQ,KAAK;AAAA,QACb,cAAc;AAAA,QACd,YAAY,IAAI;AAAA,QAChB,WAAW;AAAA,QACX,eAAe,IAAI;AAAA,QACnB,gBAAgB,IAAI;AAAA,QACpB,UAAU,YAAY,YAAY;AAAA,MACpC,CAAC;AAAA,IACH;AAEA,WAAO,aAAa,KAAK,EAAE,IAAI,IAAI,IAAI,eAAe,aAAa,MAAM,KAAK,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAClG,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,UAAM,QAAQ,iBAAiB,QAAQ,MAAM,QAAQ;AACrD,WAAO,MAAM,qCAAqC,EAAE,SAAS,MAAM,CAAC;AACpE,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,iCAAiC;AAAA,MAC1C,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -6,6 +6,7 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
|
6
6
|
import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
|
|
7
7
|
import { retrySyncSchema } from "../../../data/validators.js";
|
|
8
8
|
import { startDataSyncRun } from "../../../lib/start-run.js";
|
|
9
|
+
import { resolveAdapterForIntegration, resolveStartCursor } from "../../../lib/start-cursor.js";
|
|
9
10
|
import {
|
|
10
11
|
runCrudMutationGuardAfterSuccess,
|
|
11
12
|
validateCrudMutationGuard
|
|
@@ -71,7 +72,14 @@ async function POST(req, ctx) {
|
|
|
71
72
|
if (guardResult && !guardResult.ok) {
|
|
72
73
|
return NextResponse.json(guardResult.body, { status: guardResult.status });
|
|
73
74
|
}
|
|
74
|
-
const cursor = parsedBody.data.fromBeginning ? null : previous.cursor ?? await
|
|
75
|
+
const cursor = parsedBody.data.fromBeginning ? null : previous.cursor ?? await resolveStartCursor({
|
|
76
|
+
syncRunService,
|
|
77
|
+
adapter: resolveAdapterForIntegration(previous.integrationId),
|
|
78
|
+
integrationId: previous.integrationId,
|
|
79
|
+
entityType: previous.entityType,
|
|
80
|
+
direction: previous.direction,
|
|
81
|
+
scope
|
|
82
|
+
});
|
|
75
83
|
const { run, progressJob } = await startDataSyncRun({
|
|
76
84
|
syncRunService,
|
|
77
85
|
progressService,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/data_sync/api/runs/%5Bid%5D/retry.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { organizationScopeRequiredResponse, resolveActiveOrganizationId } from '@open-mercato/shared/lib/auth/organizationScope'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport type { ProgressService } from '../../../../progress/lib/progressService'\nimport type { SyncRunService } from '../../../lib/sync-run-service'\nimport { retrySyncSchema } from '../../../data/validators'\nimport { startDataSyncRun } from '../../../lib/start-run'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\n\nconst paramsSchema = z.object({ id: z.string().uuid() })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['data_sync.run'] },\n}\n\nexport const openApi = {\n tags: ['DataSync'],\n summary: 'Retry a failed sync run',\n}\n\nexport async function POST(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const organizationId = resolveActiveOrganizationId(auth)\n if (!organizationId) {\n return organizationScopeRequiredResponse()\n }\n\n const rawParams = (ctx.params && typeof (ctx.params as Promise<unknown>).then === 'function')\n ? await (ctx.params as Promise<{ id?: string }>)\n : (ctx.params as { id?: string } | undefined)\n\n const parsedParams = paramsSchema.safeParse(rawParams)\n if (!parsedParams.success) {\n return NextResponse.json({ error: 'Invalid run id' }, { status: 400 })\n }\n\n const payload = await readJsonSafe(req)\n const parsedBody = retrySyncSchema.safeParse(payload ?? {})\n if (!parsedBody.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsedBody.error.flatten() }, { status: 422 })\n }\n\n const container = await createRequestContainer()\n const syncRunService = container.resolve('dataSyncRunService') as SyncRunService\n const progressService = container.resolve('progressService') as ProgressService\n const scope = { organizationId, tenantId: auth.tenantId }\n\n const previous = await syncRunService.getRun(parsedParams.data.id, scope)\n if (!previous) {\n return NextResponse.json({ error: 'Run not found' }, { status: 404 })\n }\n if (previous.status !== 'failed' && previous.status !== 'cancelled') {\n return NextResponse.json({ error: 'Only failed or cancelled runs can be retried' }, { status: 409 })\n }\n\n const overlap = await syncRunService.findRunningOverlap(\n previous.integrationId,\n previous.entityType,\n previous.direction,\n scope,\n )\n if (overlap) {\n return NextResponse.json({ error: 'A sync run is already in progress for this integration and entity direction' }, { status: 409 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: scope.organizationId,\n userId: auth.sub,\n resourceKind: 'data_sync.run',\n resourceId: previous.id,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { action: 'retry', ...parsedBody.data },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const cursor = parsedBody.data.fromBeginning\n ? null\n : previous.cursor ?? await syncRunService
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { organizationScopeRequiredResponse, resolveActiveOrganizationId } from '@open-mercato/shared/lib/auth/organizationScope'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport type { ProgressService } from '../../../../progress/lib/progressService'\nimport type { SyncRunService } from '../../../lib/sync-run-service'\nimport { retrySyncSchema } from '../../../data/validators'\nimport { startDataSyncRun } from '../../../lib/start-run'\nimport { resolveAdapterForIntegration, resolveStartCursor } from '../../../lib/start-cursor'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\n\nconst paramsSchema = z.object({ id: z.string().uuid() })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['data_sync.run'] },\n}\n\nexport const openApi = {\n tags: ['DataSync'],\n summary: 'Retry a failed sync run',\n}\n\nexport async function POST(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const organizationId = resolveActiveOrganizationId(auth)\n if (!organizationId) {\n return organizationScopeRequiredResponse()\n }\n\n const rawParams = (ctx.params && typeof (ctx.params as Promise<unknown>).then === 'function')\n ? await (ctx.params as Promise<{ id?: string }>)\n : (ctx.params as { id?: string } | undefined)\n\n const parsedParams = paramsSchema.safeParse(rawParams)\n if (!parsedParams.success) {\n return NextResponse.json({ error: 'Invalid run id' }, { status: 400 })\n }\n\n const payload = await readJsonSafe(req)\n const parsedBody = retrySyncSchema.safeParse(payload ?? {})\n if (!parsedBody.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsedBody.error.flatten() }, { status: 422 })\n }\n\n const container = await createRequestContainer()\n const syncRunService = container.resolve('dataSyncRunService') as SyncRunService\n const progressService = container.resolve('progressService') as ProgressService\n const scope = { organizationId, tenantId: auth.tenantId }\n\n const previous = await syncRunService.getRun(parsedParams.data.id, scope)\n if (!previous) {\n return NextResponse.json({ error: 'Run not found' }, { status: 404 })\n }\n if (previous.status !== 'failed' && previous.status !== 'cancelled') {\n return NextResponse.json({ error: 'Only failed or cancelled runs can be retried' }, { status: 409 })\n }\n\n const overlap = await syncRunService.findRunningOverlap(\n previous.integrationId,\n previous.entityType,\n previous.direction,\n scope,\n )\n if (overlap) {\n return NextResponse.json({ error: 'A sync run is already in progress for this integration and entity direction' }, { status: 409 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: scope.organizationId,\n userId: auth.sub,\n resourceKind: 'data_sync.run',\n resourceId: previous.id,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { action: 'retry', ...parsedBody.data },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const cursor = parsedBody.data.fromBeginning\n ? null\n : previous.cursor ?? await resolveStartCursor({\n syncRunService,\n adapter: resolveAdapterForIntegration(previous.integrationId),\n integrationId: previous.integrationId,\n entityType: previous.entityType,\n direction: previous.direction,\n scope,\n })\n\n const { run, progressJob } = await startDataSyncRun({\n syncRunService,\n progressService,\n scope: {\n ...scope,\n userId: auth.sub,\n },\n input: {\n integrationId: previous.integrationId,\n entityType: previous.entityType,\n direction: previous.direction,\n cursor,\n triggeredBy: auth.sub,\n batchSize: 100,\n progressJob: {\n name: `Retry data sync ${previous.integrationId} \u2014 ${previous.entityType}`,\n },\n },\n })\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: scope.organizationId,\n userId: auth.sub,\n resourceKind: 'data_sync.run',\n resourceId: run.id,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ id: run.id, progressJobId: progressJob?.id ?? null }, { status: 201 })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getIntegration } from "@open-mercato/shared/modules/integrations/types";
|
|
1
2
|
const DATA_SYNC_ADAPTER_REGISTRY_KEY = /* @__PURE__ */ Symbol.for("@open-mercato/data-sync/adapter-registry");
|
|
2
3
|
function getAdapterRegistry() {
|
|
3
4
|
const globalScope = globalThis;
|
|
@@ -15,9 +16,17 @@ function getDataSyncAdapter(providerKey) {
|
|
|
15
16
|
function getAllDataSyncAdapters() {
|
|
16
17
|
return Array.from(getAdapterRegistry().values());
|
|
17
18
|
}
|
|
19
|
+
function resolveProviderKey(integrationId) {
|
|
20
|
+
return getIntegration(integrationId)?.providerKey ?? integrationId;
|
|
21
|
+
}
|
|
22
|
+
function resolveAdapterForIntegration(integrationId) {
|
|
23
|
+
return getDataSyncAdapter(resolveProviderKey(integrationId)) ?? null;
|
|
24
|
+
}
|
|
18
25
|
export {
|
|
19
26
|
getAllDataSyncAdapters,
|
|
20
27
|
getDataSyncAdapter,
|
|
21
|
-
registerDataSyncAdapter
|
|
28
|
+
registerDataSyncAdapter,
|
|
29
|
+
resolveAdapterForIntegration,
|
|
30
|
+
resolveProviderKey
|
|
22
31
|
};
|
|
23
32
|
//# sourceMappingURL=adapter-registry.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/data_sync/lib/adapter-registry.ts"],
|
|
4
|
-
"sourcesContent": ["import type { DataSyncAdapter } from './adapter'\n\nconst DATA_SYNC_ADAPTER_REGISTRY_KEY = Symbol.for('@open-mercato/data-sync/adapter-registry')\n\ntype GlobalWithDataSyncRegistry = typeof globalThis & {\n [DATA_SYNC_ADAPTER_REGISTRY_KEY]?: Map<string, DataSyncAdapter>\n}\n\nfunction getAdapterRegistry(): Map<string, DataSyncAdapter> {\n const globalScope = globalThis as GlobalWithDataSyncRegistry\n if (!globalScope[DATA_SYNC_ADAPTER_REGISTRY_KEY]) {\n globalScope[DATA_SYNC_ADAPTER_REGISTRY_KEY] = new Map<string, DataSyncAdapter>()\n }\n return globalScope[DATA_SYNC_ADAPTER_REGISTRY_KEY]\n}\n\nexport function registerDataSyncAdapter(adapter: DataSyncAdapter): void {\n getAdapterRegistry().set(adapter.providerKey, adapter)\n}\n\nexport function getDataSyncAdapter(providerKey: string): DataSyncAdapter | undefined {\n return getAdapterRegistry().get(providerKey)\n}\n\nexport function getAllDataSyncAdapters(): DataSyncAdapter[] {\n return Array.from(getAdapterRegistry().values())\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import { getIntegration } from '@open-mercato/shared/modules/integrations/types'\nimport type { DataSyncAdapter } from './adapter'\n\nconst DATA_SYNC_ADAPTER_REGISTRY_KEY = Symbol.for('@open-mercato/data-sync/adapter-registry')\n\ntype GlobalWithDataSyncRegistry = typeof globalThis & {\n [DATA_SYNC_ADAPTER_REGISTRY_KEY]?: Map<string, DataSyncAdapter>\n}\n\nfunction getAdapterRegistry(): Map<string, DataSyncAdapter> {\n const globalScope = globalThis as GlobalWithDataSyncRegistry\n if (!globalScope[DATA_SYNC_ADAPTER_REGISTRY_KEY]) {\n globalScope[DATA_SYNC_ADAPTER_REGISTRY_KEY] = new Map<string, DataSyncAdapter>()\n }\n return globalScope[DATA_SYNC_ADAPTER_REGISTRY_KEY]\n}\n\nexport function registerDataSyncAdapter(adapter: DataSyncAdapter): void {\n getAdapterRegistry().set(adapter.providerKey, adapter)\n}\n\nexport function getDataSyncAdapter(providerKey: string): DataSyncAdapter | undefined {\n return getAdapterRegistry().get(providerKey)\n}\n\nexport function getAllDataSyncAdapters(): DataSyncAdapter[] {\n return Array.from(getAdapterRegistry().values())\n}\n\nexport function resolveProviderKey(integrationId: string): string {\n return getIntegration(integrationId)?.providerKey ?? integrationId\n}\n\n/**\n * The adapter serving an integration. Single source of truth on purpose: the\n * engine uses it to decide whether to WRITE the shared cursor row and the start\n * paths use it to decide whether to READ one, and those two decisions must agree\n * for every integration or an opted-out entity type silently resumes wrong.\n */\nexport function resolveAdapterForIntegration(integrationId: string): DataSyncAdapter | null {\n return getDataSyncAdapter(resolveProviderKey(integrationId)) ?? null\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,sBAAsB;AAG/B,MAAM,iCAAiC,uBAAO,IAAI,0CAA0C;AAM5F,SAAS,qBAAmD;AAC1D,QAAM,cAAc;AACpB,MAAI,CAAC,YAAY,8BAA8B,GAAG;AAChD,gBAAY,8BAA8B,IAAI,oBAAI,IAA6B;AAAA,EACjF;AACA,SAAO,YAAY,8BAA8B;AACnD;AAEO,SAAS,wBAAwB,SAAgC;AACtE,qBAAmB,EAAE,IAAI,QAAQ,aAAa,OAAO;AACvD;AAEO,SAAS,mBAAmB,aAAkD;AACnF,SAAO,mBAAmB,EAAE,IAAI,WAAW;AAC7C;AAEO,SAAS,yBAA4C;AAC1D,SAAO,MAAM,KAAK,mBAAmB,EAAE,OAAO,CAAC;AACjD;AAEO,SAAS,mBAAmB,eAA+B;AAChE,SAAO,eAAe,aAAa,GAAG,eAAe;AACvD;AAQO,SAAS,6BAA6B,eAA+C;AAC1F,SAAO,mBAAmB,mBAAmB,aAAa,CAAC,KAAK;AAClE;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { resolveAdapterForIntegration } from "./adapter-registry.js";
|
|
2
|
+
function persistsSharedCursor(adapter, entityType) {
|
|
3
|
+
return adapter?.persistsSharedCursor?.(entityType) ?? true;
|
|
4
|
+
}
|
|
5
|
+
async function resolveStartCursor(params) {
|
|
6
|
+
const { syncRunService, adapter, integrationId, entityType, direction, scope } = params;
|
|
7
|
+
if (persistsSharedCursor(adapter, entityType)) {
|
|
8
|
+
return syncRunService.resolveCursor(integrationId, entityType, direction, scope);
|
|
9
|
+
}
|
|
10
|
+
return syncRunService.resolveResumeCursor(integrationId, entityType, direction, scope);
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
persistsSharedCursor,
|
|
14
|
+
resolveAdapterForIntegration,
|
|
15
|
+
resolveStartCursor
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=start-cursor.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/data_sync/lib/start-cursor.ts"],
|
|
4
|
+
"sourcesContent": ["import type { DataSyncAdapter } from './adapter'\nimport type { SyncRunService } from './sync-run-service'\n\ntype SyncScope = {\n organizationId: string\n tenantId: string\n}\n\nexport { resolveAdapterForIntegration } from './adapter-registry'\n\nexport function persistsSharedCursor(adapter: DataSyncAdapter | null | undefined, entityType: string): boolean {\n return adapter?.persistsSharedCursor?.(entityType) ?? true\n}\n\n/**\n * Start position for a non-full run. Entity types that mirror their cursor into\n * the shared `sync_cursors` row read it from there. Entity types whose adapter\n * opted out never write that row, so reading it would silently turn every\n * incremental run into a full one \u2014 they resume from their own last run\n * instead.\n */\nexport async function resolveStartCursor(params: {\n syncRunService: SyncRunService\n adapter?: DataSyncAdapter | null\n integrationId: string\n entityType: string\n direction: 'import' | 'export'\n scope: SyncScope\n}): Promise<string | null> {\n const { syncRunService, adapter, integrationId, entityType, direction, scope } = params\n if (persistsSharedCursor(adapter, entityType)) {\n return syncRunService.resolveCursor(integrationId, entityType, direction, scope)\n }\n return syncRunService.resolveResumeCursor(integrationId, entityType, direction, scope)\n}\n"],
|
|
5
|
+
"mappings": "AAQA,SAAS,oCAAoC;AAEtC,SAAS,qBAAqB,SAA6C,YAA6B;AAC7G,SAAO,SAAS,uBAAuB,UAAU,KAAK;AACxD;AASA,eAAsB,mBAAmB,QAOd;AACzB,QAAM,EAAE,gBAAgB,SAAS,eAAe,YAAY,WAAW,MAAM,IAAI;AACjF,MAAI,qBAAqB,SAAS,UAAU,GAAG;AAC7C,WAAO,eAAe,cAAc,eAAe,YAAY,WAAW,KAAK;AAAA,EACjF;AACA,SAAO,eAAe,oBAAoB,eAAe,YAAY,WAAW,KAAK;AACvF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { STALE_JOB_TIMEOUT_SECONDS } from "../../progress/lib/progressService.js";
|
|
2
2
|
import { refreshCoverageSnapshot } from "../../query_index/lib/coverage.js";
|
|
3
3
|
import { emitDataSyncEvent } from "../events.js";
|
|
4
|
-
import { getDataSyncAdapter } from "./adapter-registry.js";
|
|
4
|
+
import { getDataSyncAdapter, resolveProviderKey } from "./adapter-registry.js";
|
|
5
5
|
import { SyncRunOwnershipConflictError } from "./sync-run-service.js";
|
|
6
6
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
7
7
|
const logger = createLogger("data_sync").child({ component: "sync-engine" });
|
|
8
|
-
function resolveProviderKey(integrationId) {
|
|
9
|
-
return getIntegration(integrationId)?.providerKey ?? integrationId;
|
|
10
|
-
}
|
|
11
8
|
function applyImportCounters(batch) {
|
|
12
9
|
let createdCount = 0;
|
|
13
10
|
let updatedCount = 0;
|
|
@@ -37,6 +34,25 @@ function applyExportCounters(batch) {
|
|
|
37
34
|
processedCount: batch.results.length
|
|
38
35
|
};
|
|
39
36
|
}
|
|
37
|
+
const HEARTBEAT_TICK_MS = STALE_JOB_TIMEOUT_SECONDS * 1e3 / 4;
|
|
38
|
+
async function* withHeartbeat(source, tick, intervalMs) {
|
|
39
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
40
|
+
try {
|
|
41
|
+
while (true) {
|
|
42
|
+
const timer = setInterval(tick, intervalMs);
|
|
43
|
+
let result;
|
|
44
|
+
try {
|
|
45
|
+
result = await iterator.next();
|
|
46
|
+
} finally {
|
|
47
|
+
clearInterval(timer);
|
|
48
|
+
}
|
|
49
|
+
if (result.done) return;
|
|
50
|
+
yield result.value;
|
|
51
|
+
}
|
|
52
|
+
} finally {
|
|
53
|
+
await iterator.return?.();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
40
56
|
function createSyncEngine(deps) {
|
|
41
57
|
const { syncRunService, integrationCredentialsService, integrationLogService, integrationStateService, progressService } = deps;
|
|
42
58
|
async function resolveMapping(adapter, entityType, scope) {
|
|
@@ -60,6 +76,37 @@ function createSyncEngine(deps) {
|
|
|
60
76
|
}
|
|
61
77
|
);
|
|
62
78
|
}
|
|
79
|
+
async function seedProcessedCount(progressJobId, scope) {
|
|
80
|
+
if (!progressJobId) return 0;
|
|
81
|
+
const job = await progressService.getJob(progressJobId, {
|
|
82
|
+
tenantId: scope.tenantId,
|
|
83
|
+
organizationId: scope.organizationId,
|
|
84
|
+
userId: scope.userId
|
|
85
|
+
});
|
|
86
|
+
return job?.processedCount ?? 0;
|
|
87
|
+
}
|
|
88
|
+
function makeHeartbeatTick(progressJobId, scope) {
|
|
89
|
+
const touchJobHeartbeat = progressService.touchJobHeartbeat?.bind(progressService);
|
|
90
|
+
if (!progressJobId || !touchJobHeartbeat) return () => {
|
|
91
|
+
};
|
|
92
|
+
let inFlight = false;
|
|
93
|
+
return () => {
|
|
94
|
+
if (inFlight) return;
|
|
95
|
+
inFlight = true;
|
|
96
|
+
touchJobHeartbeat(progressJobId, {
|
|
97
|
+
tenantId: scope.tenantId,
|
|
98
|
+
organizationId: scope.organizationId,
|
|
99
|
+
userId: scope.userId
|
|
100
|
+
}).catch((error) => {
|
|
101
|
+
logger.warn("Progress heartbeat failed", {
|
|
102
|
+
progressJobId,
|
|
103
|
+
error: error instanceof Error ? error.message : String(error)
|
|
104
|
+
});
|
|
105
|
+
}).finally(() => {
|
|
106
|
+
inFlight = false;
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
}
|
|
63
110
|
async function refreshCoverageSnapshots(entityTypes, scope) {
|
|
64
111
|
if (!entityTypes || entityTypes.length === 0) return;
|
|
65
112
|
await Promise.allSettled(
|
|
@@ -312,6 +359,7 @@ function createSyncEngine(deps) {
|
|
|
312
359
|
throw new Error(`No import adapter registered for provider ${providerKey}`);
|
|
313
360
|
}
|
|
314
361
|
const operationalTelemetry = adapter.operationalTelemetry === true;
|
|
362
|
+
const persistSharedCursor = adapter.persistsSharedCursor?.(run.entityType) ?? true;
|
|
315
363
|
const credentials = await integrationCredentialsService.resolve(run.integrationId, scope);
|
|
316
364
|
if (!credentials) {
|
|
317
365
|
throw new Error(`Integration ${run.integrationId} is missing credentials`);
|
|
@@ -358,19 +406,23 @@ function createSyncEngine(deps) {
|
|
|
358
406
|
});
|
|
359
407
|
}
|
|
360
408
|
const mapping = await resolveMapping(adapter, run.entityType, scope);
|
|
361
|
-
let processedCount =
|
|
409
|
+
let processedCount = await seedProcessedCount(run.progressJobId, scope);
|
|
362
410
|
let totalCount = null;
|
|
363
411
|
let committedBatches = activeRun.batchesCompleted ?? 0;
|
|
364
412
|
try {
|
|
365
|
-
for await (const batch of
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
413
|
+
for await (const batch of withHeartbeat(
|
|
414
|
+
adapter.streamImport({
|
|
415
|
+
entityType: run.entityType,
|
|
416
|
+
cursor: run.cursor ?? void 0,
|
|
417
|
+
batchSize,
|
|
418
|
+
credentials,
|
|
419
|
+
mapping,
|
|
420
|
+
scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },
|
|
421
|
+
runId: run.id
|
|
422
|
+
}),
|
|
423
|
+
makeHeartbeatTick(run.progressJobId, scope),
|
|
424
|
+
HEARTBEAT_TICK_MS
|
|
425
|
+
)) {
|
|
374
426
|
if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId)) {
|
|
375
427
|
await finalizeRun(run.id, "cancelled", scope, void 0, operationalTelemetry);
|
|
376
428
|
return;
|
|
@@ -387,7 +439,7 @@ function createSyncEngine(deps) {
|
|
|
387
439
|
},
|
|
388
440
|
batch.cursor,
|
|
389
441
|
scope,
|
|
390
|
-
committedBatches
|
|
442
|
+
{ expectedBatchesCompleted: committedBatches, persistSharedCursor }
|
|
391
443
|
);
|
|
392
444
|
committedBatches += 1;
|
|
393
445
|
await updateProgress(run.progressJobId, processedCount, totalCount, scope);
|
|
@@ -455,6 +507,7 @@ function createSyncEngine(deps) {
|
|
|
455
507
|
throw new Error(`No export adapter registered for provider ${providerKey}`);
|
|
456
508
|
}
|
|
457
509
|
const operationalTelemetry = adapter.operationalTelemetry === true;
|
|
510
|
+
const persistSharedCursor = adapter.persistsSharedCursor?.(run.entityType) ?? true;
|
|
458
511
|
const credentials = await integrationCredentialsService.resolve(run.integrationId, scope);
|
|
459
512
|
if (!credentials) {
|
|
460
513
|
throw new Error(`Integration ${run.integrationId} is missing credentials`);
|
|
@@ -501,18 +554,22 @@ function createSyncEngine(deps) {
|
|
|
501
554
|
});
|
|
502
555
|
}
|
|
503
556
|
const mapping = await resolveMapping(adapter, run.entityType, scope);
|
|
504
|
-
let processedCount =
|
|
557
|
+
let processedCount = await seedProcessedCount(run.progressJobId, scope);
|
|
505
558
|
let committedBatches = activeRun.batchesCompleted ?? 0;
|
|
506
559
|
try {
|
|
507
|
-
for await (const batch of
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
560
|
+
for await (const batch of withHeartbeat(
|
|
561
|
+
adapter.streamExport({
|
|
562
|
+
entityType: run.entityType,
|
|
563
|
+
cursor: run.cursor ?? void 0,
|
|
564
|
+
batchSize,
|
|
565
|
+
credentials,
|
|
566
|
+
mapping,
|
|
567
|
+
scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },
|
|
568
|
+
runId: run.id
|
|
569
|
+
}),
|
|
570
|
+
makeHeartbeatTick(run.progressJobId, scope),
|
|
571
|
+
HEARTBEAT_TICK_MS
|
|
572
|
+
)) {
|
|
516
573
|
if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId)) {
|
|
517
574
|
await finalizeRun(run.id, "cancelled", scope, void 0, operationalTelemetry);
|
|
518
575
|
return;
|
|
@@ -530,7 +587,7 @@ function createSyncEngine(deps) {
|
|
|
530
587
|
},
|
|
531
588
|
batch.cursor,
|
|
532
589
|
scope,
|
|
533
|
-
committedBatches
|
|
590
|
+
{ expectedBatchesCompleted: committedBatches, persistSharedCursor }
|
|
534
591
|
);
|
|
535
592
|
committedBatches += 1;
|
|
536
593
|
await updateProgress(run.progressJobId, processedCount, null, scope);
|