@open-mercato/core 0.4.5-develop-8f98466993 → 0.4.5-develop-0b66ecfdd4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js +46 -44
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js.map +2 -2
- package/dist/modules/translations/api/context.js +10 -1
- package/dist/modules/translations/api/context.js.map +2 -2
- package/dist/modules/translations/commands/index.js +2 -0
- package/dist/modules/translations/commands/index.js.map +7 -0
- package/dist/modules/translations/commands/translations.js +160 -0
- package/dist/modules/translations/commands/translations.js.map +7 -0
- package/dist/modules/translations/index.js +1 -0
- package/dist/modules/translations/index.js.map +2 -2
- package/package.json +2 -2
- package/src/modules/translations/api/[entityType]/[entityId]/route.ts +65 -60
- package/src/modules/translations/api/context.ts +12 -0
- package/src/modules/translations/commands/index.ts +1 -0
- package/src/modules/translations/commands/translations.ts +253 -0
- package/src/modules/translations/index.ts +1 -0
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { resolveTranslationsRouteContext } from "@open-mercato/core/modules/translations/api/context";
|
|
4
4
|
import { translationBodySchema, entityTypeParamSchema, entityIdParamSchema } from "@open-mercato/core/modules/translations/data/validators";
|
|
5
5
|
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
6
|
+
import { serializeOperationMetadata } from "@open-mercato/shared/lib/commands/operationMetadata";
|
|
6
7
|
const paramsSchema = z.object({
|
|
7
8
|
entityType: entityTypeParamSchema,
|
|
8
9
|
entityId: entityIdParamSchema
|
|
@@ -53,49 +54,40 @@ async function PUT(req, ctx) {
|
|
|
53
54
|
});
|
|
54
55
|
const rawBody = await req.json().catch(() => ({}));
|
|
55
56
|
const translations = translationBodySchema.parse(rawBody);
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}).andWhereRaw("tenant_id is not distinct from ?", [context.tenantId]).andWhereRaw("organization_id is not distinct from ?", [context.organizationId]).first();
|
|
60
|
-
const now = context.knex.fn.now();
|
|
61
|
-
if (existing) {
|
|
62
|
-
await context.knex("entity_translations").where({ id: existing.id }).update({
|
|
63
|
-
translations,
|
|
64
|
-
updated_at: now
|
|
65
|
-
});
|
|
66
|
-
} else {
|
|
67
|
-
await context.knex("entity_translations").insert({
|
|
68
|
-
entity_type: entityType,
|
|
69
|
-
entity_id: entityId,
|
|
70
|
-
organization_id: context.organizationId,
|
|
71
|
-
tenant_id: context.tenantId,
|
|
72
|
-
translations,
|
|
73
|
-
created_at: now,
|
|
74
|
-
updated_at: now
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
try {
|
|
78
|
-
const bus = context.container.resolve("eventBus");
|
|
79
|
-
await bus.emitEvent("translations.translation.updated", {
|
|
57
|
+
const commandBus = context.container.resolve("commandBus");
|
|
58
|
+
const { result, logEntry } = await commandBus.execute("translations.translation.save", {
|
|
59
|
+
input: {
|
|
80
60
|
entityType,
|
|
81
61
|
entityId,
|
|
62
|
+
translations,
|
|
82
63
|
organizationId: context.organizationId,
|
|
83
64
|
tenantId: context.tenantId
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
const
|
|
89
|
-
entity_type: entityType,
|
|
90
|
-
entity_id: entityId
|
|
91
|
-
}).andWhereRaw("tenant_id is not distinct from ?", [context.tenantId]).andWhereRaw("organization_id is not distinct from ?", [context.organizationId]).first();
|
|
92
|
-
return NextResponse.json({
|
|
65
|
+
},
|
|
66
|
+
ctx: context.commandCtx
|
|
67
|
+
});
|
|
68
|
+
const row = await context.knex("entity_translations").where({ id: result.rowId }).first();
|
|
69
|
+
const response = NextResponse.json({
|
|
93
70
|
entityType: row.entity_type,
|
|
94
71
|
entityId: row.entity_id,
|
|
95
72
|
translations: row.translations,
|
|
96
73
|
createdAt: row.created_at,
|
|
97
74
|
updatedAt: row.updated_at
|
|
98
75
|
});
|
|
76
|
+
if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
|
|
77
|
+
response.headers.set(
|
|
78
|
+
"x-om-operation",
|
|
79
|
+
serializeOperationMetadata({
|
|
80
|
+
id: logEntry.id,
|
|
81
|
+
undoToken: logEntry.undoToken,
|
|
82
|
+
commandId: logEntry.commandId,
|
|
83
|
+
actionLabel: logEntry.actionLabel ?? null,
|
|
84
|
+
resourceKind: logEntry.resourceKind ?? "translations.translation",
|
|
85
|
+
resourceId: logEntry.resourceId ?? result.rowId,
|
|
86
|
+
executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : typeof logEntry.createdAt === "string" ? logEntry.createdAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return response;
|
|
99
91
|
} catch (err) {
|
|
100
92
|
if (err instanceof CrudHttpError) {
|
|
101
93
|
return NextResponse.json(err.body, { status: err.status });
|
|
@@ -114,22 +106,32 @@ async function DELETE(req, ctx) {
|
|
|
114
106
|
entityType: ctx.params?.entityType,
|
|
115
107
|
entityId: ctx.params?.entityId
|
|
116
108
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}).andWhereRaw("tenant_id is not distinct from ?", [context.tenantId]).andWhereRaw("organization_id is not distinct from ?", [context.organizationId]).del();
|
|
121
|
-
try {
|
|
122
|
-
const bus = context.container.resolve("eventBus");
|
|
123
|
-
await bus.emitEvent("translations.translation.deleted", {
|
|
109
|
+
const commandBus = context.container.resolve("commandBus");
|
|
110
|
+
const { logEntry } = await commandBus.execute("translations.translation.delete", {
|
|
111
|
+
input: {
|
|
124
112
|
entityType,
|
|
125
113
|
entityId,
|
|
126
114
|
organizationId: context.organizationId,
|
|
127
115
|
tenantId: context.tenantId
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
116
|
+
},
|
|
117
|
+
ctx: context.commandCtx
|
|
118
|
+
});
|
|
119
|
+
const response = new NextResponse(null, { status: 204 });
|
|
120
|
+
if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
|
|
121
|
+
response.headers.set(
|
|
122
|
+
"x-om-operation",
|
|
123
|
+
serializeOperationMetadata({
|
|
124
|
+
id: logEntry.id,
|
|
125
|
+
undoToken: logEntry.undoToken,
|
|
126
|
+
commandId: logEntry.commandId,
|
|
127
|
+
actionLabel: logEntry.actionLabel ?? null,
|
|
128
|
+
resourceKind: logEntry.resourceKind ?? "translations.translation",
|
|
129
|
+
resourceId: logEntry.resourceId ?? null,
|
|
130
|
+
executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : typeof logEntry.createdAt === "string" ? logEntry.createdAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
131
|
+
})
|
|
132
|
+
);
|
|
131
133
|
}
|
|
132
|
-
return
|
|
134
|
+
return response;
|
|
133
135
|
} catch (err) {
|
|
134
136
|
if (err instanceof CrudHttpError) {
|
|
135
137
|
return NextResponse.json(err.body, { status: err.status });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/translations/api/%5BentityType%5D/%5BentityId%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { resolveTranslationsRouteContext } from '@open-mercato/core/modules/translations/api/context'\nimport { translationBodySchema, entityTypeParamSchema, entityIdParamSchema } from '@open-mercato/core/modules/translations/data/validators'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nconst paramsSchema = z.object({\n entityType: entityTypeParamSchema,\n entityId: entityIdParamSchema,\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['translations.view'] },\n PUT: { requireAuth: true, requireFeatures: ['translations.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['translations.manage'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { entityType?: string; entityId?: string } }) {\n try {\n const context = await resolveTranslationsRouteContext(req)\n const { entityType, entityId } = paramsSchema.parse({\n entityType: ctx.params?.entityType,\n entityId: ctx.params?.entityId,\n })\n\n const row = await context.knex('entity_translations')\n .where({\n entity_type: entityType,\n entity_id: entityId,\n })\n .andWhereRaw('tenant_id is not distinct from ?', [context.tenantId])\n .andWhereRaw('organization_id is not distinct from ?', [context.organizationId])\n .first()\n\n if (!row) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n\n return NextResponse.json({\n entityType: row.entity_type,\n entityId: row.entity_id,\n translations: row.translations,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n })\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid parameters', details: err.issues }, { status: 400 })\n }\n console.error('[translations/:entityType/:entityId.GET] Unexpected error', err)\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nexport async function PUT(req: Request, ctx: { params?: { entityType?: string; entityId?: string } }) {\n try {\n const context = await resolveTranslationsRouteContext(req)\n const { entityType, entityId } = paramsSchema.parse({\n entityType: ctx.params?.entityType,\n entityId: ctx.params?.entityId,\n })\n\n const rawBody = await req.json().catch(() => ({}))\n const translations = translationBodySchema.parse(rawBody)\n\n const
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,uCAAuC;AAChD,SAAS,uBAAuB,uBAAuB,2BAA2B;AAClF,SAAS,qBAAqB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { resolveTranslationsRouteContext } from '@open-mercato/core/modules/translations/api/context'\nimport { translationBodySchema, entityTypeParamSchema, entityIdParamSchema } from '@open-mercato/core/modules/translations/data/validators'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nconst paramsSchema = z.object({\n entityType: entityTypeParamSchema,\n entityId: entityIdParamSchema,\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['translations.view'] },\n PUT: { requireAuth: true, requireFeatures: ['translations.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['translations.manage'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { entityType?: string; entityId?: string } }) {\n try {\n const context = await resolveTranslationsRouteContext(req)\n const { entityType, entityId } = paramsSchema.parse({\n entityType: ctx.params?.entityType,\n entityId: ctx.params?.entityId,\n })\n\n const row = await context.knex('entity_translations')\n .where({\n entity_type: entityType,\n entity_id: entityId,\n })\n .andWhereRaw('tenant_id is not distinct from ?', [context.tenantId])\n .andWhereRaw('organization_id is not distinct from ?', [context.organizationId])\n .first()\n\n if (!row) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n\n return NextResponse.json({\n entityType: row.entity_type,\n entityId: row.entity_id,\n translations: row.translations,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n })\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid parameters', details: err.issues }, { status: 400 })\n }\n console.error('[translations/:entityType/:entityId.GET] Unexpected error', err)\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nexport async function PUT(req: Request, ctx: { params?: { entityType?: string; entityId?: string } }) {\n try {\n const context = await resolveTranslationsRouteContext(req)\n const { entityType, entityId } = paramsSchema.parse({\n entityType: ctx.params?.entityType,\n entityId: ctx.params?.entityId,\n })\n\n const rawBody = await req.json().catch(() => ({}))\n const translations = translationBodySchema.parse(rawBody)\n\n const commandBus = context.container.resolve('commandBus') as CommandBus\n const { result, logEntry } = await commandBus.execute<\n { entityType: string; entityId: string; translations: typeof translations; organizationId: string | null; tenantId: string },\n { rowId: string }\n >('translations.translation.save', {\n input: {\n entityType,\n entityId,\n translations,\n organizationId: context.organizationId,\n tenantId: context.tenantId,\n },\n ctx: context.commandCtx,\n })\n\n const row = await context.knex('entity_translations')\n .where({ id: result.rowId })\n .first()\n\n const response = NextResponse.json({\n entityType: row.entity_type,\n entityId: row.entity_id,\n translations: row.translations,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n })\n\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'translations.translation',\n resourceId: logEntry.resourceId ?? result.rowId,\n executedAt: logEntry.createdAt instanceof Date\n ? logEntry.createdAt.toISOString()\n : typeof logEntry.createdAt === 'string'\n ? logEntry.createdAt\n : new Date().toISOString(),\n }),\n )\n }\n\n return response\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Validation failed', details: err.issues }, { status: 400 })\n }\n console.error('[translations/:entityType/:entityId.PUT] Unexpected error', err)\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nexport async function DELETE(req: Request, ctx: { params?: { entityType?: string; entityId?: string } }) {\n try {\n const context = await resolveTranslationsRouteContext(req)\n const { entityType, entityId } = paramsSchema.parse({\n entityType: ctx.params?.entityType,\n entityId: ctx.params?.entityId,\n })\n\n const commandBus = context.container.resolve('commandBus') as CommandBus\n const { logEntry } = await commandBus.execute<\n { entityType: string; entityId: string; organizationId: string | null; tenantId: string },\n { deleted: boolean }\n >('translations.translation.delete', {\n input: {\n entityType,\n entityId,\n organizationId: context.organizationId,\n tenantId: context.tenantId,\n },\n ctx: context.commandCtx,\n })\n\n const response = new NextResponse(null, { status: 204 })\n\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'translations.translation',\n resourceId: logEntry.resourceId ?? null,\n executedAt: logEntry.createdAt instanceof Date\n ? logEntry.createdAt.toISOString()\n : typeof logEntry.createdAt === 'string'\n ? logEntry.createdAt\n : new Date().toISOString(),\n }),\n )\n }\n\n return response\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid parameters', details: err.issues }, { status: 400 })\n }\n console.error('[translations/:entityType/:entityId.DELETE] Unexpected error', err)\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nconst translationsTag = 'Translations'\n\nconst getDoc: OpenApiMethodDoc = {\n summary: 'Get entity translations',\n description: 'Returns the full translation record for a single entity.',\n tags: [translationsTag],\n responses: [\n { status: 200, description: 'Translation record found.' },\n ],\n errors: [\n { status: 401, description: 'Authentication required' },\n { status: 404, description: 'No translations found for this entity' },\n ],\n}\n\nconst putDoc: OpenApiMethodDoc = {\n summary: 'Create or update entity translations',\n description: 'Full replacement of translations JSONB for an entity.',\n tags: [translationsTag],\n responses: [\n { status: 200, description: 'Translations saved.' },\n ],\n errors: [\n { status: 400, description: 'Validation failed' },\n { status: 401, description: 'Authentication required' },\n ],\n}\n\nconst deleteDoc: OpenApiMethodDoc = {\n summary: 'Delete entity translations',\n description: 'Removes all translations for an entity.',\n tags: [translationsTag],\n responses: [\n { status: 204, description: 'Translations deleted.' },\n ],\n errors: [\n { status: 401, description: 'Authentication required' },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: translationsTag,\n summary: 'Entity translation resource',\n pathParams: paramsSchema,\n methods: {\n GET: getDoc,\n PUT: putDoc,\n DELETE: deleteDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -13,13 +13,22 @@ async function resolveTranslationsRouteContext(req) {
|
|
|
13
13
|
const knex = em.getConnection().getKnex();
|
|
14
14
|
const tenantId = scope?.tenantId ?? auth.tenantId;
|
|
15
15
|
const organizationId = scope?.selectedId ?? auth.orgId ?? null;
|
|
16
|
+
const commandCtx = {
|
|
17
|
+
container,
|
|
18
|
+
auth,
|
|
19
|
+
organizationScope: scope,
|
|
20
|
+
selectedOrganizationId: organizationId,
|
|
21
|
+
organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),
|
|
22
|
+
request: req
|
|
23
|
+
};
|
|
16
24
|
return {
|
|
17
25
|
container,
|
|
18
26
|
auth,
|
|
19
27
|
em,
|
|
20
28
|
knex,
|
|
21
29
|
organizationId,
|
|
22
|
-
tenantId
|
|
30
|
+
tenantId,
|
|
31
|
+
commandCtx
|
|
23
32
|
};
|
|
24
33
|
}
|
|
25
34
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/translations/api/context.ts"],
|
|
4
|
-
"sourcesContent": ["import { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport type { Knex } from 'knex'\n\nexport type TranslationsRouteContext = {\n container: AwilixContainer\n auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n em: EntityManager\n knex: Knex\n organizationId: string | null\n tenantId: string\n}\n\nexport async function resolveTranslationsRouteContext(req: Request): Promise<TranslationsRouteContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: 'Unauthorized' })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const em = container.resolve('em') as EntityManager\n const knex = (em as unknown as { getConnection(): { getKnex(): Knex } }).getConnection().getKnex()\n const tenantId: string = scope?.tenantId ?? auth.tenantId\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n\n return {\n container,\n auth,\n em,\n knex,\n organizationId,\n tenantId,\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AACnD,SAAS,qBAAqB;
|
|
4
|
+
"sourcesContent": ["import { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport type { Knex } from 'knex'\n\nexport type TranslationsRouteContext = {\n container: AwilixContainer\n auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n em: EntityManager\n knex: Knex\n organizationId: string | null\n tenantId: string\n commandCtx: CommandRuntimeContext\n}\n\nexport async function resolveTranslationsRouteContext(req: Request): Promise<TranslationsRouteContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: 'Unauthorized' })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const em = container.resolve('em') as EntityManager\n const knex = (em as unknown as { getConnection(): { getKnex(): Knex } }).getConnection().getKnex()\n const tenantId: string = scope?.tenantId ?? auth.tenantId\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n\n const commandCtx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n return {\n container,\n auth,\n em,\n knex,\n organizationId,\n tenantId,\n commandCtx,\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AACnD,SAAS,qBAAqB;AAgB9B,eAAsB,gCAAgC,KAAiD;AACrG,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,QAAQ,CAAC,KAAK,UAAU;AAC3B,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,eAAe,CAAC;AAAA,EACxD;AAEA,QAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AACxF,QAAM,KAAK,UAAU,QAAQ,IAAI;AACjC,QAAM,OAAQ,GAA2D,cAAc,EAAE,QAAQ;AACjG,QAAM,WAAmB,OAAO,YAAY,KAAK;AACjD,QAAM,iBAAiB,OAAO,cAAc,KAAK,SAAS;AAE1D,QAAM,aAAoC;AAAA,IACxC;AAAA,IACA;AAAA,IACA,mBAAmB;AAAA,IACnB,wBAAwB;AAAA,IACxB,iBAAiB,OAAO,cAAc,KAAK,QAAQ,CAAC,KAAK,KAAK,IAAI;AAAA,IAClE,SAAS;AAAA,EACX;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { registerCommand } from "@open-mercato/shared/lib/commands";
|
|
2
|
+
import { ensureTenantScope } from "@open-mercato/shared/lib/commands/scope";
|
|
3
|
+
import { extractUndoPayload } from "@open-mercato/shared/lib/commands/undo";
|
|
4
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
5
|
+
import { emitTranslationsEvent } from "../events.js";
|
|
6
|
+
function resolveKnex(ctx) {
|
|
7
|
+
const em = ctx.container.resolve("em");
|
|
8
|
+
return em.getConnection().getKnex();
|
|
9
|
+
}
|
|
10
|
+
async function loadTranslationSnapshot(knex, entityType, entityId, tenantId, organizationId) {
|
|
11
|
+
const row = await knex("entity_translations").where({ entity_type: entityType, entity_id: entityId }).andWhereRaw("tenant_id is not distinct from ?", [tenantId]).andWhereRaw("organization_id is not distinct from ?", [organizationId]).first();
|
|
12
|
+
if (!row) return null;
|
|
13
|
+
return {
|
|
14
|
+
id: row.id,
|
|
15
|
+
entityType: row.entity_type,
|
|
16
|
+
entityId: row.entity_id,
|
|
17
|
+
translations: row.translations ?? null,
|
|
18
|
+
organizationId: row.organization_id ?? null,
|
|
19
|
+
tenantId: row.tenant_id
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const saveTranslationCommand = {
|
|
23
|
+
id: "translations.translation.save",
|
|
24
|
+
async prepare(input, ctx) {
|
|
25
|
+
ensureTenantScope(ctx, input.tenantId);
|
|
26
|
+
const knex = resolveKnex(ctx);
|
|
27
|
+
const snapshot = await loadTranslationSnapshot(knex, input.entityType, input.entityId, input.tenantId, input.organizationId);
|
|
28
|
+
return { before: snapshot };
|
|
29
|
+
},
|
|
30
|
+
async execute(input, ctx) {
|
|
31
|
+
const knex = resolveKnex(ctx);
|
|
32
|
+
const existing = await knex("entity_translations").where({ entity_type: input.entityType, entity_id: input.entityId }).andWhereRaw("tenant_id is not distinct from ?", [input.tenantId]).andWhereRaw("organization_id is not distinct from ?", [input.organizationId]).first();
|
|
33
|
+
const now = knex.fn.now();
|
|
34
|
+
if (existing) {
|
|
35
|
+
await knex("entity_translations").where({ id: existing.id }).update({ translations: input.translations, updated_at: now });
|
|
36
|
+
} else {
|
|
37
|
+
await knex("entity_translations").insert({
|
|
38
|
+
entity_type: input.entityType,
|
|
39
|
+
entity_id: input.entityId,
|
|
40
|
+
organization_id: input.organizationId,
|
|
41
|
+
tenant_id: input.tenantId,
|
|
42
|
+
translations: input.translations,
|
|
43
|
+
created_at: now,
|
|
44
|
+
updated_at: now
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
await emitTranslationsEvent("translations.translation.updated", {
|
|
48
|
+
entityType: input.entityType,
|
|
49
|
+
entityId: input.entityId,
|
|
50
|
+
organizationId: input.organizationId,
|
|
51
|
+
tenantId: input.tenantId
|
|
52
|
+
}, { persistent: true }).catch(() => void 0);
|
|
53
|
+
const saved = await knex("entity_translations").where({ entity_type: input.entityType, entity_id: input.entityId }).andWhereRaw("tenant_id is not distinct from ?", [input.tenantId]).andWhereRaw("organization_id is not distinct from ?", [input.organizationId]).first();
|
|
54
|
+
return { rowId: saved.id };
|
|
55
|
+
},
|
|
56
|
+
async captureAfter(input, _result, ctx) {
|
|
57
|
+
const knex = resolveKnex(ctx);
|
|
58
|
+
return await loadTranslationSnapshot(knex, input.entityType, input.entityId, input.tenantId, input.organizationId);
|
|
59
|
+
},
|
|
60
|
+
async buildLog({ snapshots, result }) {
|
|
61
|
+
const { translate } = await resolveTranslations();
|
|
62
|
+
const before = snapshots.before;
|
|
63
|
+
const after = snapshots.after;
|
|
64
|
+
return {
|
|
65
|
+
actionLabel: translate("translations.audit.save", "Save translation"),
|
|
66
|
+
resourceKind: "translations.translation",
|
|
67
|
+
resourceId: result.rowId,
|
|
68
|
+
tenantId: after?.tenantId ?? before?.tenantId ?? null,
|
|
69
|
+
organizationId: after?.organizationId ?? before?.organizationId ?? null,
|
|
70
|
+
snapshotBefore: before ?? null,
|
|
71
|
+
snapshotAfter: after ?? null,
|
|
72
|
+
payload: {
|
|
73
|
+
undo: { before: before ?? null, after: after ?? null }
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
async undo({ logEntry, ctx }) {
|
|
78
|
+
const payload = extractUndoPayload(logEntry);
|
|
79
|
+
const before = payload?.before ?? null;
|
|
80
|
+
const knex = resolveKnex(ctx);
|
|
81
|
+
if (!before || !before.translations) {
|
|
82
|
+
const resourceId = logEntry?.resourceId;
|
|
83
|
+
if (resourceId) {
|
|
84
|
+
await knex("entity_translations").where({ id: resourceId }).del();
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
const existing = await knex("entity_translations").where({ entity_type: before.entityType, entity_id: before.entityId }).andWhereRaw("tenant_id is not distinct from ?", [before.tenantId]).andWhereRaw("organization_id is not distinct from ?", [before.organizationId]).first();
|
|
88
|
+
if (existing) {
|
|
89
|
+
await knex("entity_translations").where({ id: existing.id }).update({ translations: before.translations, updated_at: knex.fn.now() });
|
|
90
|
+
} else {
|
|
91
|
+
await knex("entity_translations").insert({
|
|
92
|
+
entity_type: before.entityType,
|
|
93
|
+
entity_id: before.entityId,
|
|
94
|
+
organization_id: before.organizationId,
|
|
95
|
+
tenant_id: before.tenantId,
|
|
96
|
+
translations: before.translations,
|
|
97
|
+
created_at: knex.fn.now(),
|
|
98
|
+
updated_at: knex.fn.now()
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const deleteTranslationCommand = {
|
|
105
|
+
id: "translations.translation.delete",
|
|
106
|
+
async prepare(input, ctx) {
|
|
107
|
+
ensureTenantScope(ctx, input.tenantId);
|
|
108
|
+
const knex = resolveKnex(ctx);
|
|
109
|
+
const snapshot = await loadTranslationSnapshot(knex, input.entityType, input.entityId, input.tenantId, input.organizationId);
|
|
110
|
+
return { before: snapshot };
|
|
111
|
+
},
|
|
112
|
+
async execute(input, ctx) {
|
|
113
|
+
const knex = resolveKnex(ctx);
|
|
114
|
+
const count = await knex("entity_translations").where({ entity_type: input.entityType, entity_id: input.entityId }).andWhereRaw("tenant_id is not distinct from ?", [input.tenantId]).andWhereRaw("organization_id is not distinct from ?", [input.organizationId]).del();
|
|
115
|
+
await emitTranslationsEvent("translations.translation.deleted", {
|
|
116
|
+
entityType: input.entityType,
|
|
117
|
+
entityId: input.entityId,
|
|
118
|
+
organizationId: input.organizationId,
|
|
119
|
+
tenantId: input.tenantId
|
|
120
|
+
}, { persistent: true }).catch(() => void 0);
|
|
121
|
+
return { deleted: count > 0 };
|
|
122
|
+
},
|
|
123
|
+
async buildLog({ snapshots }) {
|
|
124
|
+
const before = snapshots.before;
|
|
125
|
+
if (!before) return null;
|
|
126
|
+
const { translate } = await resolveTranslations();
|
|
127
|
+
return {
|
|
128
|
+
actionLabel: translate("translations.audit.delete", "Delete translation"),
|
|
129
|
+
resourceKind: "translations.translation",
|
|
130
|
+
resourceId: before.id ?? void 0,
|
|
131
|
+
tenantId: before.tenantId,
|
|
132
|
+
organizationId: before.organizationId,
|
|
133
|
+
snapshotBefore: before,
|
|
134
|
+
payload: {
|
|
135
|
+
undo: { before }
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
async undo({ logEntry, ctx }) {
|
|
140
|
+
const payload = extractUndoPayload(logEntry);
|
|
141
|
+
const before = payload?.before;
|
|
142
|
+
if (!before || !before.translations) return;
|
|
143
|
+
const knex = resolveKnex(ctx);
|
|
144
|
+
const existing = await knex("entity_translations").where({ entity_type: before.entityType, entity_id: before.entityId }).andWhereRaw("tenant_id is not distinct from ?", [before.tenantId]).andWhereRaw("organization_id is not distinct from ?", [before.organizationId]).first();
|
|
145
|
+
if (!existing) {
|
|
146
|
+
await knex("entity_translations").insert({
|
|
147
|
+
entity_type: before.entityType,
|
|
148
|
+
entity_id: before.entityId,
|
|
149
|
+
organization_id: before.organizationId,
|
|
150
|
+
tenant_id: before.tenantId,
|
|
151
|
+
translations: before.translations,
|
|
152
|
+
created_at: knex.fn.now(),
|
|
153
|
+
updated_at: knex.fn.now()
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
registerCommand(saveTranslationCommand);
|
|
159
|
+
registerCommand(deleteTranslationCommand);
|
|
160
|
+
//# sourceMappingURL=translations.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/translations/commands/translations.ts"],
|
|
4
|
+
"sourcesContent": ["import { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'\nimport { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { Knex } from 'knex'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { emitTranslationsEvent } from '../events'\n\ntype TranslationSnapshot = {\n id: string | null\n entityType: string\n entityId: string\n translations: Record<string, Record<string, string | null>> | null\n organizationId: string | null\n tenantId: string\n}\n\ntype TranslationUndoPayload = {\n before?: TranslationSnapshot | null\n after?: TranslationSnapshot | null\n}\n\ntype SaveInput = {\n entityType: string\n entityId: string\n translations: Record<string, Record<string, string | null>>\n organizationId: string | null\n tenantId: string\n}\n\ntype DeleteInput = {\n entityType: string\n entityId: string\n organizationId: string | null\n tenantId: string\n}\n\nfunction resolveKnex(ctx: CommandRuntimeContext): Knex {\n const em = ctx.container.resolve('em') as EntityManager\n return (em as unknown as { getConnection(): { getKnex(): Knex } }).getConnection().getKnex()\n}\n\nasync function loadTranslationSnapshot(\n knex: Knex,\n entityType: string,\n entityId: string,\n tenantId: string,\n organizationId: string | null,\n): Promise<TranslationSnapshot | null> {\n const row = await knex('entity_translations')\n .where({ entity_type: entityType, entity_id: entityId })\n .andWhereRaw('tenant_id is not distinct from ?', [tenantId])\n .andWhereRaw('organization_id is not distinct from ?', [organizationId])\n .first()\n\n if (!row) return null\n return {\n id: row.id,\n entityType: row.entity_type,\n entityId: row.entity_id,\n translations: row.translations ?? null,\n organizationId: row.organization_id ?? null,\n tenantId: row.tenant_id,\n }\n}\n\nconst saveTranslationCommand: CommandHandler<SaveInput, { rowId: string }> = {\n id: 'translations.translation.save',\n\n async prepare(input, ctx) {\n ensureTenantScope(ctx, input.tenantId)\n const knex = resolveKnex(ctx)\n const snapshot = await loadTranslationSnapshot(knex, input.entityType, input.entityId, input.tenantId, input.organizationId)\n return { before: snapshot }\n },\n\n async execute(input, ctx) {\n const knex = resolveKnex(ctx)\n const existing = await knex('entity_translations')\n .where({ entity_type: input.entityType, entity_id: input.entityId })\n .andWhereRaw('tenant_id is not distinct from ?', [input.tenantId])\n .andWhereRaw('organization_id is not distinct from ?', [input.organizationId])\n .first()\n\n const now = knex.fn.now()\n\n if (existing) {\n await knex('entity_translations')\n .where({ id: existing.id })\n .update({ translations: input.translations, updated_at: now })\n } else {\n await knex('entity_translations').insert({\n entity_type: input.entityType,\n entity_id: input.entityId,\n organization_id: input.organizationId,\n tenant_id: input.tenantId,\n translations: input.translations,\n created_at: now,\n updated_at: now,\n })\n }\n\n await emitTranslationsEvent('translations.translation.updated', {\n entityType: input.entityType,\n entityId: input.entityId,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n }, { persistent: true }).catch(() => undefined)\n\n const saved = await knex('entity_translations')\n .where({ entity_type: input.entityType, entity_id: input.entityId })\n .andWhereRaw('tenant_id is not distinct from ?', [input.tenantId])\n .andWhereRaw('organization_id is not distinct from ?', [input.organizationId])\n .first()\n\n return { rowId: saved.id }\n },\n\n async captureAfter(input, _result, ctx) {\n const knex = resolveKnex(ctx)\n return await loadTranslationSnapshot(knex, input.entityType, input.entityId, input.tenantId, input.organizationId)\n },\n\n async buildLog({ snapshots, result }) {\n const { translate } = await resolveTranslations()\n const before = snapshots.before as TranslationSnapshot | null | undefined\n const after = snapshots.after as TranslationSnapshot | null | undefined\n return {\n actionLabel: translate('translations.audit.save', 'Save translation'),\n resourceKind: 'translations.translation',\n resourceId: result.rowId,\n tenantId: after?.tenantId ?? before?.tenantId ?? null,\n organizationId: after?.organizationId ?? before?.organizationId ?? null,\n snapshotBefore: before ?? null,\n snapshotAfter: after ?? null,\n payload: {\n undo: { before: before ?? null, after: after ?? null } satisfies TranslationUndoPayload,\n },\n }\n },\n\n async undo({ logEntry, ctx }) {\n const payload = extractUndoPayload<TranslationUndoPayload>(logEntry)\n const before = payload?.before ?? null\n const knex = resolveKnex(ctx)\n\n if (!before || !before.translations) {\n // Was a create \u2014 delete the record\n const resourceId = logEntry?.resourceId\n if (resourceId) {\n await knex('entity_translations').where({ id: resourceId }).del()\n }\n } else {\n // Was an update \u2014 restore previous translations\n const existing = await knex('entity_translations')\n .where({ entity_type: before.entityType, entity_id: before.entityId })\n .andWhereRaw('tenant_id is not distinct from ?', [before.tenantId])\n .andWhereRaw('organization_id is not distinct from ?', [before.organizationId])\n .first()\n\n if (existing) {\n await knex('entity_translations')\n .where({ id: existing.id })\n .update({ translations: before.translations, updated_at: knex.fn.now() })\n } else {\n await knex('entity_translations').insert({\n entity_type: before.entityType,\n entity_id: before.entityId,\n organization_id: before.organizationId,\n tenant_id: before.tenantId,\n translations: before.translations,\n created_at: knex.fn.now(),\n updated_at: knex.fn.now(),\n })\n }\n }\n },\n}\n\nconst deleteTranslationCommand: CommandHandler<DeleteInput, { deleted: boolean }> = {\n id: 'translations.translation.delete',\n\n async prepare(input, ctx) {\n ensureTenantScope(ctx, input.tenantId)\n const knex = resolveKnex(ctx)\n const snapshot = await loadTranslationSnapshot(knex, input.entityType, input.entityId, input.tenantId, input.organizationId)\n return { before: snapshot }\n },\n\n async execute(input, ctx) {\n const knex = resolveKnex(ctx)\n const count = await knex('entity_translations')\n .where({ entity_type: input.entityType, entity_id: input.entityId })\n .andWhereRaw('tenant_id is not distinct from ?', [input.tenantId])\n .andWhereRaw('organization_id is not distinct from ?', [input.organizationId])\n .del()\n\n await emitTranslationsEvent('translations.translation.deleted', {\n entityType: input.entityType,\n entityId: input.entityId,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n }, { persistent: true }).catch(() => undefined)\n\n return { deleted: count > 0 }\n },\n\n async buildLog({ snapshots }) {\n const before = snapshots.before as TranslationSnapshot | null | undefined\n if (!before) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('translations.audit.delete', 'Delete translation'),\n resourceKind: 'translations.translation',\n resourceId: before.id ?? undefined,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n payload: {\n undo: { before } satisfies TranslationUndoPayload,\n },\n }\n },\n\n async undo({ logEntry, ctx }) {\n const payload = extractUndoPayload<TranslationUndoPayload>(logEntry)\n const before = payload?.before\n if (!before || !before.translations) return\n const knex = resolveKnex(ctx)\n\n const existing = await knex('entity_translations')\n .where({ entity_type: before.entityType, entity_id: before.entityId })\n .andWhereRaw('tenant_id is not distinct from ?', [before.tenantId])\n .andWhereRaw('organization_id is not distinct from ?', [before.organizationId])\n .first()\n\n if (!existing) {\n await knex('entity_translations').insert({\n entity_type: before.entityType,\n entity_id: before.entityId,\n organization_id: before.organizationId,\n tenant_id: before.tenantId,\n translations: before.translations,\n created_at: knex.fn.now(),\n updated_at: knex.fn.now(),\n })\n }\n },\n}\n\nregisterCommand(saveTranslationCommand)\nregisterCommand(deleteTranslationCommand)\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/translations/index.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ModuleInfo } from '@open-mercato/shared/modules/registry'\n\nexport const metadata: ModuleInfo = {\n name: 'translations',\n title: 'Entity Translations',\n version: '0.1.0',\n description: 'System-wide entity translation storage and locale overlay for CRUD responses.',\n author: 'Open Mercato Team',\n license: 'Proprietary',\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { ModuleInfo } from '@open-mercato/shared/modules/registry'\nimport './commands'\n\nexport const metadata: ModuleInfo = {\n name: 'translations',\n title: 'Entity Translations',\n version: '0.1.0',\n description: 'System-wide entity translation storage and locale overlay for CRUD responses.',\n author: 'Open Mercato Team',\n license: 'Proprietary',\n}\n"],
|
|
5
|
+
"mappings": "AACA,OAAO;AAEA,MAAM,WAAuB;AAAA,EAClC,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,SAAS;AACX;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.4.5-develop-
|
|
3
|
+
"version": "0.4.5-develop-0b66ecfdd4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
}
|
|
208
208
|
},
|
|
209
209
|
"dependencies": {
|
|
210
|
-
"@open-mercato/shared": "0.4.5-develop-
|
|
210
|
+
"@open-mercato/shared": "0.4.5-develop-0b66ecfdd4",
|
|
211
211
|
"@types/semver": "^7.5.8",
|
|
212
212
|
"@xyflow/react": "^12.6.0",
|
|
213
213
|
"ai": "^6.0.0",
|
|
@@ -3,6 +3,8 @@ import { z } from 'zod'
|
|
|
3
3
|
import { resolveTranslationsRouteContext } from '@open-mercato/core/modules/translations/api/context'
|
|
4
4
|
import { translationBodySchema, entityTypeParamSchema, entityIdParamSchema } from '@open-mercato/core/modules/translations/data/validators'
|
|
5
5
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
6
|
+
import { CommandBus } from '@open-mercato/shared/lib/commands'
|
|
7
|
+
import { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'
|
|
6
8
|
import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
7
9
|
|
|
8
10
|
const paramsSchema = z.object({
|
|
@@ -67,64 +69,53 @@ export async function PUT(req: Request, ctx: { params?: { entityType?: string; e
|
|
|
67
69
|
const rawBody = await req.json().catch(() => ({}))
|
|
68
70
|
const translations = translationBodySchema.parse(rawBody)
|
|
69
71
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
.andWhereRaw('organization_id is not distinct from ?', [context.organizationId])
|
|
77
|
-
.first()
|
|
78
|
-
|
|
79
|
-
const now = context.knex.fn.now()
|
|
80
|
-
|
|
81
|
-
if (existing) {
|
|
82
|
-
await context.knex('entity_translations')
|
|
83
|
-
.where({ id: existing.id })
|
|
84
|
-
.update({
|
|
85
|
-
translations,
|
|
86
|
-
updated_at: now,
|
|
87
|
-
})
|
|
88
|
-
} else {
|
|
89
|
-
await context.knex('entity_translations').insert({
|
|
90
|
-
entity_type: entityType,
|
|
91
|
-
entity_id: entityId,
|
|
92
|
-
organization_id: context.organizationId,
|
|
93
|
-
tenant_id: context.tenantId,
|
|
94
|
-
translations,
|
|
95
|
-
created_at: now,
|
|
96
|
-
updated_at: now,
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
const bus = context.container.resolve<{ emitEvent: (event: string, payload: unknown) => Promise<void> }>('eventBus')
|
|
102
|
-
await bus.emitEvent('translations.translation.updated', {
|
|
72
|
+
const commandBus = context.container.resolve('commandBus') as CommandBus
|
|
73
|
+
const { result, logEntry } = await commandBus.execute<
|
|
74
|
+
{ entityType: string; entityId: string; translations: typeof translations; organizationId: string | null; tenantId: string },
|
|
75
|
+
{ rowId: string }
|
|
76
|
+
>('translations.translation.save', {
|
|
77
|
+
input: {
|
|
103
78
|
entityType,
|
|
104
79
|
entityId,
|
|
80
|
+
translations,
|
|
105
81
|
organizationId: context.organizationId,
|
|
106
82
|
tenantId: context.tenantId,
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
83
|
+
},
|
|
84
|
+
ctx: context.commandCtx,
|
|
85
|
+
})
|
|
111
86
|
|
|
112
87
|
const row = await context.knex('entity_translations')
|
|
113
|
-
.where({
|
|
114
|
-
entity_type: entityType,
|
|
115
|
-
entity_id: entityId,
|
|
116
|
-
})
|
|
117
|
-
.andWhereRaw('tenant_id is not distinct from ?', [context.tenantId])
|
|
118
|
-
.andWhereRaw('organization_id is not distinct from ?', [context.organizationId])
|
|
88
|
+
.where({ id: result.rowId })
|
|
119
89
|
.first()
|
|
120
90
|
|
|
121
|
-
|
|
91
|
+
const response = NextResponse.json({
|
|
122
92
|
entityType: row.entity_type,
|
|
123
93
|
entityId: row.entity_id,
|
|
124
94
|
translations: row.translations,
|
|
125
95
|
createdAt: row.created_at,
|
|
126
96
|
updatedAt: row.updated_at,
|
|
127
97
|
})
|
|
98
|
+
|
|
99
|
+
if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
|
|
100
|
+
response.headers.set(
|
|
101
|
+
'x-om-operation',
|
|
102
|
+
serializeOperationMetadata({
|
|
103
|
+
id: logEntry.id,
|
|
104
|
+
undoToken: logEntry.undoToken,
|
|
105
|
+
commandId: logEntry.commandId,
|
|
106
|
+
actionLabel: logEntry.actionLabel ?? null,
|
|
107
|
+
resourceKind: logEntry.resourceKind ?? 'translations.translation',
|
|
108
|
+
resourceId: logEntry.resourceId ?? result.rowId,
|
|
109
|
+
executedAt: logEntry.createdAt instanceof Date
|
|
110
|
+
? logEntry.createdAt.toISOString()
|
|
111
|
+
: typeof logEntry.createdAt === 'string'
|
|
112
|
+
? logEntry.createdAt
|
|
113
|
+
: new Date().toISOString(),
|
|
114
|
+
}),
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return response
|
|
128
119
|
} catch (err) {
|
|
129
120
|
if (err instanceof CrudHttpError) {
|
|
130
121
|
return NextResponse.json(err.body, { status: err.status })
|
|
@@ -145,28 +136,42 @@ export async function DELETE(req: Request, ctx: { params?: { entityType?: string
|
|
|
145
136
|
entityId: ctx.params?.entityId,
|
|
146
137
|
})
|
|
147
138
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
.andWhereRaw('organization_id is not distinct from ?', [context.organizationId])
|
|
155
|
-
.del()
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
const bus = context.container.resolve<{ emitEvent: (event: string, payload: unknown) => Promise<void> }>('eventBus')
|
|
159
|
-
await bus.emitEvent('translations.translation.deleted', {
|
|
139
|
+
const commandBus = context.container.resolve('commandBus') as CommandBus
|
|
140
|
+
const { logEntry } = await commandBus.execute<
|
|
141
|
+
{ entityType: string; entityId: string; organizationId: string | null; tenantId: string },
|
|
142
|
+
{ deleted: boolean }
|
|
143
|
+
>('translations.translation.delete', {
|
|
144
|
+
input: {
|
|
160
145
|
entityType,
|
|
161
146
|
entityId,
|
|
162
147
|
organizationId: context.organizationId,
|
|
163
148
|
tenantId: context.tenantId,
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
149
|
+
},
|
|
150
|
+
ctx: context.commandCtx,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const response = new NextResponse(null, { status: 204 })
|
|
154
|
+
|
|
155
|
+
if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
|
|
156
|
+
response.headers.set(
|
|
157
|
+
'x-om-operation',
|
|
158
|
+
serializeOperationMetadata({
|
|
159
|
+
id: logEntry.id,
|
|
160
|
+
undoToken: logEntry.undoToken,
|
|
161
|
+
commandId: logEntry.commandId,
|
|
162
|
+
actionLabel: logEntry.actionLabel ?? null,
|
|
163
|
+
resourceKind: logEntry.resourceKind ?? 'translations.translation',
|
|
164
|
+
resourceId: logEntry.resourceId ?? null,
|
|
165
|
+
executedAt: logEntry.createdAt instanceof Date
|
|
166
|
+
? logEntry.createdAt.toISOString()
|
|
167
|
+
: typeof logEntry.createdAt === 'string'
|
|
168
|
+
? logEntry.createdAt
|
|
169
|
+
: new Date().toISOString(),
|
|
170
|
+
}),
|
|
171
|
+
)
|
|
167
172
|
}
|
|
168
173
|
|
|
169
|
-
return
|
|
174
|
+
return response
|
|
170
175
|
} catch (err) {
|
|
171
176
|
if (err instanceof CrudHttpError) {
|
|
172
177
|
return NextResponse.json(err.body, { status: err.status })
|
|
@@ -2,6 +2,7 @@ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
|
2
2
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
3
3
|
import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
4
4
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
5
|
+
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
5
6
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
6
7
|
import type { AwilixContainer } from 'awilix'
|
|
7
8
|
import type { Knex } from 'knex'
|
|
@@ -13,6 +14,7 @@ export type TranslationsRouteContext = {
|
|
|
13
14
|
knex: Knex
|
|
14
15
|
organizationId: string | null
|
|
15
16
|
tenantId: string
|
|
17
|
+
commandCtx: CommandRuntimeContext
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export async function resolveTranslationsRouteContext(req: Request): Promise<TranslationsRouteContext> {
|
|
@@ -28,6 +30,15 @@ export async function resolveTranslationsRouteContext(req: Request): Promise<Tra
|
|
|
28
30
|
const tenantId: string = scope?.tenantId ?? auth.tenantId
|
|
29
31
|
const organizationId = scope?.selectedId ?? auth.orgId ?? null
|
|
30
32
|
|
|
33
|
+
const commandCtx: CommandRuntimeContext = {
|
|
34
|
+
container,
|
|
35
|
+
auth,
|
|
36
|
+
organizationScope: scope,
|
|
37
|
+
selectedOrganizationId: organizationId,
|
|
38
|
+
organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),
|
|
39
|
+
request: req,
|
|
40
|
+
}
|
|
41
|
+
|
|
31
42
|
return {
|
|
32
43
|
container,
|
|
33
44
|
auth,
|
|
@@ -35,5 +46,6 @@ export async function resolveTranslationsRouteContext(req: Request): Promise<Tra
|
|
|
35
46
|
knex,
|
|
36
47
|
organizationId,
|
|
37
48
|
tenantId,
|
|
49
|
+
commandCtx,
|
|
38
50
|
}
|
|
39
51
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './translations'
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { registerCommand } from '@open-mercato/shared/lib/commands'
|
|
2
|
+
import type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
3
|
+
import { ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'
|
|
4
|
+
import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
|
|
5
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
6
|
+
import type { Knex } from 'knex'
|
|
7
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
8
|
+
import { emitTranslationsEvent } from '../events'
|
|
9
|
+
|
|
10
|
+
type TranslationSnapshot = {
|
|
11
|
+
id: string | null
|
|
12
|
+
entityType: string
|
|
13
|
+
entityId: string
|
|
14
|
+
translations: Record<string, Record<string, string | null>> | null
|
|
15
|
+
organizationId: string | null
|
|
16
|
+
tenantId: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type TranslationUndoPayload = {
|
|
20
|
+
before?: TranslationSnapshot | null
|
|
21
|
+
after?: TranslationSnapshot | null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type SaveInput = {
|
|
25
|
+
entityType: string
|
|
26
|
+
entityId: string
|
|
27
|
+
translations: Record<string, Record<string, string | null>>
|
|
28
|
+
organizationId: string | null
|
|
29
|
+
tenantId: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type DeleteInput = {
|
|
33
|
+
entityType: string
|
|
34
|
+
entityId: string
|
|
35
|
+
organizationId: string | null
|
|
36
|
+
tenantId: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveKnex(ctx: CommandRuntimeContext): Knex {
|
|
40
|
+
const em = ctx.container.resolve('em') as EntityManager
|
|
41
|
+
return (em as unknown as { getConnection(): { getKnex(): Knex } }).getConnection().getKnex()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function loadTranslationSnapshot(
|
|
45
|
+
knex: Knex,
|
|
46
|
+
entityType: string,
|
|
47
|
+
entityId: string,
|
|
48
|
+
tenantId: string,
|
|
49
|
+
organizationId: string | null,
|
|
50
|
+
): Promise<TranslationSnapshot | null> {
|
|
51
|
+
const row = await knex('entity_translations')
|
|
52
|
+
.where({ entity_type: entityType, entity_id: entityId })
|
|
53
|
+
.andWhereRaw('tenant_id is not distinct from ?', [tenantId])
|
|
54
|
+
.andWhereRaw('organization_id is not distinct from ?', [organizationId])
|
|
55
|
+
.first()
|
|
56
|
+
|
|
57
|
+
if (!row) return null
|
|
58
|
+
return {
|
|
59
|
+
id: row.id,
|
|
60
|
+
entityType: row.entity_type,
|
|
61
|
+
entityId: row.entity_id,
|
|
62
|
+
translations: row.translations ?? null,
|
|
63
|
+
organizationId: row.organization_id ?? null,
|
|
64
|
+
tenantId: row.tenant_id,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const saveTranslationCommand: CommandHandler<SaveInput, { rowId: string }> = {
|
|
69
|
+
id: 'translations.translation.save',
|
|
70
|
+
|
|
71
|
+
async prepare(input, ctx) {
|
|
72
|
+
ensureTenantScope(ctx, input.tenantId)
|
|
73
|
+
const knex = resolveKnex(ctx)
|
|
74
|
+
const snapshot = await loadTranslationSnapshot(knex, input.entityType, input.entityId, input.tenantId, input.organizationId)
|
|
75
|
+
return { before: snapshot }
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
async execute(input, ctx) {
|
|
79
|
+
const knex = resolveKnex(ctx)
|
|
80
|
+
const existing = await knex('entity_translations')
|
|
81
|
+
.where({ entity_type: input.entityType, entity_id: input.entityId })
|
|
82
|
+
.andWhereRaw('tenant_id is not distinct from ?', [input.tenantId])
|
|
83
|
+
.andWhereRaw('organization_id is not distinct from ?', [input.organizationId])
|
|
84
|
+
.first()
|
|
85
|
+
|
|
86
|
+
const now = knex.fn.now()
|
|
87
|
+
|
|
88
|
+
if (existing) {
|
|
89
|
+
await knex('entity_translations')
|
|
90
|
+
.where({ id: existing.id })
|
|
91
|
+
.update({ translations: input.translations, updated_at: now })
|
|
92
|
+
} else {
|
|
93
|
+
await knex('entity_translations').insert({
|
|
94
|
+
entity_type: input.entityType,
|
|
95
|
+
entity_id: input.entityId,
|
|
96
|
+
organization_id: input.organizationId,
|
|
97
|
+
tenant_id: input.tenantId,
|
|
98
|
+
translations: input.translations,
|
|
99
|
+
created_at: now,
|
|
100
|
+
updated_at: now,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await emitTranslationsEvent('translations.translation.updated', {
|
|
105
|
+
entityType: input.entityType,
|
|
106
|
+
entityId: input.entityId,
|
|
107
|
+
organizationId: input.organizationId,
|
|
108
|
+
tenantId: input.tenantId,
|
|
109
|
+
}, { persistent: true }).catch(() => undefined)
|
|
110
|
+
|
|
111
|
+
const saved = await knex('entity_translations')
|
|
112
|
+
.where({ entity_type: input.entityType, entity_id: input.entityId })
|
|
113
|
+
.andWhereRaw('tenant_id is not distinct from ?', [input.tenantId])
|
|
114
|
+
.andWhereRaw('organization_id is not distinct from ?', [input.organizationId])
|
|
115
|
+
.first()
|
|
116
|
+
|
|
117
|
+
return { rowId: saved.id }
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
async captureAfter(input, _result, ctx) {
|
|
121
|
+
const knex = resolveKnex(ctx)
|
|
122
|
+
return await loadTranslationSnapshot(knex, input.entityType, input.entityId, input.tenantId, input.organizationId)
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
async buildLog({ snapshots, result }) {
|
|
126
|
+
const { translate } = await resolveTranslations()
|
|
127
|
+
const before = snapshots.before as TranslationSnapshot | null | undefined
|
|
128
|
+
const after = snapshots.after as TranslationSnapshot | null | undefined
|
|
129
|
+
return {
|
|
130
|
+
actionLabel: translate('translations.audit.save', 'Save translation'),
|
|
131
|
+
resourceKind: 'translations.translation',
|
|
132
|
+
resourceId: result.rowId,
|
|
133
|
+
tenantId: after?.tenantId ?? before?.tenantId ?? null,
|
|
134
|
+
organizationId: after?.organizationId ?? before?.organizationId ?? null,
|
|
135
|
+
snapshotBefore: before ?? null,
|
|
136
|
+
snapshotAfter: after ?? null,
|
|
137
|
+
payload: {
|
|
138
|
+
undo: { before: before ?? null, after: after ?? null } satisfies TranslationUndoPayload,
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
async undo({ logEntry, ctx }) {
|
|
144
|
+
const payload = extractUndoPayload<TranslationUndoPayload>(logEntry)
|
|
145
|
+
const before = payload?.before ?? null
|
|
146
|
+
const knex = resolveKnex(ctx)
|
|
147
|
+
|
|
148
|
+
if (!before || !before.translations) {
|
|
149
|
+
// Was a create — delete the record
|
|
150
|
+
const resourceId = logEntry?.resourceId
|
|
151
|
+
if (resourceId) {
|
|
152
|
+
await knex('entity_translations').where({ id: resourceId }).del()
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
// Was an update — restore previous translations
|
|
156
|
+
const existing = await knex('entity_translations')
|
|
157
|
+
.where({ entity_type: before.entityType, entity_id: before.entityId })
|
|
158
|
+
.andWhereRaw('tenant_id is not distinct from ?', [before.tenantId])
|
|
159
|
+
.andWhereRaw('organization_id is not distinct from ?', [before.organizationId])
|
|
160
|
+
.first()
|
|
161
|
+
|
|
162
|
+
if (existing) {
|
|
163
|
+
await knex('entity_translations')
|
|
164
|
+
.where({ id: existing.id })
|
|
165
|
+
.update({ translations: before.translations, updated_at: knex.fn.now() })
|
|
166
|
+
} else {
|
|
167
|
+
await knex('entity_translations').insert({
|
|
168
|
+
entity_type: before.entityType,
|
|
169
|
+
entity_id: before.entityId,
|
|
170
|
+
organization_id: before.organizationId,
|
|
171
|
+
tenant_id: before.tenantId,
|
|
172
|
+
translations: before.translations,
|
|
173
|
+
created_at: knex.fn.now(),
|
|
174
|
+
updated_at: knex.fn.now(),
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const deleteTranslationCommand: CommandHandler<DeleteInput, { deleted: boolean }> = {
|
|
182
|
+
id: 'translations.translation.delete',
|
|
183
|
+
|
|
184
|
+
async prepare(input, ctx) {
|
|
185
|
+
ensureTenantScope(ctx, input.tenantId)
|
|
186
|
+
const knex = resolveKnex(ctx)
|
|
187
|
+
const snapshot = await loadTranslationSnapshot(knex, input.entityType, input.entityId, input.tenantId, input.organizationId)
|
|
188
|
+
return { before: snapshot }
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
async execute(input, ctx) {
|
|
192
|
+
const knex = resolveKnex(ctx)
|
|
193
|
+
const count = await knex('entity_translations')
|
|
194
|
+
.where({ entity_type: input.entityType, entity_id: input.entityId })
|
|
195
|
+
.andWhereRaw('tenant_id is not distinct from ?', [input.tenantId])
|
|
196
|
+
.andWhereRaw('organization_id is not distinct from ?', [input.organizationId])
|
|
197
|
+
.del()
|
|
198
|
+
|
|
199
|
+
await emitTranslationsEvent('translations.translation.deleted', {
|
|
200
|
+
entityType: input.entityType,
|
|
201
|
+
entityId: input.entityId,
|
|
202
|
+
organizationId: input.organizationId,
|
|
203
|
+
tenantId: input.tenantId,
|
|
204
|
+
}, { persistent: true }).catch(() => undefined)
|
|
205
|
+
|
|
206
|
+
return { deleted: count > 0 }
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
async buildLog({ snapshots }) {
|
|
210
|
+
const before = snapshots.before as TranslationSnapshot | null | undefined
|
|
211
|
+
if (!before) return null
|
|
212
|
+
const { translate } = await resolveTranslations()
|
|
213
|
+
return {
|
|
214
|
+
actionLabel: translate('translations.audit.delete', 'Delete translation'),
|
|
215
|
+
resourceKind: 'translations.translation',
|
|
216
|
+
resourceId: before.id ?? undefined,
|
|
217
|
+
tenantId: before.tenantId,
|
|
218
|
+
organizationId: before.organizationId,
|
|
219
|
+
snapshotBefore: before,
|
|
220
|
+
payload: {
|
|
221
|
+
undo: { before } satisfies TranslationUndoPayload,
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
async undo({ logEntry, ctx }) {
|
|
227
|
+
const payload = extractUndoPayload<TranslationUndoPayload>(logEntry)
|
|
228
|
+
const before = payload?.before
|
|
229
|
+
if (!before || !before.translations) return
|
|
230
|
+
const knex = resolveKnex(ctx)
|
|
231
|
+
|
|
232
|
+
const existing = await knex('entity_translations')
|
|
233
|
+
.where({ entity_type: before.entityType, entity_id: before.entityId })
|
|
234
|
+
.andWhereRaw('tenant_id is not distinct from ?', [before.tenantId])
|
|
235
|
+
.andWhereRaw('organization_id is not distinct from ?', [before.organizationId])
|
|
236
|
+
.first()
|
|
237
|
+
|
|
238
|
+
if (!existing) {
|
|
239
|
+
await knex('entity_translations').insert({
|
|
240
|
+
entity_type: before.entityType,
|
|
241
|
+
entity_id: before.entityId,
|
|
242
|
+
organization_id: before.organizationId,
|
|
243
|
+
tenant_id: before.tenantId,
|
|
244
|
+
translations: before.translations,
|
|
245
|
+
created_at: knex.fn.now(),
|
|
246
|
+
updated_at: knex.fn.now(),
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
registerCommand(saveTranslationCommand)
|
|
253
|
+
registerCommand(deleteTranslationCommand)
|