@open-mercato/core 0.6.7-develop.6744.1.0483b18565 → 0.6.7-develop.6749.1.6b54c56dfe
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/auth/api/profile/route.js +8 -2
- package/dist/modules/auth/api/profile/route.js.map +2 -2
- package/dist/modules/auth/backend/auth/profile/page.js.map +2 -2
- package/dist/modules/auth/backend/profile/change-password/page.js.map +2 -2
- package/dist/modules/data_sync/lib/queue-policy.js +15 -0
- package/dist/modules/data_sync/lib/queue-policy.js.map +7 -0
- package/dist/modules/data_sync/lib/queue.js +16 -1
- package/dist/modules/data_sync/lib/queue.js.map +2 -2
- package/dist/modules/data_sync/lib/start-run.js +2 -1
- package/dist/modules/data_sync/lib/start-run.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-engine.js +35 -2
- package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-run-service.js +70 -2
- package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
- package/dist/modules/data_sync/workers/sync-export.js +9 -2
- package/dist/modules/data_sync/workers/sync-export.js.map +2 -2
- package/dist/modules/data_sync/workers/sync-import.js +9 -2
- package/dist/modules/data_sync/workers/sync-import.js.map +2 -2
- package/dist/modules/notifications/api/route.js +3 -1
- package/dist/modules/notifications/api/route.js.map +2 -2
- package/dist/modules/notifications/api/unread-count/route.js +25 -6
- package/dist/modules/notifications/api/unread-count/route.js.map +2 -2
- package/dist/modules/notifications/lib/notificationScope.js +26 -0
- package/dist/modules/notifications/lib/notificationScope.js.map +7 -0
- package/dist/modules/notifications/lib/notificationService.js +7 -3
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/notifications/lib/routeHelpers.js +16 -2
- package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/profile/route.ts +8 -2
- package/src/modules/auth/backend/auth/profile/page.tsx +1 -0
- package/src/modules/auth/backend/profile/change-password/page.tsx +1 -0
- package/src/modules/data_sync/lib/adapter.ts +18 -0
- package/src/modules/data_sync/lib/queue-policy.ts +30 -0
- package/src/modules/data_sync/lib/queue.ts +19 -1
- package/src/modules/data_sync/lib/start-run.ts +2 -1
- package/src/modules/data_sync/lib/sync-engine.ts +46 -0
- package/src/modules/data_sync/lib/sync-run-service.ts +82 -1
- package/src/modules/data_sync/workers/sync-export.ts +8 -1
- package/src/modules/data_sync/workers/sync-import.ts +8 -1
- package/src/modules/notifications/api/route.ts +2 -0
- package/src/modules/notifications/api/unread-count/route.ts +42 -5
- package/src/modules/notifications/lib/notificationScope.ts +40 -0
- package/src/modules/notifications/lib/notificationService.ts +5 -0
- package/src/modules/notifications/lib/routeHelpers.ts +19 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -12,6 +12,7 @@ import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
|
12
12
|
const logger = createLogger("auth").child({ component: "profile" });
|
|
13
13
|
const profileResponseSchema = z.object({
|
|
14
14
|
email: z.string().email(),
|
|
15
|
+
name: z.string().nullable().optional(),
|
|
15
16
|
roles: z.array(z.string())
|
|
16
17
|
});
|
|
17
18
|
const passwordSchema = buildPasswordSchema();
|
|
@@ -92,7 +93,12 @@ async function GET(req) {
|
|
|
92
93
|
if (!user) {
|
|
93
94
|
return NextResponse.json({ error: translate("auth.users.form.errors.notFound", "User not found") }, { status: 404 });
|
|
94
95
|
}
|
|
95
|
-
|
|
96
|
+
const displayName = typeof user.name === "string" ? user.name.trim() : "";
|
|
97
|
+
return NextResponse.json({
|
|
98
|
+
email: String(user.email),
|
|
99
|
+
name: displayName.length > 0 ? displayName : null,
|
|
100
|
+
roles: auth.roles ?? []
|
|
101
|
+
});
|
|
96
102
|
} catch (err) {
|
|
97
103
|
logger.error("Profile load failed", { err });
|
|
98
104
|
return NextResponse.json({ error: translate("auth.profile.form.errors.load", "Failed to load profile.") }, { status: 400 });
|
|
@@ -191,7 +197,7 @@ const openApi = {
|
|
|
191
197
|
methods: {
|
|
192
198
|
GET: {
|
|
193
199
|
summary: "Get current profile",
|
|
194
|
-
description: "Returns the email address for the signed-in user.",
|
|
200
|
+
description: "Returns the email address, display name, and roles for the signed-in user. The display name is null when unset.",
|
|
195
201
|
responses: [
|
|
196
202
|
{ status: 200, description: "Profile payload", schema: profileResponseSchema },
|
|
197
203
|
{ status: 401, description: "Unauthorized", schema: z.object({ error: z.string() }) },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/auth/api/profile/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolicy'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('auth').child({ component: 'profile' })\n\nconst profileResponseSchema = z.object({\n email: z.string().email(),\n roles: z.array(z.string()),\n})\n\nconst passwordSchema = buildPasswordSchema()\n\nconst updateSchemaBase = z.object({\n email: z.string().email().optional(),\n currentPassword: z.string().trim().min(1).optional(),\n password: passwordSchema.optional(),\n})\n\nfunction buildUpdateSchema(translate: (key: string, fallback: string) => string) {\n return updateSchemaBase.superRefine((data, ctx) => {\n if (!data.email && !data.password) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: translate(\n 'auth.profile.form.errors.emailOrPasswordRequired',\n 'Provide an email or password.',\n ),\n path: ['email'],\n })\n }\n if (data.password && !data.currentPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: translate(\n 'auth.profile.form.errors.currentPasswordRequired',\n 'Current password is required.',\n ),\n path: ['currentPassword'],\n })\n }\n if (data.currentPassword && !data.password) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: translate(\n 'auth.profile.form.errors.newPasswordRequired',\n 'New password is required.',\n ),\n path: ['password'],\n })\n }\n })\n}\n\nconst updateSchema = buildUpdateSchema((_key, fallback) => fallback)\n\nconst profileUpdateResponseSchema = z.object({\n ok: z.literal(true),\n email: z.string().email(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true },\n PUT: { requireAuth: true },\n}\n\nfunction buildCommandContext(container: Awaited<ReturnType<typeof createRequestContainer>>, auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>, req: Request): CommandRuntimeContext {\n return {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n }\n}\n\nexport async function GET(req: Request) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: translate('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n try {\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n const user = await findOneWithDecryption(\n em,\n User,\n { id: auth.sub, deletedAt: null },\n undefined,\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n )\n if (!user) {\n return NextResponse.json({ error: translate('auth.users.form.errors.notFound', 'User not found') }, { status: 404 })\n }\n return NextResponse.json({
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolicy'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('auth').child({ component: 'profile' })\n\nconst profileResponseSchema = z.object({\n email: z.string().email(),\n name: z.string().nullable().optional(),\n roles: z.array(z.string()),\n})\n\nconst passwordSchema = buildPasswordSchema()\n\nconst updateSchemaBase = z.object({\n email: z.string().email().optional(),\n currentPassword: z.string().trim().min(1).optional(),\n password: passwordSchema.optional(),\n})\n\nfunction buildUpdateSchema(translate: (key: string, fallback: string) => string) {\n return updateSchemaBase.superRefine((data, ctx) => {\n if (!data.email && !data.password) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: translate(\n 'auth.profile.form.errors.emailOrPasswordRequired',\n 'Provide an email or password.',\n ),\n path: ['email'],\n })\n }\n if (data.password && !data.currentPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: translate(\n 'auth.profile.form.errors.currentPasswordRequired',\n 'Current password is required.',\n ),\n path: ['currentPassword'],\n })\n }\n if (data.currentPassword && !data.password) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: translate(\n 'auth.profile.form.errors.newPasswordRequired',\n 'New password is required.',\n ),\n path: ['password'],\n })\n }\n })\n}\n\nconst updateSchema = buildUpdateSchema((_key, fallback) => fallback)\n\nconst profileUpdateResponseSchema = z.object({\n ok: z.literal(true),\n email: z.string().email(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true },\n PUT: { requireAuth: true },\n}\n\nfunction buildCommandContext(container: Awaited<ReturnType<typeof createRequestContainer>>, auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>, req: Request): CommandRuntimeContext {\n return {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n }\n}\n\nexport async function GET(req: Request) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: translate('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n try {\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n const user = await findOneWithDecryption(\n em,\n User,\n { id: auth.sub, deletedAt: null },\n undefined,\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n )\n if (!user) {\n return NextResponse.json({ error: translate('auth.users.form.errors.notFound', 'User not found') }, { status: 404 })\n }\n const displayName = typeof user.name === 'string' ? user.name.trim() : ''\n return NextResponse.json({\n email: String(user.email),\n name: displayName.length > 0 ? displayName : null,\n roles: auth.roles ?? [],\n })\n } catch (err) {\n logger.error('Profile load failed', { err })\n return NextResponse.json({ error: translate('auth.profile.form.errors.load', 'Failed to load profile.') }, { status: 400 })\n }\n}\n\nexport async function PUT(req: Request) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: translate('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n try {\n const body = await req.json().catch(() => ({}))\n const parsed = buildUpdateSchema(translate).safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n {\n error: translate('auth.profile.form.errors.invalid', 'Invalid profile update.'),\n issues: parsed.error.issues,\n },\n { status: 400 },\n )\n }\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n const authService = container.resolve('authService') as AuthService\n if (parsed.data.password) {\n const user = await findOneWithDecryption(\n em,\n User,\n { id: auth.sub, deletedAt: null },\n undefined,\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n )\n if (!user) {\n return NextResponse.json({ error: translate('auth.users.form.errors.notFound', 'User not found') }, { status: 404 })\n }\n const currentPassword = parsed.data.currentPassword?.trim() ?? ''\n const isCurrentPasswordValid = await authService.verifyPassword(user, currentPassword)\n if (!isCurrentPasswordValid) {\n const message = translate(\n 'auth.profile.form.errors.currentPasswordInvalid',\n 'Current password is incorrect.',\n )\n return NextResponse.json(\n {\n error: message,\n issues: [{ path: ['currentPassword'], message }],\n },\n { status: 400 },\n )\n }\n }\n const commandBus = (container.resolve('commandBus') as CommandBus)\n const ctx = buildCommandContext(container, auth, req)\n const { result } = await commandBus.execute<{ id: string; email?: string; password?: string }, User>(\n 'auth.users.update',\n {\n input: {\n id: auth.sub,\n email: parsed.data.email,\n password: parsed.data.password,\n },\n ctx,\n },\n )\n const roles = await authService.getUserRoles(result, result.tenantId ? String(result.tenantId) : null)\n const jwt = signJwt({\n sub: String(result.id),\n sid: typeof auth.sid === 'string' ? auth.sid : undefined,\n tenantId: result.tenantId ? String(result.tenantId) : null,\n orgId: result.organizationId ? String(result.organizationId) : null,\n email: result.email,\n roles,\n })\n const res = NextResponse.json({ ok: true, email: String(result.email) })\n res.cookies.set('auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n return res\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('Profile update failed', { err })\n return NextResponse.json({ error: translate('auth.profile.form.errors.save', 'Failed to update profile.') }, { status: 400 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Profile settings',\n methods: {\n GET: {\n summary: 'Get current profile',\n description: 'Returns the email address, display name, and roles for the signed-in user. The display name is null when unset.',\n responses: [\n { status: 200, description: 'Profile payload', schema: profileResponseSchema },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 404, description: 'User not found', schema: z.object({ error: z.string() }) },\n ],\n },\n PUT: {\n summary: 'Update current profile',\n description: 'Updates the email address or password for the signed-in user.',\n requestBody: {\n contentType: 'application/json',\n schema: updateSchema,\n },\n responses: [\n { status: 200, description: 'Profile updated', schema: profileUpdateResponseSchema },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/auth/backend/auth/profile/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { z } from 'zod'\nimport { Save } from 'lucide-react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { buildPasswordSchema, formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/shared/lib/auth/passwordPolicy'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('auth').child({ component: 'profile-page' })\n\ntype ProfileResponse = {\n email?: string | null\n}\n\ntype ProfileUpdateResponse = {\n ok?: boolean\n email?: string | null\n}\n\ntype ProfileFormValues = {\n email: string\n currentPassword?: string\n password?: string\n confirmPassword?: string\n}\n\nexport default function AuthProfilePage() {\n const t = useT()\n const router = useRouter()\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [email, setEmail] = React.useState('')\n const [formKey, setFormKey] = React.useState(0)\n const formId = React.useId()\n const passwordPolicy = React.useMemo(() => getPasswordPolicy(), [])\n const passwordRequirements = React.useMemo(\n () => formatPasswordRequirements(passwordPolicy, t),\n [passwordPolicy, t],\n )\n const passwordDescription = React.useMemo(() => (\n passwordRequirements\n ? t('auth.password.requirements.help', 'Password requirements: {requirements}', { requirements: passwordRequirements })\n : undefined\n ), [passwordRequirements, t])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setLoading(true)\n setError(null)\n try {\n const { ok, result } = await apiCall<ProfileResponse>('/api/auth/profile')\n if (!ok) throw new Error(t('auth.profile.form.errors.load', 'Failed to load profile.'))\n const resolvedEmail = typeof result?.email === 'string' ? result.email : ''\n if (!cancelled) setEmail(resolvedEmail)\n } catch (err) {\n logger.error('Failed to load auth profile', { err })\n if (!cancelled) setError(t('auth.profile.form.errors.load', 'Failed to load profile.'))\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [t])\n\n const fields = React.useMemo<CrudField[]>(() => [\n { id: 'email', label: t('auth.profile.form.email', 'Email'), type: 'text', required: true },\n {\n id: 'currentPassword',\n label: t('auth.profile.form.currentPassword', 'Current password'),\n type: 'password',\n },\n {\n id: 'password',\n label: t('auth.profile.form.password', 'New password'),\n type: 'password',\n description: passwordDescription,\n },\n {\n id: 'confirmPassword',\n label: t('auth.profile.form.confirmPassword', 'Confirm new password'),\n type: 'password',\n },\n ], [passwordDescription, t])\n\n const schema = React.useMemo(() => {\n const passwordSchema = buildPasswordSchema({\n policy: passwordPolicy,\n message: t('auth.profile.form.errors.passwordRequirements', 'Password must meet the requirements.'),\n })\n const optionalPasswordSchema = z.union([z.literal(''), passwordSchema]).optional()\n return z.object({\n email: z.string().trim().min(1, t('auth.profile.form.errors.emailRequired', 'Email is required.')),\n currentPassword: z.string().optional(),\n password: optionalPasswordSchema,\n confirmPassword: z.string().optional(),\n }).superRefine((values, ctx) => {\n const currentPassword = values.currentPassword?.trim() ?? ''\n const password = values.password?.trim() ?? ''\n const confirmPassword = values.confirmPassword?.trim() ?? ''\n const hasPasswordIntent = Boolean(currentPassword || password || confirmPassword)\n\n if (hasPasswordIntent && !currentPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.currentPasswordRequired', 'Current password is required.'),\n path: ['currentPassword'],\n })\n }\n if (hasPasswordIntent && !password) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.newPasswordRequired', 'New password is required.'),\n path: ['password'],\n })\n }\n if (hasPasswordIntent && !confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.confirmPasswordRequired', 'Please confirm the new password.'),\n path: ['confirmPassword'],\n })\n }\n if (password && confirmPassword && password !== confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.passwordMismatch', 'Passwords do not match.'),\n path: ['confirmPassword'],\n })\n }\n })\n }, [passwordPolicy, t])\n\n const handleSubmit = React.useCallback(async (values: ProfileFormValues) => {\n const nextEmail = values.email?.trim() ?? ''\n const currentPassword = values.currentPassword?.trim() ?? ''\n const password = values.password?.trim() ?? ''\n\n if (!password && nextEmail === email) {\n throw createCrudFormError(t('auth.profile.form.errors.noChanges', 'No changes to save.'))\n }\n\n const payload: { email: string; currentPassword?: string; password?: string } = { email: nextEmail }\n if (password) payload.password = password\n if (password) payload.currentPassword = currentPassword\n\n const result = await readApiResultOrThrow<ProfileUpdateResponse>(\n '/api/auth/profile',\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n { errorMessage: t('auth.profile.form.errors.save', 'Failed to update profile.') },\n )\n\n const resolvedEmail = typeof result?.email === 'string' ? result.email : nextEmail\n setEmail(resolvedEmail)\n setFormKey((prev) => prev + 1)\n flash(t('auth.profile.form.success', 'Profile updated.'), 'success')\n router.refresh()\n }, [email, router, t])\n\n return (\n <Page>\n <PageBody>\n {loading ? (\n <LoadingMessage label={t('auth.profile.form.loading', 'Loading profile...')} />\n ) : error ? (\n <ErrorMessage label={error} />\n ) : (\n <section className=\"space-y-6 rounded-lg border bg-background p-6\">\n <header className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('auth.profile.title', 'Profile')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('auth.profile.subtitle', 'Change password')}\n </p>\n </div>\n <Button type=\"submit\" form={formId}>\n <Save className=\"size-4 mr-2\" />\n {t('auth.profile.form.save', 'Save changes')}\n </Button>\n </header>\n <CrudForm<ProfileFormValues>\n key={formKey}\n formId={formId}\n schema={schema}\n fields={fields}\n initialValues={{\n email,\n currentPassword: '',\n password: '',\n confirmPassword: '',\n }}\n submitLabel={t('auth.profile.form.save', 'Save changes')}\n onSubmit={handleSubmit}\n embedded\n hideFooterActions\n />\n </section>\n )}\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { z } from 'zod'\nimport { Save } from 'lucide-react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { buildPasswordSchema, formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/shared/lib/auth/passwordPolicy'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('auth').child({ component: 'profile-page' })\n\ntype ProfileResponse = {\n email?: string | null\n name?: string | null\n}\n\ntype ProfileUpdateResponse = {\n ok?: boolean\n email?: string | null\n}\n\ntype ProfileFormValues = {\n email: string\n currentPassword?: string\n password?: string\n confirmPassword?: string\n}\n\nexport default function AuthProfilePage() {\n const t = useT()\n const router = useRouter()\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [email, setEmail] = React.useState('')\n const [formKey, setFormKey] = React.useState(0)\n const formId = React.useId()\n const passwordPolicy = React.useMemo(() => getPasswordPolicy(), [])\n const passwordRequirements = React.useMemo(\n () => formatPasswordRequirements(passwordPolicy, t),\n [passwordPolicy, t],\n )\n const passwordDescription = React.useMemo(() => (\n passwordRequirements\n ? t('auth.password.requirements.help', 'Password requirements: {requirements}', { requirements: passwordRequirements })\n : undefined\n ), [passwordRequirements, t])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setLoading(true)\n setError(null)\n try {\n const { ok, result } = await apiCall<ProfileResponse>('/api/auth/profile')\n if (!ok) throw new Error(t('auth.profile.form.errors.load', 'Failed to load profile.'))\n const resolvedEmail = typeof result?.email === 'string' ? result.email : ''\n if (!cancelled) setEmail(resolvedEmail)\n } catch (err) {\n logger.error('Failed to load auth profile', { err })\n if (!cancelled) setError(t('auth.profile.form.errors.load', 'Failed to load profile.'))\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [t])\n\n const fields = React.useMemo<CrudField[]>(() => [\n { id: 'email', label: t('auth.profile.form.email', 'Email'), type: 'text', required: true },\n {\n id: 'currentPassword',\n label: t('auth.profile.form.currentPassword', 'Current password'),\n type: 'password',\n },\n {\n id: 'password',\n label: t('auth.profile.form.password', 'New password'),\n type: 'password',\n description: passwordDescription,\n },\n {\n id: 'confirmPassword',\n label: t('auth.profile.form.confirmPassword', 'Confirm new password'),\n type: 'password',\n },\n ], [passwordDescription, t])\n\n const schema = React.useMemo(() => {\n const passwordSchema = buildPasswordSchema({\n policy: passwordPolicy,\n message: t('auth.profile.form.errors.passwordRequirements', 'Password must meet the requirements.'),\n })\n const optionalPasswordSchema = z.union([z.literal(''), passwordSchema]).optional()\n return z.object({\n email: z.string().trim().min(1, t('auth.profile.form.errors.emailRequired', 'Email is required.')),\n currentPassword: z.string().optional(),\n password: optionalPasswordSchema,\n confirmPassword: z.string().optional(),\n }).superRefine((values, ctx) => {\n const currentPassword = values.currentPassword?.trim() ?? ''\n const password = values.password?.trim() ?? ''\n const confirmPassword = values.confirmPassword?.trim() ?? ''\n const hasPasswordIntent = Boolean(currentPassword || password || confirmPassword)\n\n if (hasPasswordIntent && !currentPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.currentPasswordRequired', 'Current password is required.'),\n path: ['currentPassword'],\n })\n }\n if (hasPasswordIntent && !password) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.newPasswordRequired', 'New password is required.'),\n path: ['password'],\n })\n }\n if (hasPasswordIntent && !confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.confirmPasswordRequired', 'Please confirm the new password.'),\n path: ['confirmPassword'],\n })\n }\n if (password && confirmPassword && password !== confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.passwordMismatch', 'Passwords do not match.'),\n path: ['confirmPassword'],\n })\n }\n })\n }, [passwordPolicy, t])\n\n const handleSubmit = React.useCallback(async (values: ProfileFormValues) => {\n const nextEmail = values.email?.trim() ?? ''\n const currentPassword = values.currentPassword?.trim() ?? ''\n const password = values.password?.trim() ?? ''\n\n if (!password && nextEmail === email) {\n throw createCrudFormError(t('auth.profile.form.errors.noChanges', 'No changes to save.'))\n }\n\n const payload: { email: string; currentPassword?: string; password?: string } = { email: nextEmail }\n if (password) payload.password = password\n if (password) payload.currentPassword = currentPassword\n\n const result = await readApiResultOrThrow<ProfileUpdateResponse>(\n '/api/auth/profile',\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n { errorMessage: t('auth.profile.form.errors.save', 'Failed to update profile.') },\n )\n\n const resolvedEmail = typeof result?.email === 'string' ? result.email : nextEmail\n setEmail(resolvedEmail)\n setFormKey((prev) => prev + 1)\n flash(t('auth.profile.form.success', 'Profile updated.'), 'success')\n router.refresh()\n }, [email, router, t])\n\n return (\n <Page>\n <PageBody>\n {loading ? (\n <LoadingMessage label={t('auth.profile.form.loading', 'Loading profile...')} />\n ) : error ? (\n <ErrorMessage label={error} />\n ) : (\n <section className=\"space-y-6 rounded-lg border bg-background p-6\">\n <header className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('auth.profile.title', 'Profile')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('auth.profile.subtitle', 'Change password')}\n </p>\n </div>\n <Button type=\"submit\" form={formId}>\n <Save className=\"size-4 mr-2\" />\n {t('auth.profile.form.save', 'Save changes')}\n </Button>\n </header>\n <CrudForm<ProfileFormValues>\n key={formKey}\n formId={formId}\n schema={schema}\n fields={fields}\n initialValues={{\n email,\n currentPassword: '',\n password: '',\n confirmPassword: '',\n }}\n submitLabel={t('auth.profile.form.save', 'Save changes')}\n onSubmit={handleSubmit}\n embedded\n hideFooterActions\n />\n </section>\n )}\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/auth/backend/profile/change-password/page.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { z } from 'zod'\nimport { Save } from 'lucide-react'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { buildPasswordSchema, formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/shared/lib/auth/passwordPolicy'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('auth').child({ component: 'change-password-page' })\n\ntype ProfileResponse = {\n email?: string | null\n}\n\ntype ProfileUpdateResponse = {\n ok?: boolean\n email?: string | null\n}\n\ntype ProfileFormValues = {\n email: string\n currentPassword?: string\n password?: string\n confirmPassword?: string\n}\n\nexport default function ProfileChangePasswordPage() {\n const t = useT()\n const router = useRouter()\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [email, setEmail] = React.useState('')\n const [formKey, setFormKey] = React.useState(0)\n const formId = React.useId()\n const passwordPolicy = React.useMemo(() => getPasswordPolicy(), [])\n const passwordRequirements = React.useMemo(\n () => formatPasswordRequirements(passwordPolicy, t),\n [passwordPolicy, t],\n )\n const passwordDescription = React.useMemo(() => (\n passwordRequirements\n ? t('auth.password.requirements.help', 'Password requirements: {requirements}', { requirements: passwordRequirements })\n : undefined\n ), [passwordRequirements, t])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setLoading(true)\n setError(null)\n try {\n const { ok, result } = await apiCall<ProfileResponse>('/api/auth/profile')\n if (!ok) throw new Error(t('auth.profile.form.errors.load', 'Failed to load profile.'))\n const resolvedEmail = typeof result?.email === 'string' ? result.email : ''\n if (!cancelled) setEmail(resolvedEmail)\n } catch (err) {\n logger.error('Failed to load auth profile', { err })\n if (!cancelled) setError(t('auth.profile.form.errors.load', 'Failed to load profile.'))\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [t])\n\n const fields = React.useMemo<CrudField[]>(() => [\n { id: 'email', label: t('auth.profile.form.email', 'Email'), type: 'text', required: true },\n {\n id: 'currentPassword',\n label: t('auth.profile.form.currentPassword', 'Current password'),\n type: 'password',\n },\n {\n id: 'password',\n label: t('auth.profile.form.password', 'New password'),\n type: 'password',\n description: passwordDescription,\n },\n {\n id: 'confirmPassword',\n label: t('auth.profile.form.confirmPassword', 'Confirm new password'),\n type: 'password',\n },\n ], [passwordDescription, t])\n\n const schema = React.useMemo(() => {\n const passwordSchema = buildPasswordSchema({\n policy: passwordPolicy,\n message: t('auth.profile.form.errors.passwordRequirements', 'Password must meet the requirements.'),\n })\n const optionalPasswordSchema = z.union([z.literal(''), passwordSchema]).optional()\n return z.object({\n email: z.string().trim().min(1, t('auth.profile.form.errors.emailRequired', 'Email is required.')),\n currentPassword: z.string().optional(),\n password: optionalPasswordSchema,\n confirmPassword: z.string().optional(),\n }).superRefine((values, ctx) => {\n const currentPassword = values.currentPassword?.trim() ?? ''\n const password = values.password?.trim() ?? ''\n const confirmPassword = values.confirmPassword?.trim() ?? ''\n const hasPasswordIntent = Boolean(currentPassword || password || confirmPassword)\n\n if (hasPasswordIntent && !currentPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.currentPasswordRequired', 'Current password is required.'),\n path: ['currentPassword'],\n })\n }\n if (hasPasswordIntent && !password) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.newPasswordRequired', 'New password is required.'),\n path: ['password'],\n })\n }\n if (hasPasswordIntent && !confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.confirmPasswordRequired', 'Please confirm the new password.'),\n path: ['confirmPassword'],\n })\n }\n if (password && confirmPassword && password !== confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.passwordMismatch', 'Passwords do not match.'),\n path: ['confirmPassword'],\n })\n }\n })\n }, [passwordPolicy, t])\n\n const handleSubmit = React.useCallback(async (values: ProfileFormValues) => {\n const nextEmail = values.email?.trim() ?? ''\n const currentPassword = values.currentPassword?.trim() ?? ''\n const password = values.password?.trim() ?? ''\n\n if (!password && nextEmail === email) {\n throw createCrudFormError(t('auth.profile.form.errors.noChanges', 'No changes to save.'))\n }\n\n const payload: { email: string; currentPassword?: string; password?: string } = { email: nextEmail }\n if (password) payload.password = password\n if (password) payload.currentPassword = currentPassword\n\n const result = await readApiResultOrThrow<ProfileUpdateResponse>(\n '/api/auth/profile',\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n { errorMessage: t('auth.profile.form.errors.save', 'Failed to update profile.') },\n )\n\n const resolvedEmail = typeof result?.email === 'string' ? result.email : nextEmail\n setEmail(resolvedEmail)\n setFormKey((prev) => prev + 1)\n flash(t('auth.profile.form.success', 'Profile updated.'), 'success')\n router.refresh()\n }, [email, router, t])\n\n if (loading) {\n return <LoadingMessage label={t('auth.profile.form.loading', 'Loading profile...')} />\n }\n\n if (error) {\n return <ErrorMessage label={error} />\n }\n\n return (\n <section className=\"space-y-6 rounded-lg border bg-background p-6 max-w-2xl\">\n <header className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('auth.changePassword.title', 'Change Password')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('auth.profile.subtitle', 'Change password')}\n </p>\n </div>\n <Button type=\"submit\" form={formId}>\n <Save className=\"size-4 mr-2\" />\n {t('auth.profile.form.save', 'Save changes')}\n </Button>\n </header>\n <CrudForm<ProfileFormValues>\n key={formKey}\n formId={formId}\n schema={schema}\n fields={fields}\n initialValues={{\n email,\n currentPassword: '',\n password: '',\n confirmPassword: '',\n }}\n submitLabel={t('auth.profile.form.save', 'Save changes')}\n onSubmit={handleSubmit}\n embedded\n hideFooterActions\n />\n </section>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { z } from 'zod'\nimport { Save } from 'lucide-react'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { buildPasswordSchema, formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/shared/lib/auth/passwordPolicy'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('auth').child({ component: 'change-password-page' })\n\ntype ProfileResponse = {\n email?: string | null\n name?: string | null\n}\n\ntype ProfileUpdateResponse = {\n ok?: boolean\n email?: string | null\n}\n\ntype ProfileFormValues = {\n email: string\n currentPassword?: string\n password?: string\n confirmPassword?: string\n}\n\nexport default function ProfileChangePasswordPage() {\n const t = useT()\n const router = useRouter()\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [email, setEmail] = React.useState('')\n const [formKey, setFormKey] = React.useState(0)\n const formId = React.useId()\n const passwordPolicy = React.useMemo(() => getPasswordPolicy(), [])\n const passwordRequirements = React.useMemo(\n () => formatPasswordRequirements(passwordPolicy, t),\n [passwordPolicy, t],\n )\n const passwordDescription = React.useMemo(() => (\n passwordRequirements\n ? t('auth.password.requirements.help', 'Password requirements: {requirements}', { requirements: passwordRequirements })\n : undefined\n ), [passwordRequirements, t])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setLoading(true)\n setError(null)\n try {\n const { ok, result } = await apiCall<ProfileResponse>('/api/auth/profile')\n if (!ok) throw new Error(t('auth.profile.form.errors.load', 'Failed to load profile.'))\n const resolvedEmail = typeof result?.email === 'string' ? result.email : ''\n if (!cancelled) setEmail(resolvedEmail)\n } catch (err) {\n logger.error('Failed to load auth profile', { err })\n if (!cancelled) setError(t('auth.profile.form.errors.load', 'Failed to load profile.'))\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [t])\n\n const fields = React.useMemo<CrudField[]>(() => [\n { id: 'email', label: t('auth.profile.form.email', 'Email'), type: 'text', required: true },\n {\n id: 'currentPassword',\n label: t('auth.profile.form.currentPassword', 'Current password'),\n type: 'password',\n },\n {\n id: 'password',\n label: t('auth.profile.form.password', 'New password'),\n type: 'password',\n description: passwordDescription,\n },\n {\n id: 'confirmPassword',\n label: t('auth.profile.form.confirmPassword', 'Confirm new password'),\n type: 'password',\n },\n ], [passwordDescription, t])\n\n const schema = React.useMemo(() => {\n const passwordSchema = buildPasswordSchema({\n policy: passwordPolicy,\n message: t('auth.profile.form.errors.passwordRequirements', 'Password must meet the requirements.'),\n })\n const optionalPasswordSchema = z.union([z.literal(''), passwordSchema]).optional()\n return z.object({\n email: z.string().trim().min(1, t('auth.profile.form.errors.emailRequired', 'Email is required.')),\n currentPassword: z.string().optional(),\n password: optionalPasswordSchema,\n confirmPassword: z.string().optional(),\n }).superRefine((values, ctx) => {\n const currentPassword = values.currentPassword?.trim() ?? ''\n const password = values.password?.trim() ?? ''\n const confirmPassword = values.confirmPassword?.trim() ?? ''\n const hasPasswordIntent = Boolean(currentPassword || password || confirmPassword)\n\n if (hasPasswordIntent && !currentPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.currentPasswordRequired', 'Current password is required.'),\n path: ['currentPassword'],\n })\n }\n if (hasPasswordIntent && !password) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.newPasswordRequired', 'New password is required.'),\n path: ['password'],\n })\n }\n if (hasPasswordIntent && !confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.confirmPasswordRequired', 'Please confirm the new password.'),\n path: ['confirmPassword'],\n })\n }\n if (password && confirmPassword && password !== confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.passwordMismatch', 'Passwords do not match.'),\n path: ['confirmPassword'],\n })\n }\n })\n }, [passwordPolicy, t])\n\n const handleSubmit = React.useCallback(async (values: ProfileFormValues) => {\n const nextEmail = values.email?.trim() ?? ''\n const currentPassword = values.currentPassword?.trim() ?? ''\n const password = values.password?.trim() ?? ''\n\n if (!password && nextEmail === email) {\n throw createCrudFormError(t('auth.profile.form.errors.noChanges', 'No changes to save.'))\n }\n\n const payload: { email: string; currentPassword?: string; password?: string } = { email: nextEmail }\n if (password) payload.password = password\n if (password) payload.currentPassword = currentPassword\n\n const result = await readApiResultOrThrow<ProfileUpdateResponse>(\n '/api/auth/profile',\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n { errorMessage: t('auth.profile.form.errors.save', 'Failed to update profile.') },\n )\n\n const resolvedEmail = typeof result?.email === 'string' ? result.email : nextEmail\n setEmail(resolvedEmail)\n setFormKey((prev) => prev + 1)\n flash(t('auth.profile.form.success', 'Profile updated.'), 'success')\n router.refresh()\n }, [email, router, t])\n\n if (loading) {\n return <LoadingMessage label={t('auth.profile.form.loading', 'Loading profile...')} />\n }\n\n if (error) {\n return <ErrorMessage label={error} />\n }\n\n return (\n <section className=\"space-y-6 rounded-lg border bg-background p-6 max-w-2xl\">\n <header className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('auth.changePassword.title', 'Change Password')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('auth.profile.subtitle', 'Change password')}\n </p>\n </div>\n <Button type=\"submit\" form={formId}>\n <Save className=\"size-4 mr-2\" />\n {t('auth.profile.form.save', 'Save changes')}\n </Button>\n </header>\n <CrudForm<ProfileFormValues>\n key={formKey}\n formId={formId}\n schema={schema}\n fields={fields}\n initialValues={{\n email,\n currentPassword: '',\n password: '',\n confirmPassword: '',\n }}\n submitLabel={t('auth.profile.form.save', 'Save changes')}\n onSubmit={handleSubmit}\n embedded\n hideFooterActions\n />\n </section>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const DATA_SYNC_IMPORT_QUEUE = "data-sync-import";
|
|
2
|
+
const DATA_SYNC_EXPORT_QUEUE = "data-sync-export";
|
|
3
|
+
const DATA_SYNC_RESUMABLE_QUEUES = [DATA_SYNC_IMPORT_QUEUE, DATA_SYNC_EXPORT_QUEUE];
|
|
4
|
+
const DATA_SYNC_QUEUE_ATTEMPTS = 3;
|
|
5
|
+
const DATA_SYNC_LOCK_DURATION_MS = 12e4;
|
|
6
|
+
const DATA_SYNC_MAX_STALLED_COUNT = 10;
|
|
7
|
+
export {
|
|
8
|
+
DATA_SYNC_EXPORT_QUEUE,
|
|
9
|
+
DATA_SYNC_IMPORT_QUEUE,
|
|
10
|
+
DATA_SYNC_LOCK_DURATION_MS,
|
|
11
|
+
DATA_SYNC_MAX_STALLED_COUNT,
|
|
12
|
+
DATA_SYNC_QUEUE_ATTEMPTS,
|
|
13
|
+
DATA_SYNC_RESUMABLE_QUEUES
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=queue-policy.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/data_sync/lib/queue-policy.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Queue names whose jobs resume an existing SyncRun instead of starting a fresh\n * one. The workers import these to declare their `metadata.queue`, so a rename\n * cannot silently drop the retry policy.\n */\nexport const DATA_SYNC_IMPORT_QUEUE = 'data-sync-import'\nexport const DATA_SYNC_EXPORT_QUEUE = 'data-sync-export'\n\nexport const DATA_SYNC_RESUMABLE_QUEUES: readonly string[] = [DATA_SYNC_IMPORT_QUEUE, DATA_SYNC_EXPORT_QUEUE]\n\nexport const DATA_SYNC_QUEUE_ATTEMPTS = 3\n\n/**\n * A backfill batch can hold the processor well past BullMQ's 30s default lock,\n * so these jobs stall on slow upstreams even when the worker is perfectly\n * healthy. `DATA_SYNC_LOCK_DURATION_MS` is the primary defence \u2014 it stops most\n * of those false stalls at the source rather than reacting to them.\n *\n * `DATA_SYNC_MAX_STALLED_COUNT` covers what is left. BullMQ fails a job once it\n * stalls more than this many times, and at the default of 1 a single stall\n * during a long backfill discards the run permanently. 10 buys enough\n * redeliveries to survive a rolling restart. Raising it is only safe because a\n * duplicate delivery can no longer corrupt the run: the ownership\n * compare-and-swap in `commitBatchProgress` lets exactly one worker advance a\n * run, and every other delivery aborts on its first commit. A job that poisons its\n * worker still dies \u2014 after 10 stalls, with the run left `running` and its\n * cursor unmoved, which is the state the admin runs list surfaces.\n */\nexport const DATA_SYNC_LOCK_DURATION_MS = 120_000\nexport const DATA_SYNC_MAX_STALLED_COUNT = 10\n"],
|
|
5
|
+
"mappings": "AAKO,MAAM,yBAAyB;AAC/B,MAAM,yBAAyB;AAE/B,MAAM,6BAAgD,CAAC,wBAAwB,sBAAsB;AAErG,MAAM,2BAA2B;AAkBjC,MAAM,6BAA6B;AACnC,MAAM,8BAA8B;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { createModuleQueue } from "@open-mercato/queue";
|
|
2
|
+
import {
|
|
3
|
+
DATA_SYNC_LOCK_DURATION_MS,
|
|
4
|
+
DATA_SYNC_MAX_STALLED_COUNT,
|
|
5
|
+
DATA_SYNC_QUEUE_ATTEMPTS,
|
|
6
|
+
DATA_SYNC_RESUMABLE_QUEUES
|
|
7
|
+
} from "./queue-policy.js";
|
|
2
8
|
const queues = /* @__PURE__ */ new Map();
|
|
9
|
+
const resumableQueueNames = new Set(DATA_SYNC_RESUMABLE_QUEUES);
|
|
3
10
|
function getSyncQueue(queueName) {
|
|
4
11
|
const existing = queues.get(queueName);
|
|
5
12
|
if (existing) return existing;
|
|
6
13
|
const concurrency = Math.max(1, Number.parseInt(process.env.DATA_SYNC_QUEUE_CONCURRENCY ?? "5", 10) || 5);
|
|
7
|
-
const created = createModuleQueue(
|
|
14
|
+
const created = createModuleQueue(
|
|
15
|
+
queueName,
|
|
16
|
+
resumableQueueNames.has(queueName) ? {
|
|
17
|
+
concurrency,
|
|
18
|
+
attempts: DATA_SYNC_QUEUE_ATTEMPTS,
|
|
19
|
+
lockDuration: DATA_SYNC_LOCK_DURATION_MS,
|
|
20
|
+
maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT
|
|
21
|
+
} : { concurrency }
|
|
22
|
+
);
|
|
8
23
|
queues.set(queueName, created);
|
|
9
24
|
return created;
|
|
10
25
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/data_sync/lib/queue.ts"],
|
|
4
|
-
"sourcesContent": ["import { createModuleQueue, type Queue } from '@open-mercato/queue'\n\nconst queues = new Map<string, Queue<Record<string, unknown>>>()\n\nexport function getSyncQueue(queueName: string): Queue<Record<string, unknown>> {\n const existing = queues.get(queueName)\n if (existing) return existing\n\n const concurrency = Math.max(1, Number.parseInt(process.env.DATA_SYNC_QUEUE_CONCURRENCY ?? '5', 10) || 5)\n const created = createModuleQueue<Record<string, unknown>>(queueName
|
|
5
|
-
"mappings": "AAAA,SAAS,yBAAqC;
|
|
4
|
+
"sourcesContent": ["import { createModuleQueue, type Queue } from '@open-mercato/queue'\nimport {\n DATA_SYNC_LOCK_DURATION_MS,\n DATA_SYNC_MAX_STALLED_COUNT,\n DATA_SYNC_QUEUE_ATTEMPTS,\n DATA_SYNC_RESUMABLE_QUEUES,\n} from './queue-policy'\n\nconst queues = new Map<string, Queue<Record<string, unknown>>>()\n\nconst resumableQueueNames = new Set<string>(DATA_SYNC_RESUMABLE_QUEUES)\n\nexport function getSyncQueue(queueName: string): Queue<Record<string, unknown>> {\n const existing = queues.get(queueName)\n if (existing) return existing\n\n const concurrency = Math.max(1, Number.parseInt(process.env.DATA_SYNC_QUEUE_CONCURRENCY ?? '5', 10) || 5)\n const created = createModuleQueue<Record<string, unknown>>(\n queueName,\n resumableQueueNames.has(queueName)\n ? {\n concurrency,\n attempts: DATA_SYNC_QUEUE_ATTEMPTS,\n lockDuration: DATA_SYNC_LOCK_DURATION_MS,\n maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT,\n }\n : { concurrency },\n )\n\n queues.set(queueName, created)\n return created\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,yBAAqC;AAC9C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,SAAS,oBAAI,IAA4C;AAE/D,MAAM,sBAAsB,IAAI,IAAY,0BAA0B;AAE/D,SAAS,aAAa,WAAmD;AAC9E,QAAM,WAAW,OAAO,IAAI,SAAS;AACrC,MAAI,SAAU,QAAO;AAErB,QAAM,cAAc,KAAK,IAAI,GAAG,OAAO,SAAS,QAAQ,IAAI,+BAA+B,KAAK,EAAE,KAAK,CAAC;AACxG,QAAM,UAAU;AAAA,IACd;AAAA,IACA,oBAAoB,IAAI,SAAS,IAC7B;AAAA,MACA;AAAA,MACA,UAAU;AAAA,MACV,cAAc;AAAA,MACd,iBAAiB;AAAA,IACnB,IACE,EAAE,YAAY;AAAA,EACpB;AAEA,SAAO,IAAI,WAAW,OAAO;AAC7B,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getSyncQueue } from "./queue.js";
|
|
2
|
+
import { DATA_SYNC_EXPORT_QUEUE, DATA_SYNC_IMPORT_QUEUE } from "./queue-policy.js";
|
|
2
3
|
async function startDataSyncRun(params) {
|
|
3
4
|
const { syncRunService, progressService, scope, input } = params;
|
|
4
5
|
const createProgressJob = input.createProgressJob !== false;
|
|
@@ -35,7 +36,7 @@ async function startDataSyncRun(params) {
|
|
|
35
36
|
tenantId: scope.tenantId
|
|
36
37
|
}
|
|
37
38
|
);
|
|
38
|
-
const queueName = input.direction === "import" ?
|
|
39
|
+
const queueName = input.direction === "import" ? DATA_SYNC_IMPORT_QUEUE : DATA_SYNC_EXPORT_QUEUE;
|
|
39
40
|
const queue = getSyncQueue(queueName);
|
|
40
41
|
await queue.enqueue({
|
|
41
42
|
runId: run.id,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/data_sync/lib/start-run.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ProgressService } from '../../progress/lib/progressService'\nimport type { SyncRunService } from './sync-run-service'\nimport { getSyncQueue } from './queue'\n\nexport type DataSyncStartScope = {\n organizationId: string\n tenantId: string\n userId?: string | null\n}\n\nexport type StartDataSyncRunInput = {\n integrationId: string\n entityType: string\n direction: 'import' | 'export'\n cursor?: string | null\n triggeredBy?: string | null\n batchSize?: number\n createProgressJob?: boolean\n progressJob?: {\n jobType?: string\n name?: string\n description?: string\n cancellable?: boolean\n meta?: Record<string, unknown>\n }\n}\n\nexport async function startDataSyncRun(params: {\n syncRunService: SyncRunService\n progressService: ProgressService\n scope: DataSyncStartScope\n input: StartDataSyncRunInput\n}) {\n const { syncRunService, progressService, scope, input } = params\n const createProgressJob = input.createProgressJob !== false\n\n const progressJob = createProgressJob\n ? await progressService.createJob(\n {\n jobType: input.progressJob?.jobType ?? `data_sync:${input.direction}`,\n name: input.progressJob?.name ?? `Data sync ${input.integrationId} \u2014 ${input.entityType}`,\n description: input.progressJob?.description ?? `${input.entityType} ${input.direction}`,\n cancellable: input.progressJob?.cancellable ?? true,\n meta: {\n integrationId: input.integrationId,\n entityType: input.entityType,\n direction: input.direction,\n ...(input.progressJob?.meta ?? {}),\n },\n },\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n )\n : null\n\n const run = await syncRunService.createRun(\n {\n integrationId: input.integrationId,\n entityType: input.entityType,\n direction: input.direction,\n cursor: input.cursor ?? null,\n triggeredBy: input.triggeredBy ?? scope.userId ?? null,\n progressJobId: progressJob?.id ?? null,\n },\n {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n )\n\n const queueName = input.direction === 'import' ?
|
|
5
|
-
"mappings": "AAEA,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["import type { ProgressService } from '../../progress/lib/progressService'\nimport type { SyncRunService } from './sync-run-service'\nimport { getSyncQueue } from './queue'\nimport { DATA_SYNC_EXPORT_QUEUE, DATA_SYNC_IMPORT_QUEUE } from './queue-policy'\n\nexport type DataSyncStartScope = {\n organizationId: string\n tenantId: string\n userId?: string | null\n}\n\nexport type StartDataSyncRunInput = {\n integrationId: string\n entityType: string\n direction: 'import' | 'export'\n cursor?: string | null\n triggeredBy?: string | null\n batchSize?: number\n createProgressJob?: boolean\n progressJob?: {\n jobType?: string\n name?: string\n description?: string\n cancellable?: boolean\n meta?: Record<string, unknown>\n }\n}\n\nexport async function startDataSyncRun(params: {\n syncRunService: SyncRunService\n progressService: ProgressService\n scope: DataSyncStartScope\n input: StartDataSyncRunInput\n}) {\n const { syncRunService, progressService, scope, input } = params\n const createProgressJob = input.createProgressJob !== false\n\n const progressJob = createProgressJob\n ? await progressService.createJob(\n {\n jobType: input.progressJob?.jobType ?? `data_sync:${input.direction}`,\n name: input.progressJob?.name ?? `Data sync ${input.integrationId} \u2014 ${input.entityType}`,\n description: input.progressJob?.description ?? `${input.entityType} ${input.direction}`,\n cancellable: input.progressJob?.cancellable ?? true,\n meta: {\n integrationId: input.integrationId,\n entityType: input.entityType,\n direction: input.direction,\n ...(input.progressJob?.meta ?? {}),\n },\n },\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n )\n : null\n\n const run = await syncRunService.createRun(\n {\n integrationId: input.integrationId,\n entityType: input.entityType,\n direction: input.direction,\n cursor: input.cursor ?? null,\n triggeredBy: input.triggeredBy ?? scope.userId ?? null,\n progressJobId: progressJob?.id ?? null,\n },\n {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n )\n\n const queueName = input.direction === 'import' ? DATA_SYNC_IMPORT_QUEUE : DATA_SYNC_EXPORT_QUEUE\n const queue = getSyncQueue(queueName)\n await queue.enqueue({\n runId: run.id,\n batchSize: input.batchSize ?? 100,\n scope: {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n userId: scope.userId ?? null,\n },\n })\n\n return {\n run,\n progressJob,\n }\n}\n"],
|
|
5
|
+
"mappings": "AAEA,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB,8BAA8B;AAyB/D,eAAsB,iBAAiB,QAKpC;AACD,QAAM,EAAE,gBAAgB,iBAAiB,OAAO,MAAM,IAAI;AAC1D,QAAM,oBAAoB,MAAM,sBAAsB;AAEtD,QAAM,cAAc,oBAChB,MAAM,gBAAgB;AAAA,IACtB;AAAA,MACE,SAAS,MAAM,aAAa,WAAW,aAAa,MAAM,SAAS;AAAA,MACnE,MAAM,MAAM,aAAa,QAAQ,aAAa,MAAM,aAAa,WAAM,MAAM,UAAU;AAAA,MACvF,aAAa,MAAM,aAAa,eAAe,GAAG,MAAM,UAAU,IAAI,MAAM,SAAS;AAAA,MACrF,aAAa,MAAM,aAAa,eAAe;AAAA,MAC/C,MAAM;AAAA,QACJ,eAAe,MAAM;AAAA,QACrB,YAAY,MAAM;AAAA,QAClB,WAAW,MAAM;AAAA,QACjB,GAAI,MAAM,aAAa,QAAQ,CAAC;AAAA,MAClC;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,MACtB,QAAQ,MAAM;AAAA,IAChB;AAAA,EACF,IACE;AAEJ,QAAM,MAAM,MAAM,eAAe;AAAA,IAC/B;AAAA,MACE,eAAe,MAAM;AAAA,MACrB,YAAY,MAAM;AAAA,MAClB,WAAW,MAAM;AAAA,MACjB,QAAQ,MAAM,UAAU;AAAA,MACxB,aAAa,MAAM,eAAe,MAAM,UAAU;AAAA,MAClD,eAAe,aAAa,MAAM;AAAA,IACpC;AAAA,IACA;AAAA,MACE,gBAAgB,MAAM;AAAA,MACtB,UAAU,MAAM;AAAA,IAClB;AAAA,EACF;AAEA,QAAM,YAAY,MAAM,cAAc,WAAW,yBAAyB;AAC1E,QAAM,QAAQ,aAAa,SAAS;AACpC,QAAM,MAAM,QAAQ;AAAA,IAClB,OAAO,IAAI;AAAA,IACX,WAAW,MAAM,aAAa;AAAA,IAC9B,OAAO;AAAA,MACL,gBAAgB,MAAM;AAAA,MACtB,UAAU,MAAM;AAAA,MAChB,QAAQ,MAAM,UAAU;AAAA,IAC1B;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,7 @@ import { getIntegration } from "@open-mercato/shared/modules/integrations/types"
|
|
|
2
2
|
import { refreshCoverageSnapshot } from "../../query_index/lib/coverage.js";
|
|
3
3
|
import { emitDataSyncEvent } from "../events.js";
|
|
4
4
|
import { getDataSyncAdapter } from "./adapter-registry.js";
|
|
5
|
+
import { SyncRunOwnershipConflictError } from "./sync-run-service.js";
|
|
5
6
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
6
7
|
const logger = createLogger("data_sync").child({ component: "sync-engine" });
|
|
7
8
|
function resolveProviderKey(integrationId) {
|
|
@@ -143,6 +144,14 @@ function createSyncEngine(deps) {
|
|
|
143
144
|
if (alreadyFinalizedWithSameStatus) {
|
|
144
145
|
return;
|
|
145
146
|
}
|
|
147
|
+
if (run.status !== status) {
|
|
148
|
+
logger.warn("Skipping finalization of a sync run another worker already finalized", {
|
|
149
|
+
runId,
|
|
150
|
+
requestedStatus: status,
|
|
151
|
+
actualStatus: run.status
|
|
152
|
+
});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
146
155
|
if (run.progressJobId) {
|
|
147
156
|
if (status === "completed") {
|
|
148
157
|
await progressService.completeJob(
|
|
@@ -307,6 +316,7 @@ function createSyncEngine(deps) {
|
|
|
307
316
|
if (!credentials) {
|
|
308
317
|
throw new Error(`Integration ${run.integrationId} is missing credentials`);
|
|
309
318
|
}
|
|
319
|
+
const resumed = run.status === "running";
|
|
310
320
|
const activeRun = await syncRunService.markStatus(run.id, "running", scope);
|
|
311
321
|
if (!activeRun || activeRun.status !== "running") {
|
|
312
322
|
return;
|
|
@@ -316,6 +326,7 @@ function createSyncEngine(deps) {
|
|
|
316
326
|
integrationId: run.integrationId,
|
|
317
327
|
entityType: run.entityType,
|
|
318
328
|
direction: run.direction,
|
|
329
|
+
resumed,
|
|
319
330
|
tenantId: scope.tenantId,
|
|
320
331
|
organizationId: scope.organizationId
|
|
321
332
|
});
|
|
@@ -349,6 +360,7 @@ function createSyncEngine(deps) {
|
|
|
349
360
|
const mapping = await resolveMapping(adapter, run.entityType, scope);
|
|
350
361
|
let processedCount = 0;
|
|
351
362
|
let totalCount = null;
|
|
363
|
+
let committedBatches = activeRun.batchesCompleted ?? 0;
|
|
352
364
|
try {
|
|
353
365
|
for await (const batch of adapter.streamImport({
|
|
354
366
|
entityType: run.entityType,
|
|
@@ -374,8 +386,10 @@ function createSyncEngine(deps) {
|
|
|
374
386
|
batchesCompleted: 1
|
|
375
387
|
},
|
|
376
388
|
batch.cursor,
|
|
377
|
-
scope
|
|
389
|
+
scope,
|
|
390
|
+
committedBatches
|
|
378
391
|
);
|
|
392
|
+
committedBatches += 1;
|
|
379
393
|
await updateProgress(run.progressJobId, processedCount, totalCount, scope);
|
|
380
394
|
await refreshCoverageSnapshots(batch.refreshCoverageEntityTypes, scope);
|
|
381
395
|
await logImportItemFailures(run.id, run.integrationId, batch.items, scope);
|
|
@@ -397,6 +411,13 @@ function createSyncEngine(deps) {
|
|
|
397
411
|
});
|
|
398
412
|
}
|
|
399
413
|
} catch (error) {
|
|
414
|
+
if (error instanceof SyncRunOwnershipConflictError) {
|
|
415
|
+
logger.warn("Yielding import run to a concurrent worker that already advanced it", {
|
|
416
|
+
runId: run.id,
|
|
417
|
+
expectedBatchesCompleted: error.expectedBatchesCompleted
|
|
418
|
+
});
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
400
421
|
const message = error instanceof Error ? error.message : "Sync import failed";
|
|
401
422
|
await integrationLogService.write(
|
|
402
423
|
{
|
|
@@ -438,6 +459,7 @@ function createSyncEngine(deps) {
|
|
|
438
459
|
if (!credentials) {
|
|
439
460
|
throw new Error(`Integration ${run.integrationId} is missing credentials`);
|
|
440
461
|
}
|
|
462
|
+
const resumed = run.status === "running";
|
|
441
463
|
const activeRun = await syncRunService.markStatus(run.id, "running", scope);
|
|
442
464
|
if (!activeRun || activeRun.status !== "running") {
|
|
443
465
|
return;
|
|
@@ -447,6 +469,7 @@ function createSyncEngine(deps) {
|
|
|
447
469
|
integrationId: run.integrationId,
|
|
448
470
|
entityType: run.entityType,
|
|
449
471
|
direction: run.direction,
|
|
472
|
+
resumed,
|
|
450
473
|
tenantId: scope.tenantId,
|
|
451
474
|
organizationId: scope.organizationId
|
|
452
475
|
});
|
|
@@ -479,6 +502,7 @@ function createSyncEngine(deps) {
|
|
|
479
502
|
}
|
|
480
503
|
const mapping = await resolveMapping(adapter, run.entityType, scope);
|
|
481
504
|
let processedCount = 0;
|
|
505
|
+
let committedBatches = activeRun.batchesCompleted ?? 0;
|
|
482
506
|
try {
|
|
483
507
|
for await (const batch of adapter.streamExport({
|
|
484
508
|
entityType: run.entityType,
|
|
@@ -505,8 +529,10 @@ function createSyncEngine(deps) {
|
|
|
505
529
|
batchesCompleted: 1
|
|
506
530
|
},
|
|
507
531
|
batch.cursor,
|
|
508
|
-
scope
|
|
532
|
+
scope,
|
|
533
|
+
committedBatches
|
|
509
534
|
);
|
|
535
|
+
committedBatches += 1;
|
|
510
536
|
await updateProgress(run.progressJobId, processedCount, null, scope);
|
|
511
537
|
await logExportItemFailures(run.id, run.integrationId, batch.results, scope);
|
|
512
538
|
await writeOperationalLog({
|
|
@@ -526,6 +552,13 @@ function createSyncEngine(deps) {
|
|
|
526
552
|
});
|
|
527
553
|
}
|
|
528
554
|
} catch (error) {
|
|
555
|
+
if (error instanceof SyncRunOwnershipConflictError) {
|
|
556
|
+
logger.warn("Yielding export run to a concurrent worker that already advanced it", {
|
|
557
|
+
runId: run.id,
|
|
558
|
+
expectedBatchesCompleted: error.expectedBatchesCompleted
|
|
559
|
+
});
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
529
562
|
const message = error instanceof Error ? error.message : "Sync export failed";
|
|
530
563
|
await integrationLogService.write(
|
|
531
564
|
{
|