@open-mercato/core 0.6.6-develop.5738.1.4182c2b2c7 → 0.6.6-develop.5751.1.39143f001b
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/currencies/api/currencies/options/route.js +40 -5
- package/dist/modules/currencies/api/currencies/options/route.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js +43 -3
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js.map +2 -2
- package/dist/modules/dictionaries/fields/dictionary.js +16 -7
- package/dist/modules/dictionaries/fields/dictionary.js.map +2 -2
- package/dist/modules/notifications/api/unread-count/route.js +33 -1
- package/dist/modules/notifications/api/unread-count/route.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/currencies/api/currencies/options/route.ts +55 -5
- package/src/modules/dictionaries/api/[dictionaryId]/entries/route.ts +58 -3
- package/src/modules/dictionaries/fields/dictionary.tsx +15 -8
- package/src/modules/notifications/api/unread-count/route.ts +36 -1
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { runWithCacheTenant } from "@open-mercato/cache";
|
|
3
4
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
5
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
6
|
import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
|
|
7
|
+
import {
|
|
8
|
+
buildCollectionTags,
|
|
9
|
+
isCrudCacheEnabled,
|
|
10
|
+
resolveCrudCache
|
|
11
|
+
} from "@open-mercato/shared/lib/crud/cache";
|
|
6
12
|
import { Currency } from "../../../data/entities.js";
|
|
7
13
|
const metadata = {
|
|
8
14
|
GET: { requireAuth: true, requireFeatures: ["currencies.view"] }
|
|
9
15
|
};
|
|
16
|
+
const CURRENCY_OPTIONS_RESOURCE = "currencies.currency";
|
|
17
|
+
const CURRENCY_OPTIONS_TTL_MS = 5 * 6e4;
|
|
18
|
+
function buildOptionsCacheKey(params) {
|
|
19
|
+
return `currencies:options:org=${params.orgId ?? "null"}:active=${params.includeInactive ? "all" : "active"}:limit=${params.limit}`;
|
|
20
|
+
}
|
|
10
21
|
const optionsQuerySchema = z.object({
|
|
11
22
|
q: z.string().optional(),
|
|
12
23
|
query: z.string().optional(),
|
|
@@ -34,14 +45,25 @@ async function GET(req) {
|
|
|
34
45
|
const em = container.resolve("em");
|
|
35
46
|
const { q, query, search, includeInactive, limit } = parsed.data;
|
|
36
47
|
const searchTerm = (q ?? query ?? search ?? "").trim();
|
|
48
|
+
const tenantId = auth.tenantId;
|
|
49
|
+
const orgId = auth.orgId ?? null;
|
|
50
|
+
const includeInactiveFlag = includeInactive === "true";
|
|
51
|
+
const cache = isCrudCacheEnabled() && !searchTerm ? resolveCrudCache(container) : null;
|
|
52
|
+
const cacheKey = cache ? buildOptionsCacheKey({ orgId, includeInactive: includeInactiveFlag, limit }) : null;
|
|
53
|
+
if (cache && cacheKey) {
|
|
54
|
+
const cached = await runWithCacheTenant(tenantId, () => cache.get(cacheKey));
|
|
55
|
+
if (cached) {
|
|
56
|
+
return NextResponse.json(cached);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
37
59
|
const filter = {
|
|
38
|
-
tenantId
|
|
60
|
+
tenantId,
|
|
39
61
|
deletedAt: null
|
|
40
62
|
};
|
|
41
|
-
if (
|
|
42
|
-
filter.organizationId =
|
|
63
|
+
if (orgId) {
|
|
64
|
+
filter.organizationId = orgId;
|
|
43
65
|
}
|
|
44
|
-
if (
|
|
66
|
+
if (!includeInactiveFlag) {
|
|
45
67
|
filter.isActive = true;
|
|
46
68
|
}
|
|
47
69
|
if (searchTerm) {
|
|
@@ -59,7 +81,20 @@ async function GET(req) {
|
|
|
59
81
|
value: String(currency.code),
|
|
60
82
|
label: `${currency.code} - ${currency.name}`
|
|
61
83
|
}));
|
|
62
|
-
|
|
84
|
+
const payload = { items };
|
|
85
|
+
if (cache && cacheKey) {
|
|
86
|
+
try {
|
|
87
|
+
await runWithCacheTenant(
|
|
88
|
+
tenantId,
|
|
89
|
+
() => cache.set(cacheKey, payload, {
|
|
90
|
+
ttl: CURRENCY_OPTIONS_TTL_MS,
|
|
91
|
+
tags: buildCollectionTags(CURRENCY_OPTIONS_RESOURCE, tenantId, [orgId])
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return NextResponse.json(payload);
|
|
63
98
|
}
|
|
64
99
|
const optionsResponseSchema = z.object({
|
|
65
100
|
items: z.array(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/currencies/api/currencies/options/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { Currency } from '../../../data/entities'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['currencies.view'] },\n}\n\nconst optionsQuerySchema = z.object({\n q: z.string().optional(),\n query: z.string().optional(),\n search: z.string().optional(),\n includeInactive: z.enum(['true', 'false']).optional(),\n limit: z.coerce.number().min(1).max(100).default(50),\n}).loose()\n\ntype OptionsItem = {\n value: string\n label: string\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) {\n return NextResponse.json({ items: [] }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const parsed = optionsQuerySchema.safeParse({\n q: url.searchParams.get('q') ?? undefined,\n query: url.searchParams.get('query') ?? undefined,\n search: url.searchParams.get('search') ?? undefined,\n includeInactive: url.searchParams.get('includeInactive') ?? undefined,\n limit: url.searchParams.get('limit') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json({ items: [] }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n\n const { q, query, search, includeInactive, limit } = parsed.data\n const searchTerm = (q ?? query ?? search ?? '').trim()\n const
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,yBAAyB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport {\n buildCollectionTags,\n isCrudCacheEnabled,\n resolveCrudCache,\n} from '@open-mercato/shared/lib/crud/cache'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { Currency } from '../../../data/entities'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['currencies.view'] },\n}\n\nconst CURRENCY_OPTIONS_RESOURCE = 'currencies.currency'\nconst CURRENCY_OPTIONS_TTL_MS = 5 * 60_000\n\nfunction buildOptionsCacheKey(params: {\n orgId: string | null\n includeInactive: boolean\n limit: number\n}): string {\n return `currencies:options:org=${params.orgId ?? 'null'}:active=${params.includeInactive ? 'all' : 'active'}:limit=${params.limit}`\n}\n\nconst optionsQuerySchema = z.object({\n q: z.string().optional(),\n query: z.string().optional(),\n search: z.string().optional(),\n includeInactive: z.enum(['true', 'false']).optional(),\n limit: z.coerce.number().min(1).max(100).default(50),\n}).loose()\n\ntype OptionsItem = {\n value: string\n label: string\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) {\n return NextResponse.json({ items: [] }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const parsed = optionsQuerySchema.safeParse({\n q: url.searchParams.get('q') ?? undefined,\n query: url.searchParams.get('query') ?? undefined,\n search: url.searchParams.get('search') ?? undefined,\n includeInactive: url.searchParams.get('includeInactive') ?? undefined,\n limit: url.searchParams.get('limit') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json({ items: [] }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n\n const { q, query, search, includeInactive, limit } = parsed.data\n const searchTerm = (q ?? query ?? search ?? '').trim()\n const tenantId = auth.tenantId\n const orgId = auth.orgId ?? null\n const includeInactiveFlag = includeInactive === 'true'\n\n // Only the unfiltered bootstrap call (no ILIKE search term) is the hot path\n // worth caching; search variants would multiply key cardinality with little\n // hit-rate benefit, so they always read straight through.\n const cache =\n isCrudCacheEnabled() && !searchTerm ? resolveCrudCache(container) : null\n const cacheKey = cache\n ? buildOptionsCacheKey({ orgId, includeInactive: includeInactiveFlag, limit })\n : null\n\n if (cache && cacheKey) {\n const cached = await runWithCacheTenant(tenantId, () => cache.get(cacheKey))\n if (cached) {\n return NextResponse.json(cached)\n }\n }\n\n const filter: any = {\n tenantId,\n deletedAt: null,\n }\n if (orgId) {\n filter.organizationId = orgId\n }\n\n if (!includeInactiveFlag) {\n filter.isActive = true\n }\n\n if (searchTerm) {\n const escaped = escapeLikePattern(searchTerm)\n filter.$or = [\n { code: { $ilike: `%${escaped}%` } },\n { name: { $ilike: `%${escaped}%` } },\n ]\n }\n\n const rows = await em.find(Currency, filter, {\n orderBy: { code: 'ASC' },\n limit,\n })\n\n const items: OptionsItem[] = rows.map((currency) => ({\n value: String(currency.code),\n label: `${currency.code} - ${currency.name}`,\n }))\n\n const payload = { items }\n\n if (cache && cacheKey) {\n try {\n await runWithCacheTenant(tenantId, () =>\n cache.set(cacheKey, payload, {\n ttl: CURRENCY_OPTIONS_TTL_MS,\n tags: buildCollectionTags(CURRENCY_OPTIONS_RESOURCE, tenantId, [orgId]),\n }),\n )\n } catch {}\n }\n\n return NextResponse.json(payload)\n}\n\nconst optionsResponseSchema = z.object({\n items: z.array(\n z.object({\n value: z.string(),\n label: z.string(),\n })\n ),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Currencies',\n summary: 'Currency options',\n methods: {\n GET: {\n summary: 'List currency options',\n description: 'Returns currencies formatted for select inputs.',\n query: optionsQuerySchema,\n responses: [\n { status: 200, description: 'Option list', schema: optionsResponseSchema },\n { status: 401, description: 'Unauthorized', schema: z.object({ items: z.array(z.any()) }) },\n { status: 400, description: 'Invalid query', schema: z.object({ items: z.array(z.any()) }) },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { runWithCacheTenant } from "@open-mercato/cache";
|
|
3
4
|
import { Dictionary, DictionaryEntry } from "@open-mercato/core/modules/dictionaries/data/entities";
|
|
4
5
|
import { resolveDictionariesRouteContext } from "@open-mercato/core/modules/dictionaries/api/context";
|
|
5
6
|
import { createDictionaryEntrySchema } from "@open-mercato/core/modules/dictionaries/data/validators";
|
|
@@ -14,11 +15,23 @@ import {
|
|
|
14
15
|
dictionariesTag
|
|
15
16
|
} from "../../openapi.js";
|
|
16
17
|
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
18
|
+
import {
|
|
19
|
+
buildCollectionTags,
|
|
20
|
+
buildRecordTag,
|
|
21
|
+
isCrudCacheEnabled,
|
|
22
|
+
resolveCrudCache
|
|
23
|
+
} from "@open-mercato/shared/lib/crud/cache";
|
|
17
24
|
import {
|
|
18
25
|
resolveDictionaryEntrySortMode,
|
|
19
26
|
sortDictionaryEntries
|
|
20
27
|
} from "@open-mercato/core/modules/dictionaries/lib/entrySort";
|
|
21
28
|
const paramsSchema = z.object({ dictionaryId: z.string().uuid() });
|
|
29
|
+
const DICTIONARY_ENTRY_RESOURCE = "dictionaries.entry";
|
|
30
|
+
const DICTIONARY_DEFINITION_RESOURCE = "dictionaries.dictionary";
|
|
31
|
+
const DICTIONARY_ENTRIES_TTL_MS = 5 * 6e4;
|
|
32
|
+
function buildEntriesCacheKey(params) {
|
|
33
|
+
return `dictionaries:entries:${params.dictionaryId}:org=${params.organizationId ?? "null"}:sort=${params.sortMode}`;
|
|
34
|
+
}
|
|
22
35
|
async function loadDictionary(context, id, options = {}) {
|
|
23
36
|
const { allowInherited = false } = options;
|
|
24
37
|
if (!allowInherited && !context.organizationId) {
|
|
@@ -54,6 +67,17 @@ async function GET(req, ctx) {
|
|
|
54
67
|
}
|
|
55
68
|
const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId });
|
|
56
69
|
const dictionary = await loadDictionary(context, dictionaryId, { allowInherited: true });
|
|
70
|
+
const dictionaryTenantId = dictionary.tenantId;
|
|
71
|
+
const dictionaryOrgId = dictionary.organizationId ?? null;
|
|
72
|
+
const sortMode = resolveDictionaryEntrySortMode(dictionary.entrySortMode);
|
|
73
|
+
const cache = isCrudCacheEnabled() ? resolveCrudCache(context.container) : null;
|
|
74
|
+
const cacheKey = cache ? buildEntriesCacheKey({ dictionaryId, organizationId: dictionaryOrgId, sortMode }) : null;
|
|
75
|
+
if (cache && cacheKey) {
|
|
76
|
+
const cached = await runWithCacheTenant(dictionaryTenantId, () => cache.get(cacheKey));
|
|
77
|
+
if (cached) {
|
|
78
|
+
return NextResponse.json(cached);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
57
81
|
const entries = await findWithDecryption(
|
|
58
82
|
context.em,
|
|
59
83
|
DictionaryEntry,
|
|
@@ -65,8 +89,8 @@ async function GET(req, ctx) {
|
|
|
65
89
|
{},
|
|
66
90
|
{ tenantId: dictionary.tenantId, organizationId: dictionary.organizationId }
|
|
67
91
|
);
|
|
68
|
-
const sortedEntries = sortDictionaryEntries(entries,
|
|
69
|
-
|
|
92
|
+
const sortedEntries = sortDictionaryEntries(entries, sortMode);
|
|
93
|
+
const payload = {
|
|
70
94
|
items: sortedEntries.map((entry) => ({
|
|
71
95
|
id: entry.id,
|
|
72
96
|
value: entry.value,
|
|
@@ -78,7 +102,23 @@ async function GET(req, ctx) {
|
|
|
78
102
|
createdAt: entry.createdAt,
|
|
79
103
|
updatedAt: entry.updatedAt
|
|
80
104
|
}))
|
|
81
|
-
}
|
|
105
|
+
};
|
|
106
|
+
if (cache && cacheKey) {
|
|
107
|
+
try {
|
|
108
|
+
await runWithCacheTenant(
|
|
109
|
+
dictionaryTenantId,
|
|
110
|
+
() => cache.set(cacheKey, payload, {
|
|
111
|
+
ttl: DICTIONARY_ENTRIES_TTL_MS,
|
|
112
|
+
tags: [
|
|
113
|
+
...buildCollectionTags(DICTIONARY_ENTRY_RESOURCE, dictionaryTenantId, [dictionaryOrgId]),
|
|
114
|
+
buildRecordTag(DICTIONARY_DEFINITION_RESOURCE, dictionaryTenantId, dictionaryId)
|
|
115
|
+
]
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return NextResponse.json(payload);
|
|
82
122
|
} catch (err) {
|
|
83
123
|
if (isCrudHttpError(err)) {
|
|
84
124
|
return NextResponse.json(err.body, { status: err.status });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/dictionaries/api/%5BdictionaryId%5D/entries/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport { resolveDictionariesRouteContext } from '@open-mercato/core/modules/dictionaries/api/context'\nimport { createDictionaryEntrySchema } from '@open-mercato/core/modules/dictionaries/data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { 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'\nimport {\n createDictionaryEntrySchema as createEntryDocSchema,\n dictionaryEntryListResponseSchema,\n dictionaryEntryResponseSchema,\n dictionaryIdParamsSchema,\n dictionariesErrorSchema,\n dictionariesTag,\n} from '../../openapi'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n resolveDictionaryEntrySortMode,\n sortDictionaryEntries,\n} from '@open-mercato/core/modules/dictionaries/lib/entrySort'\n\nconst paramsSchema = z.object({ dictionaryId: z.string().uuid() })\n\nasync function loadDictionary(\n context: Awaited<ReturnType<typeof resolveDictionariesRouteContext>>,\n id: string,\n options: { allowInherited?: boolean } = {},\n) {\n const { allowInherited = false } = options\n if (!allowInherited && !context.organizationId) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.organization_required', 'Organization context is required') })\n }\n const baseFilter = {\n id,\n tenantId: context.tenantId,\n deletedAt: null,\n }\n const filter = allowInherited\n ? {\n ...baseFilter,\n ...(context.readableOrganizationIds.length\n ? { organizationId: { $in: context.readableOrganizationIds } }\n : {}),\n }\n : {\n ...baseFilter,\n organizationId: context.organizationId,\n }\n const dictionary = await context.em.findOne(Dictionary, filter)\n if (!dictionary) {\n throw new CrudHttpError(404, { error: context.translate('dictionaries.errors.not_found', 'Dictionary not found') })\n }\n return dictionary\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['dictionaries.view'] },\n POST: { requireAuth: true, requireFeatures: ['dictionaries.manage'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n if (!context.auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n const dictionary = await loadDictionary(context, dictionaryId, { allowInherited: true })\n const entries = await findWithDecryption(\n context.em,\n DictionaryEntry,\n {\n dictionary,\n organizationId: dictionary.organizationId,\n tenantId: dictionary.tenantId,\n },\n {},\n { tenantId: dictionary.tenantId, organizationId: dictionary.organizationId },\n )\n const sortedEntries = sortDictionaryEntries(entries, resolveDictionaryEntrySortMode(dictionary.entrySortMode))\n\n return NextResponse.json({\n items: sortedEntries.map((entry) => ({\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n position: entry.position ?? 0,\n isDefault: entry.isDefault ?? false,\n createdAt: entry.createdAt,\n updatedAt: entry.updatedAt,\n })),\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n console.error('[dictionaries/:id/entries.GET] Unexpected error', err)\n return NextResponse.json({ error: 'Failed to load dictionary entries' }, { status: 500 })\n }\n}\n\nexport async function POST(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n if (!context.auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n const payload = createDictionaryEntrySchema.parse(await req.json().catch(() => ({})))\n // These nested routes do not use makeCrudRoute, so we invoke the command bus directly.\n const commandBus = (context.container.resolve('commandBus') as CommandBus)\n const { result, logEntry } = await commandBus.execute('dictionaries.entries.create', {\n input: { ...payload, dictionaryId },\n ctx: context.ctx,\n })\n const createResult = (result ?? {}) as { entryId?: string | null }\n const createdEntryId = typeof createResult.entryId === 'string' ? createResult.entryId : null\n if (!createdEntryId) {\n throw new CrudHttpError(500, { error: context.translate('dictionaries.errors.entry_create_failed', 'Failed to create dictionary entry') })\n }\n const entry = await findOneWithDecryption(\n context.em.fork(),\n DictionaryEntry,\n createdEntryId,\n { populate: ['dictionary'] },\n { tenantId: context.auth.tenantId ?? null, organizationId: context.auth.orgId ?? null },\n )\n if (!entry) {\n throw new CrudHttpError(500, { error: context.translate('dictionaries.errors.entry_create_failed', 'Failed to create dictionary entry') })\n }\n const response = NextResponse.json({\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n position: entry.position ?? 0,\n isDefault: entry.isDefault ?? false,\n createdAt: entry.createdAt,\n updatedAt: entry.updatedAt,\n }, { status: 201 })\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 ?? 'dictionaries.entry',\n resourceId: createdEntryId,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n console.error('[dictionaries/:id/entries.POST] Unexpected error', err)\n return NextResponse.json({ error: 'Failed to create dictionary entry' }, { status: 500 })\n }\n}\n\nconst dictionaryEntriesGetDoc: OpenApiMethodDoc = {\n summary: 'List dictionary entries',\n description: 'Returns entries for the specified dictionary ordered by its configured entry sort mode.',\n tags: [dictionariesTag],\n responses: [\n { status: 200, description: 'Dictionary entries.', schema: dictionaryEntryListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid parameters', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to load dictionary entries', schema: dictionariesErrorSchema },\n ],\n}\n\nconst dictionaryEntriesPostDoc: OpenApiMethodDoc = {\n summary: 'Create dictionary entry',\n description: 'Creates a new entry in the specified dictionary.',\n tags: [dictionariesTag],\n requestBody: {\n contentType: 'application/json',\n schema: createEntryDocSchema,\n description: 'Entry value, label, and optional presentation metadata.',\n },\n responses: [\n { status: 201, description: 'Dictionary entry created.', schema: dictionaryEntryResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to create dictionary entry', schema: dictionariesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dictionariesTag,\n summary: 'Dictionary entries collection',\n pathParams: dictionaryIdParamsSchema,\n methods: {\n GET: dictionaryEntriesGetDoc,\n POST: dictionaryEntriesPostDoc,\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,YAAY,uBAAuB;AAC5C,SAAS,uCAAuC;AAChD,SAAS,mCAAmC;AAC5C,SAAS,eAAe,uBAAuB;AAE/C,SAAS,kCAAkC;AAE3C;AAAA,EACE,+BAA+B;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,uBAAuB,0BAA0B;AAC1D;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEP,MAAM,eAAe,EAAE,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAEjE,eAAe,eACb,SACA,IACA,UAAwC,CAAC,GACzC;AACA,QAAM,EAAE,iBAAiB,MAAM,IAAI;AACnC,MAAI,CAAC,kBAAkB,CAAC,QAAQ,gBAAgB;AAC9C,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,QAAQ,UAAU,6CAA6C,kCAAkC,EAAE,CAAC;AAAA,EAC5I;AACA,QAAM,aAAa;AAAA,IACjB;AAAA,IACA,UAAU,QAAQ;AAAA,IAClB,WAAW;AAAA,EACb;AACA,QAAM,SAAS,iBACX;AAAA,IACE,GAAG;AAAA,IACH,GAAI,QAAQ,wBAAwB,SAChC,EAAE,gBAAgB,EAAE,KAAK,QAAQ,wBAAwB,EAAE,IAC3D,CAAC;AAAA,EACP,IACA;AAAA,IACE,GAAG;AAAA,IACH,gBAAgB,QAAQ;AAAA,EAC1B;AACJ,QAAM,aAAa,MAAM,QAAQ,GAAG,QAAQ,YAAY,MAAM;AAC9D,MAAI,CAAC,YAAY;AACf,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,QAAQ,UAAU,iCAAiC,sBAAsB,EAAE,CAAC;AAAA,EACpH;AACA,SAAO;AACT;AAEO,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AAAA,EACjE,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,qBAAqB,EAAE;AACtE;AAEA,eAAsB,IAAI,KAAc,KAA6C;AACnF,MAAI;AACF,UAAM,UAAU,MAAM,gCAAgC,GAAG;AACzD,QAAI,CAAC,QAAQ,MAAM;AACjB,aAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrE;AACA,UAAM,EAAE,aAAa,IAAI,aAAa,MAAM,EAAE,cAAc,IAAI,QAAQ,aAAa,CAAC;AACtF,UAAM,aAAa,MAAM,eAAe,SAAS,cAAc,EAAE,gBAAgB,KAAK,CAAC;AACvF,UAAM,UAAU,MAAM;AAAA,MACpB,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,QACE;AAAA,QACA,gBAAgB,WAAW;AAAA,QAC3B,UAAU,WAAW;AAAA,MACvB;AAAA,MACA,CAAC;AAAA,MACD,EAAE,UAAU,WAAW,UAAU,gBAAgB,WAAW,eAAe;AAAA,IAC7E;AACA,UAAM,gBAAgB,sBAAsB,SAAS
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport { resolveDictionariesRouteContext } from '@open-mercato/core/modules/dictionaries/api/context'\nimport { createDictionaryEntrySchema } from '@open-mercato/core/modules/dictionaries/data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { 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'\nimport {\n createDictionaryEntrySchema as createEntryDocSchema,\n dictionaryEntryListResponseSchema,\n dictionaryEntryResponseSchema,\n dictionaryIdParamsSchema,\n dictionariesErrorSchema,\n dictionariesTag,\n} from '../../openapi'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n buildCollectionTags,\n buildRecordTag,\n isCrudCacheEnabled,\n resolveCrudCache,\n} from '@open-mercato/shared/lib/crud/cache'\nimport {\n resolveDictionaryEntrySortMode,\n sortDictionaryEntries,\n} from '@open-mercato/core/modules/dictionaries/lib/entrySort'\n\nconst paramsSchema = z.object({ dictionaryId: z.string().uuid() })\n\nconst DICTIONARY_ENTRY_RESOURCE = 'dictionaries.entry'\nconst DICTIONARY_DEFINITION_RESOURCE = 'dictionaries.dictionary'\nconst DICTIONARY_ENTRIES_TTL_MS = 5 * 60_000\n\nfunction buildEntriesCacheKey(params: {\n dictionaryId: string\n organizationId: string | null\n sortMode: string\n}): string {\n return `dictionaries:entries:${params.dictionaryId}:org=${params.organizationId ?? 'null'}:sort=${params.sortMode}`\n}\n\nasync function loadDictionary(\n context: Awaited<ReturnType<typeof resolveDictionariesRouteContext>>,\n id: string,\n options: { allowInherited?: boolean } = {},\n) {\n const { allowInherited = false } = options\n if (!allowInherited && !context.organizationId) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.organization_required', 'Organization context is required') })\n }\n const baseFilter = {\n id,\n tenantId: context.tenantId,\n deletedAt: null,\n }\n const filter = allowInherited\n ? {\n ...baseFilter,\n ...(context.readableOrganizationIds.length\n ? { organizationId: { $in: context.readableOrganizationIds } }\n : {}),\n }\n : {\n ...baseFilter,\n organizationId: context.organizationId,\n }\n const dictionary = await context.em.findOne(Dictionary, filter)\n if (!dictionary) {\n throw new CrudHttpError(404, { error: context.translate('dictionaries.errors.not_found', 'Dictionary not found') })\n }\n return dictionary\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['dictionaries.view'] },\n POST: { requireAuth: true, requireFeatures: ['dictionaries.manage'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n if (!context.auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n const dictionary = await loadDictionary(context, dictionaryId, { allowInherited: true })\n const dictionaryTenantId = dictionary.tenantId\n const dictionaryOrgId = dictionary.organizationId ?? null\n const sortMode = resolveDictionaryEntrySortMode(dictionary.entrySortMode)\n\n // Dictionary CF selects hit this on every CrudForm open, so cache the\n // decrypted+sorted options payload. The entry writes flow through the\n // command bus (resourceKind dictionaries.entry), which already flushes the\n // matching collection tag post-commit \u2014 no new invalidation wiring needed.\n const cache = isCrudCacheEnabled() ? resolveCrudCache(context.container) : null\n const cacheKey = cache\n ? buildEntriesCacheKey({ dictionaryId, organizationId: dictionaryOrgId, sortMode })\n : null\n\n if (cache && cacheKey) {\n const cached = await runWithCacheTenant(dictionaryTenantId, () => cache.get(cacheKey))\n if (cached) {\n return NextResponse.json(cached)\n }\n }\n\n const entries = await findWithDecryption(\n context.em,\n DictionaryEntry,\n {\n dictionary,\n organizationId: dictionary.organizationId,\n tenantId: dictionary.tenantId,\n },\n {},\n { tenantId: dictionary.tenantId, organizationId: dictionary.organizationId },\n )\n const sortedEntries = sortDictionaryEntries(entries, sortMode)\n\n const payload = {\n items: sortedEntries.map((entry) => ({\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n position: entry.position ?? 0,\n isDefault: entry.isDefault ?? false,\n createdAt: entry.createdAt,\n updatedAt: entry.updatedAt,\n })),\n }\n\n if (cache && cacheKey) {\n try {\n await runWithCacheTenant(dictionaryTenantId, () =>\n cache.set(cacheKey, payload, {\n ttl: DICTIONARY_ENTRIES_TTL_MS,\n tags: [\n ...buildCollectionTags(DICTIONARY_ENTRY_RESOURCE, dictionaryTenantId, [dictionaryOrgId]),\n buildRecordTag(DICTIONARY_DEFINITION_RESOURCE, dictionaryTenantId, dictionaryId),\n ],\n }),\n )\n } catch {}\n }\n\n return NextResponse.json(payload)\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n console.error('[dictionaries/:id/entries.GET] Unexpected error', err)\n return NextResponse.json({ error: 'Failed to load dictionary entries' }, { status: 500 })\n }\n}\n\nexport async function POST(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n if (!context.auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n const payload = createDictionaryEntrySchema.parse(await req.json().catch(() => ({})))\n // These nested routes do not use makeCrudRoute, so we invoke the command bus directly.\n const commandBus = (context.container.resolve('commandBus') as CommandBus)\n const { result, logEntry } = await commandBus.execute('dictionaries.entries.create', {\n input: { ...payload, dictionaryId },\n ctx: context.ctx,\n })\n const createResult = (result ?? {}) as { entryId?: string | null }\n const createdEntryId = typeof createResult.entryId === 'string' ? createResult.entryId : null\n if (!createdEntryId) {\n throw new CrudHttpError(500, { error: context.translate('dictionaries.errors.entry_create_failed', 'Failed to create dictionary entry') })\n }\n const entry = await findOneWithDecryption(\n context.em.fork(),\n DictionaryEntry,\n createdEntryId,\n { populate: ['dictionary'] },\n { tenantId: context.auth.tenantId ?? null, organizationId: context.auth.orgId ?? null },\n )\n if (!entry) {\n throw new CrudHttpError(500, { error: context.translate('dictionaries.errors.entry_create_failed', 'Failed to create dictionary entry') })\n }\n const response = NextResponse.json({\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n position: entry.position ?? 0,\n isDefault: entry.isDefault ?? false,\n createdAt: entry.createdAt,\n updatedAt: entry.updatedAt,\n }, { status: 201 })\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 ?? 'dictionaries.entry',\n resourceId: createdEntryId,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n console.error('[dictionaries/:id/entries.POST] Unexpected error', err)\n return NextResponse.json({ error: 'Failed to create dictionary entry' }, { status: 500 })\n }\n}\n\nconst dictionaryEntriesGetDoc: OpenApiMethodDoc = {\n summary: 'List dictionary entries',\n description: 'Returns entries for the specified dictionary ordered by its configured entry sort mode.',\n tags: [dictionariesTag],\n responses: [\n { status: 200, description: 'Dictionary entries.', schema: dictionaryEntryListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid parameters', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to load dictionary entries', schema: dictionariesErrorSchema },\n ],\n}\n\nconst dictionaryEntriesPostDoc: OpenApiMethodDoc = {\n summary: 'Create dictionary entry',\n description: 'Creates a new entry in the specified dictionary.',\n tags: [dictionariesTag],\n requestBody: {\n contentType: 'application/json',\n schema: createEntryDocSchema,\n description: 'Entry value, label, and optional presentation metadata.',\n },\n responses: [\n { status: 201, description: 'Dictionary entry created.', schema: dictionaryEntryResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to create dictionary entry', schema: dictionariesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dictionariesTag,\n summary: 'Dictionary entries collection',\n pathParams: dictionaryIdParamsSchema,\n methods: {\n GET: dictionaryEntriesGetDoc,\n POST: dictionaryEntriesPostDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,6 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import { FieldRegistry } from "@open-mercato/ui/backend/fields/registry";
|
|
5
5
|
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
6
6
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
7
|
+
import { Checkbox } from "@open-mercato/ui/primitives/checkbox";
|
|
7
8
|
import {
|
|
8
9
|
Select,
|
|
9
10
|
SelectContent,
|
|
@@ -36,7 +37,7 @@ function DictionaryDefaultSelector({
|
|
|
36
37
|
}
|
|
37
38
|
),
|
|
38
39
|
isLoading ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("dictionaries.customFields.loading", "Loading dictionaries\u2026") }) : null,
|
|
39
|
-
isStale ? /* @__PURE__ */ jsx("p", { className: "text-xs text-
|
|
40
|
+
isStale ? /* @__PURE__ */ jsx("p", { className: "text-xs text-status-warning-text", children: t("dictionaries.customFields.defaultValueStale", "Default entry not found \u2014 it may have been deleted or renamed.") }) : null
|
|
40
41
|
] });
|
|
41
42
|
}
|
|
42
43
|
function DictionaryFieldDefEditor({ def, onChange }) {
|
|
@@ -44,6 +45,7 @@ function DictionaryFieldDefEditor({ def, onChange }) {
|
|
|
44
45
|
const [items, setItems] = React.useState([]);
|
|
45
46
|
const [loading, setLoading] = React.useState(false);
|
|
46
47
|
const [error, setError] = React.useState(null);
|
|
48
|
+
const inlineCreateId = React.useId();
|
|
47
49
|
const selectedId = typeof def?.configJson?.dictionaryId === "string" ? def?.configJson?.dictionaryId : "";
|
|
48
50
|
const inlineCreate = def?.configJson?.dictionaryInlineCreate !== false;
|
|
49
51
|
React.useEffect(() => {
|
|
@@ -106,24 +108,31 @@ function DictionaryFieldDefEditor({ def, onChange }) {
|
|
|
106
108
|
}
|
|
107
109
|
),
|
|
108
110
|
loading ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("dictionaries.customFields.loading", "Loading dictionaries\u2026") }) : null,
|
|
109
|
-
error ? /* @__PURE__ */ jsx("p", { className: "text-xs text-
|
|
111
|
+
error ? /* @__PURE__ */ jsx("p", { className: "text-xs text-status-error-text", children: error }) : null,
|
|
110
112
|
!loading && !error && items.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("dictionaries.customFields.empty", "No dictionaries available yet. Create one first.") }) : null
|
|
111
113
|
] }),
|
|
112
114
|
selectedId ? /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2 rounded bg-background/80 px-2 py-1 text-xs text-muted-foreground", children: [
|
|
113
115
|
/* @__PURE__ */ jsx("span", { children: t("dictionaries.customFields.selectedHint", "Entries from this dictionary populate the field.") }),
|
|
114
116
|
/* @__PURE__ */ jsx("a", { href: manageHref, className: "font-medium text-primary hover:underline", target: "_blank", rel: "noreferrer", children: t("dictionaries.customFields.manageLink", "Manage dictionaries") })
|
|
115
117
|
] }) : null,
|
|
116
|
-
/* @__PURE__ */ jsxs("
|
|
118
|
+
/* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-2 text-xs", children: [
|
|
117
119
|
/* @__PURE__ */ jsx(
|
|
118
|
-
|
|
120
|
+
Checkbox,
|
|
119
121
|
{
|
|
120
|
-
|
|
122
|
+
id: inlineCreateId,
|
|
121
123
|
checked: inlineCreate,
|
|
122
|
-
|
|
124
|
+
onCheckedChange: (checked) => onChange({ dictionaryInlineCreate: checked === true }),
|
|
123
125
|
disabled: !selectedId
|
|
124
126
|
}
|
|
125
127
|
),
|
|
126
|
-
|
|
128
|
+
/* @__PURE__ */ jsx(
|
|
129
|
+
"label",
|
|
130
|
+
{
|
|
131
|
+
htmlFor: inlineCreateId,
|
|
132
|
+
className: selectedId ? "cursor-pointer select-none" : "cursor-not-allowed opacity-60",
|
|
133
|
+
children: t("dictionaries.customFields.allowInlineCreate", "Allow inline creation inside forms")
|
|
134
|
+
}
|
|
135
|
+
)
|
|
127
136
|
] }),
|
|
128
137
|
selectedId ? /* @__PURE__ */ jsx(
|
|
129
138
|
DictionaryDefaultSelector,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/dictionaries/fields/dictionary.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { CrudCustomFieldRenderProps } from '@open-mercato/ui/backend/CrudForm'\nimport { FieldRegistry } from '@open-mercato/ui/backend/fields/registry'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { DictionarySelectControl } from '../components/DictionarySelectControl'\nimport { useDictionaryEntries } from '../components/hooks/useDictionaryEntries'\n\ntype DictionaryFieldDefinition = {\n dictionaryId?: string\n dictionaryInlineCreate?: boolean\n defaultValue?: string\n}\n\ntype Props = CrudCustomFieldRenderProps & { def?: DictionaryFieldDefinition }\n\ntype DictionarySummary = {\n id: string\n name: string\n key: string\n isActive: boolean\n}\n\nfunction DictionaryDefaultSelector({\n dictionaryId,\n defaultValue,\n onChange,\n}: {\n dictionaryId: string\n defaultValue: string\n onChange: (value: string) => void\n}) {\n const t = useT()\n const { data, isLoading } = useDictionaryEntries(dictionaryId)\n const entries = data?.entries ?? []\n const isStale = defaultValue && entries.length > 0 && !entries.some((e) => e.value === defaultValue)\n\n return (\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium text-muted-foreground\">\n {t('dictionaries.customFields.defaultValue', 'Default value')}\n </label>\n <Select\n value={defaultValue ?? ''}\n onValueChange={(next) => onChange(next ?? '')}\n >\n <SelectTrigger size=\"sm\">\n <SelectValue placeholder={t('dictionaries.customFields.defaultValueNone', 'No default')} />\n </SelectTrigger>\n <SelectContent>\n {entries.map((entry) => (\n <SelectItem key={entry.value} value={entry.value}>\n {entry.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {isLoading ? (\n <p className=\"text-xs text-muted-foreground\">\n {t('dictionaries.customFields.loading', 'Loading dictionaries\u2026')}\n </p>\n ) : null}\n {isStale ? (\n <p className=\"text-xs text-
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { CrudCustomFieldRenderProps } from '@open-mercato/ui/backend/CrudForm'\nimport { FieldRegistry } from '@open-mercato/ui/backend/fields/registry'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { DictionarySelectControl } from '../components/DictionarySelectControl'\nimport { useDictionaryEntries } from '../components/hooks/useDictionaryEntries'\n\ntype DictionaryFieldDefinition = {\n dictionaryId?: string\n dictionaryInlineCreate?: boolean\n defaultValue?: string\n}\n\ntype Props = CrudCustomFieldRenderProps & { def?: DictionaryFieldDefinition }\n\ntype DictionarySummary = {\n id: string\n name: string\n key: string\n isActive: boolean\n}\n\nfunction DictionaryDefaultSelector({\n dictionaryId,\n defaultValue,\n onChange,\n}: {\n dictionaryId: string\n defaultValue: string\n onChange: (value: string) => void\n}) {\n const t = useT()\n const { data, isLoading } = useDictionaryEntries(dictionaryId)\n const entries = data?.entries ?? []\n const isStale = defaultValue && entries.length > 0 && !entries.some((e) => e.value === defaultValue)\n\n return (\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium text-muted-foreground\">\n {t('dictionaries.customFields.defaultValue', 'Default value')}\n </label>\n <Select\n value={defaultValue ?? ''}\n onValueChange={(next) => onChange(next ?? '')}\n >\n <SelectTrigger size=\"sm\">\n <SelectValue placeholder={t('dictionaries.customFields.defaultValueNone', 'No default')} />\n </SelectTrigger>\n <SelectContent>\n {entries.map((entry) => (\n <SelectItem key={entry.value} value={entry.value}>\n {entry.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {isLoading ? (\n <p className=\"text-xs text-muted-foreground\">\n {t('dictionaries.customFields.loading', 'Loading dictionaries\u2026')}\n </p>\n ) : null}\n {isStale ? (\n <p className=\"text-xs text-status-warning-text\">\n {t('dictionaries.customFields.defaultValueStale', 'Default entry not found \u2014 it may have been deleted or renamed.')}\n </p>\n ) : null}\n </div>\n )\n}\n\nfunction DictionaryFieldDefEditor({ def, onChange }: { def: { configJson?: DictionaryFieldDefinition } | undefined; onChange: (patch: Partial<DictionaryFieldDefinition>) => void }) {\n const t = useT()\n const [items, setItems] = React.useState<DictionarySummary[]>([])\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const inlineCreateId = React.useId()\n const selectedId = typeof def?.configJson?.dictionaryId === 'string' ? def?.configJson?.dictionaryId : ''\n const inlineCreate = def?.configJson?.dictionaryInlineCreate !== false\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setLoading(true)\n setError(null)\n try {\n const call = await apiCall<{ items?: unknown[]; error?: string }>(\n '/api/dictionaries?includeInactive=true',\n )\n if (!call.ok) {\n const message =\n typeof call.result?.error === 'string' ? call.result.error : 'Failed to load dictionaries'\n throw new Error(message)\n }\n const entries = Array.isArray(call.result?.items) ? call.result!.items : []\n if (!cancelled) {\n setItems(\n entries.map((entry: any) => ({\n id: String(entry.id),\n name: typeof entry.name === 'string' && entry.name.trim().length ? entry.name : String(entry.key ?? entry.id),\n key: typeof entry.key === 'string' ? entry.key : '',\n isActive: entry.isActive !== false,\n })),\n )\n }\n } catch (err) {\n if (!cancelled) {\n console.error('Failed to load dictionaries list', err)\n setError(t('dictionaries.customFields.errorLoad', 'Failed to load dictionaries.'))\n }\n } finally {\n if (!cancelled) {\n setLoading(false)\n }\n }\n }\n load().catch(() => {})\n return () => {\n cancelled = true\n }\n }, [t])\n\n const manageHref = '/backend/config/dictionaries'\n\n return (\n <div className=\"mt-3 space-y-3 rounded border border-dashed border-muted-foreground/40 bg-muted/30 p-3\">\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium text-muted-foreground\">\n {t('dictionaries.customFields.dictionaryLabel', 'Dictionary source')}\n </label>\n <Select\n value={selectedId ?? ''}\n onValueChange={(next) => onChange({ dictionaryId: next || undefined })}\n >\n <SelectTrigger size=\"sm\">\n <SelectValue placeholder={t('dictionaries.customFields.dictionaryPlaceholder', 'Select a dictionary')} />\n </SelectTrigger>\n <SelectContent>\n {items.map((item) => (\n <SelectItem key={item.id} value={item.id}>\n {item.name}\n {item.isActive ? '' : ` (${t('dictionaries.customFields.inactive', 'inactive')})`}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {loading ? (\n <p className=\"text-xs text-muted-foreground\">\n {t('dictionaries.customFields.loading', 'Loading dictionaries\u2026')}\n </p>\n ) : null}\n {error ? <p className=\"text-xs text-status-error-text\">{error}</p> : null}\n {!loading && !error && items.length === 0 ? (\n <p className=\"text-xs text-muted-foreground\">\n {t('dictionaries.customFields.empty', 'No dictionaries available yet. Create one first.')}\n </p>\n ) : null}\n </div>\n {selectedId ? (\n <div className=\"flex flex-wrap items-center justify-between gap-2 rounded bg-background/80 px-2 py-1 text-xs text-muted-foreground\">\n <span>{t('dictionaries.customFields.selectedHint', 'Entries from this dictionary populate the field.')}</span>\n <a href={manageHref} className=\"font-medium text-primary hover:underline\" target=\"_blank\" rel=\"noreferrer\">\n {t('dictionaries.customFields.manageLink', 'Manage dictionaries')}\n </a>\n </div>\n ) : null}\n <div className=\"inline-flex items-center gap-2 text-xs\">\n <Checkbox\n id={inlineCreateId}\n checked={inlineCreate}\n onCheckedChange={(checked) => onChange({ dictionaryInlineCreate: checked === true })}\n disabled={!selectedId}\n />\n <label\n htmlFor={inlineCreateId}\n className={selectedId ? 'cursor-pointer select-none' : 'cursor-not-allowed opacity-60'}\n >\n {t('dictionaries.customFields.allowInlineCreate', 'Allow inline creation inside forms')}\n </label>\n </div>\n {selectedId ? (\n <DictionaryDefaultSelector\n dictionaryId={selectedId}\n defaultValue={typeof def?.configJson?.defaultValue === 'string' ? def.configJson.defaultValue : ''}\n onChange={(value) => onChange({ defaultValue: value || undefined })}\n />\n ) : null}\n </div>\n )\n}\n\nfunction DictionaryFieldInput({ value, setValue, disabled, def }: Props) {\n const t = useT()\n const dictionaryId = def?.dictionaryId\n if (!dictionaryId) {\n return (\n <div className=\"rounded border border-dashed p-3 text-sm text-muted-foreground\">\n {t('dictionaries.config.entries.error.load', 'Failed to load dictionary entries.')}\n </div>\n )\n }\n const normalizedValue = typeof value === 'string' ? value : Array.isArray(value) ? String(value[0] ?? '') : undefined\n return (\n <DictionarySelectControl\n dictionaryId={dictionaryId}\n value={normalizedValue ?? ''}\n onChange={(next) => setValue(next ?? undefined)}\n allowInlineCreate={def?.dictionaryInlineCreate !== false}\n disabled={disabled}\n />\n )\n}\n\nFieldRegistry.register('dictionary', {\n input: DictionaryFieldInput,\n defEditor: (props) => <DictionaryFieldDefEditor {...props} />,\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,17 +1,49 @@
|
|
|
1
|
+
import { runWithCacheTenant } from "@open-mercato/cache";
|
|
2
|
+
import {
|
|
3
|
+
buildCollectionTags,
|
|
4
|
+
isCrudCacheEnabled,
|
|
5
|
+
resolveCrudCache
|
|
6
|
+
} from "@open-mercato/shared/lib/crud/cache";
|
|
1
7
|
import { Notification } from "../../data/entities.js";
|
|
2
8
|
import { unreadCountResponseSchema } from "../openapi.js";
|
|
3
9
|
import { resolveNotificationContext } from "../../lib/routeHelpers.js";
|
|
4
10
|
const metadata = {
|
|
5
11
|
GET: { requireAuth: true }
|
|
6
12
|
};
|
|
13
|
+
const UNREAD_COUNT_RESOURCE = "notifications.notification";
|
|
14
|
+
const UNREAD_COUNT_TTL_MS = 1e4;
|
|
15
|
+
function buildUnreadCountCacheKey(userId) {
|
|
16
|
+
return `notifications:unread-count:u=${userId}`;
|
|
17
|
+
}
|
|
7
18
|
async function GET(req) {
|
|
8
19
|
const { scope, ctx } = await resolveNotificationContext(req);
|
|
9
20
|
const em = ctx.container.resolve("em");
|
|
21
|
+
const userId = scope.userId;
|
|
22
|
+
const cache = userId && isCrudCacheEnabled() ? resolveCrudCache(ctx.container) : null;
|
|
23
|
+
const cacheKey = cache && userId ? buildUnreadCountCacheKey(userId) : null;
|
|
24
|
+
if (cache && cacheKey) {
|
|
25
|
+
const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey));
|
|
26
|
+
if (typeof cached === "number") {
|
|
27
|
+
return Response.json({ unreadCount: cached });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
10
30
|
const count = await em.count(Notification, {
|
|
11
|
-
recipientUserId:
|
|
31
|
+
recipientUserId: userId,
|
|
12
32
|
tenantId: scope.tenantId,
|
|
13
33
|
status: "unread"
|
|
14
34
|
});
|
|
35
|
+
if (cache && cacheKey) {
|
|
36
|
+
try {
|
|
37
|
+
await runWithCacheTenant(
|
|
38
|
+
scope.tenantId,
|
|
39
|
+
() => cache.set(cacheKey, count, {
|
|
40
|
+
ttl: UNREAD_COUNT_TTL_MS,
|
|
41
|
+
tags: buildCollectionTags(UNREAD_COUNT_RESOURCE, scope.tenantId, [null])
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
}
|
|
15
47
|
return Response.json({ unreadCount: count });
|
|
16
48
|
}
|
|
17
49
|
const openApi = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/notifications/api/unread-count/route.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { Notification } from '../../data/entities'\nimport { unreadCountResponseSchema } from '../openapi'\nimport { resolveNotificationContext } from '../../lib/routeHelpers'\n\nexport const metadata = {\n GET: { requireAuth: true },\n}\n\nexport async function GET(req: Request) {\n const { scope, ctx } = await resolveNotificationContext(req)\n const em = ctx.container.resolve('em') as EntityManager\n\n const count = await em.count(Notification, {\n recipientUserId:
|
|
5
|
-
"mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,iCAAiC;AAC1C,SAAS,kCAAkC;AAEpC,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,KAAK;AAC3B;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,EAAE,OAAO,IAAI,IAAI,MAAM,2BAA2B,GAAG;AAC3D,QAAM,KAAK,IAAI,UAAU,QAAQ,IAAI;AAErC,QAAM,QAAQ,MAAM,GAAG,MAAM,cAAc;AAAA,IACzC,iBAAiB
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport {\n buildCollectionTags,\n isCrudCacheEnabled,\n resolveCrudCache,\n} from '@open-mercato/shared/lib/crud/cache'\nimport { Notification } from '../../data/entities'\nimport { unreadCountResponseSchema } from '../openapi'\nimport { resolveNotificationContext } from '../../lib/routeHelpers'\n\nexport const metadata = {\n GET: { requireAuth: true },\n}\n\nconst UNREAD_COUNT_RESOURCE = 'notifications.notification'\nconst UNREAD_COUNT_TTL_MS = 10_000\n\nfunction buildUnreadCountCacheKey(userId: string): string {\n return `notifications:unread-count:u=${userId}`\n}\n\nexport async function GET(req: Request) {\n const { scope, ctx } = await resolveNotificationContext(req)\n const em = ctx.container.resolve('em') as EntityManager\n\n const userId = scope.userId\n const cache = userId && isCrudCacheEnabled() ? resolveCrudCache(ctx.container) : null\n const cacheKey = cache && userId ? buildUnreadCountCacheKey(userId) : null\n\n if (cache && cacheKey) {\n const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey))\n if (typeof cached === 'number') {\n return Response.json({ unreadCount: cached })\n }\n }\n\n const count = await em.count(Notification, {\n recipientUserId: userId,\n tenantId: scope.tenantId,\n status: 'unread',\n })\n\n if (cache && cacheKey) {\n try {\n await runWithCacheTenant(scope.tenantId, () =>\n cache.set(cacheKey, count, {\n ttl: UNREAD_COUNT_TTL_MS,\n tags: buildCollectionTags(UNREAD_COUNT_RESOURCE, scope.tenantId, [null]),\n }),\n )\n } catch {}\n }\n\n return Response.json({ unreadCount: count })\n}\n\nexport const openApi = {\n GET: {\n summary: 'Get unread notification count',\n tags: ['Notifications'],\n responses: {\n 200: {\n description: 'Unread count',\n content: {\n 'application/json': {\n schema: unreadCountResponseSchema,\n },\n },\n },\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAC7B,SAAS,iCAAiC;AAC1C,SAAS,kCAAkC;AAEpC,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,KAAK;AAC3B;AAEA,MAAM,wBAAwB;AAC9B,MAAM,sBAAsB;AAE5B,SAAS,yBAAyB,QAAwB;AACxD,SAAO,gCAAgC,MAAM;AAC/C;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,EAAE,OAAO,IAAI,IAAI,MAAM,2BAA2B,GAAG;AAC3D,QAAM,KAAK,IAAI,UAAU,QAAQ,IAAI;AAErC,QAAM,SAAS,MAAM;AACrB,QAAM,QAAQ,UAAU,mBAAmB,IAAI,iBAAiB,IAAI,SAAS,IAAI;AACjF,QAAM,WAAW,SAAS,SAAS,yBAAyB,MAAM,IAAI;AAEtE,MAAI,SAAS,UAAU;AACrB,UAAM,SAAS,MAAM,mBAAmB,MAAM,UAAU,MAAM,MAAM,IAAI,QAAQ,CAAC;AACjF,QAAI,OAAO,WAAW,UAAU;AAC9B,aAAO,SAAS,KAAK,EAAE,aAAa,OAAO,CAAC;AAAA,IAC9C;AAAA,EACF;AAEA,QAAM,QAAQ,MAAM,GAAG,MAAM,cAAc;AAAA,IACzC,iBAAiB;AAAA,IACjB,UAAU,MAAM;AAAA,IAChB,QAAQ;AAAA,EACV,CAAC;AAED,MAAI,SAAS,UAAU;AACrB,QAAI;AACF,YAAM;AAAA,QAAmB,MAAM;AAAA,QAAU,MACvC,MAAM,IAAI,UAAU,OAAO;AAAA,UACzB,KAAK;AAAA,UACL,MAAM,oBAAoB,uBAAuB,MAAM,UAAU,CAAC,IAAI,CAAC;AAAA,QACzE,CAAC;AAAA,MACH;AAAA,IACF,QAAQ;AAAA,IAAC;AAAA,EACX;AAEA,SAAO,SAAS,KAAK,EAAE,aAAa,MAAM,CAAC;AAC7C;AAEO,MAAM,UAAU;AAAA,EACrB,KAAK;AAAA,IACH,SAAS;AAAA,IACT,MAAM,CAAC,eAAe;AAAA,IACtB,WAAW;AAAA,MACT,KAAK;AAAA,QACH,aAAa;AAAA,QACb,SAAS;AAAA,UACP,oBAAoB;AAAA,YAClB,QAAQ;AAAA,UACV;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.5751.1.39143f001b",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -245,16 +245,16 @@
|
|
|
245
245
|
"zod": "^4.4.3"
|
|
246
246
|
},
|
|
247
247
|
"peerDependencies": {
|
|
248
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
249
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
250
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
248
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.5751.1.39143f001b",
|
|
249
|
+
"@open-mercato/shared": "0.6.6-develop.5751.1.39143f001b",
|
|
250
|
+
"@open-mercato/ui": "0.6.6-develop.5751.1.39143f001b",
|
|
251
251
|
"react": "^19.0.0",
|
|
252
252
|
"react-dom": "^19.0.0"
|
|
253
253
|
},
|
|
254
254
|
"devDependencies": {
|
|
255
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
256
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
257
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
255
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.5751.1.39143f001b",
|
|
256
|
+
"@open-mercato/shared": "0.6.6-develop.5751.1.39143f001b",
|
|
257
|
+
"@open-mercato/ui": "0.6.6-develop.5751.1.39143f001b",
|
|
258
258
|
"@testing-library/dom": "^10.4.1",
|
|
259
259
|
"@testing-library/jest-dom": "^6.9.1",
|
|
260
260
|
"@testing-library/react": "^16.3.1",
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
|
+
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
4
5
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
5
6
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
6
7
|
import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
|
|
8
|
+
import {
|
|
9
|
+
buildCollectionTags,
|
|
10
|
+
isCrudCacheEnabled,
|
|
11
|
+
resolveCrudCache,
|
|
12
|
+
} from '@open-mercato/shared/lib/crud/cache'
|
|
7
13
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
8
14
|
import { Currency } from '../../../data/entities'
|
|
9
15
|
|
|
@@ -11,6 +17,17 @@ export const metadata = {
|
|
|
11
17
|
GET: { requireAuth: true, requireFeatures: ['currencies.view'] },
|
|
12
18
|
}
|
|
13
19
|
|
|
20
|
+
const CURRENCY_OPTIONS_RESOURCE = 'currencies.currency'
|
|
21
|
+
const CURRENCY_OPTIONS_TTL_MS = 5 * 60_000
|
|
22
|
+
|
|
23
|
+
function buildOptionsCacheKey(params: {
|
|
24
|
+
orgId: string | null
|
|
25
|
+
includeInactive: boolean
|
|
26
|
+
limit: number
|
|
27
|
+
}): string {
|
|
28
|
+
return `currencies:options:org=${params.orgId ?? 'null'}:active=${params.includeInactive ? 'all' : 'active'}:limit=${params.limit}`
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
const optionsQuerySchema = z.object({
|
|
15
32
|
q: z.string().optional(),
|
|
16
33
|
query: z.string().optional(),
|
|
@@ -47,15 +64,35 @@ export async function GET(req: Request) {
|
|
|
47
64
|
|
|
48
65
|
const { q, query, search, includeInactive, limit } = parsed.data
|
|
49
66
|
const searchTerm = (q ?? query ?? search ?? '').trim()
|
|
67
|
+
const tenantId = auth.tenantId
|
|
68
|
+
const orgId = auth.orgId ?? null
|
|
69
|
+
const includeInactiveFlag = includeInactive === 'true'
|
|
70
|
+
|
|
71
|
+
// Only the unfiltered bootstrap call (no ILIKE search term) is the hot path
|
|
72
|
+
// worth caching; search variants would multiply key cardinality with little
|
|
73
|
+
// hit-rate benefit, so they always read straight through.
|
|
74
|
+
const cache =
|
|
75
|
+
isCrudCacheEnabled() && !searchTerm ? resolveCrudCache(container) : null
|
|
76
|
+
const cacheKey = cache
|
|
77
|
+
? buildOptionsCacheKey({ orgId, includeInactive: includeInactiveFlag, limit })
|
|
78
|
+
: null
|
|
79
|
+
|
|
80
|
+
if (cache && cacheKey) {
|
|
81
|
+
const cached = await runWithCacheTenant(tenantId, () => cache.get(cacheKey))
|
|
82
|
+
if (cached) {
|
|
83
|
+
return NextResponse.json(cached)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
50
87
|
const filter: any = {
|
|
51
|
-
tenantId
|
|
88
|
+
tenantId,
|
|
52
89
|
deletedAt: null,
|
|
53
90
|
}
|
|
54
|
-
if (
|
|
55
|
-
filter.organizationId =
|
|
91
|
+
if (orgId) {
|
|
92
|
+
filter.organizationId = orgId
|
|
56
93
|
}
|
|
57
94
|
|
|
58
|
-
if (
|
|
95
|
+
if (!includeInactiveFlag) {
|
|
59
96
|
filter.isActive = true
|
|
60
97
|
}
|
|
61
98
|
|
|
@@ -77,7 +114,20 @@ export async function GET(req: Request) {
|
|
|
77
114
|
label: `${currency.code} - ${currency.name}`,
|
|
78
115
|
}))
|
|
79
116
|
|
|
80
|
-
|
|
117
|
+
const payload = { items }
|
|
118
|
+
|
|
119
|
+
if (cache && cacheKey) {
|
|
120
|
+
try {
|
|
121
|
+
await runWithCacheTenant(tenantId, () =>
|
|
122
|
+
cache.set(cacheKey, payload, {
|
|
123
|
+
ttl: CURRENCY_OPTIONS_TTL_MS,
|
|
124
|
+
tags: buildCollectionTags(CURRENCY_OPTIONS_RESOURCE, tenantId, [orgId]),
|
|
125
|
+
}),
|
|
126
|
+
)
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return NextResponse.json(payload)
|
|
81
131
|
}
|
|
82
132
|
|
|
83
133
|
const optionsResponseSchema = z.object({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { z } from 'zod'
|
|
3
|
+
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
3
4
|
import { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'
|
|
4
5
|
import { resolveDictionariesRouteContext } from '@open-mercato/core/modules/dictionaries/api/context'
|
|
5
6
|
import { createDictionaryEntrySchema } from '@open-mercato/core/modules/dictionaries/data/validators'
|
|
@@ -16,6 +17,12 @@ import {
|
|
|
16
17
|
dictionariesTag,
|
|
17
18
|
} from '../../openapi'
|
|
18
19
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
20
|
+
import {
|
|
21
|
+
buildCollectionTags,
|
|
22
|
+
buildRecordTag,
|
|
23
|
+
isCrudCacheEnabled,
|
|
24
|
+
resolveCrudCache,
|
|
25
|
+
} from '@open-mercato/shared/lib/crud/cache'
|
|
19
26
|
import {
|
|
20
27
|
resolveDictionaryEntrySortMode,
|
|
21
28
|
sortDictionaryEntries,
|
|
@@ -23,6 +30,18 @@ import {
|
|
|
23
30
|
|
|
24
31
|
const paramsSchema = z.object({ dictionaryId: z.string().uuid() })
|
|
25
32
|
|
|
33
|
+
const DICTIONARY_ENTRY_RESOURCE = 'dictionaries.entry'
|
|
34
|
+
const DICTIONARY_DEFINITION_RESOURCE = 'dictionaries.dictionary'
|
|
35
|
+
const DICTIONARY_ENTRIES_TTL_MS = 5 * 60_000
|
|
36
|
+
|
|
37
|
+
function buildEntriesCacheKey(params: {
|
|
38
|
+
dictionaryId: string
|
|
39
|
+
organizationId: string | null
|
|
40
|
+
sortMode: string
|
|
41
|
+
}): string {
|
|
42
|
+
return `dictionaries:entries:${params.dictionaryId}:org=${params.organizationId ?? 'null'}:sort=${params.sortMode}`
|
|
43
|
+
}
|
|
44
|
+
|
|
26
45
|
async function loadDictionary(
|
|
27
46
|
context: Awaited<ReturnType<typeof resolveDictionariesRouteContext>>,
|
|
28
47
|
id: string,
|
|
@@ -68,6 +87,26 @@ export async function GET(req: Request, ctx: { params?: { dictionaryId?: string
|
|
|
68
87
|
}
|
|
69
88
|
const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })
|
|
70
89
|
const dictionary = await loadDictionary(context, dictionaryId, { allowInherited: true })
|
|
90
|
+
const dictionaryTenantId = dictionary.tenantId
|
|
91
|
+
const dictionaryOrgId = dictionary.organizationId ?? null
|
|
92
|
+
const sortMode = resolveDictionaryEntrySortMode(dictionary.entrySortMode)
|
|
93
|
+
|
|
94
|
+
// Dictionary CF selects hit this on every CrudForm open, so cache the
|
|
95
|
+
// decrypted+sorted options payload. The entry writes flow through the
|
|
96
|
+
// command bus (resourceKind dictionaries.entry), which already flushes the
|
|
97
|
+
// matching collection tag post-commit — no new invalidation wiring needed.
|
|
98
|
+
const cache = isCrudCacheEnabled() ? resolveCrudCache(context.container) : null
|
|
99
|
+
const cacheKey = cache
|
|
100
|
+
? buildEntriesCacheKey({ dictionaryId, organizationId: dictionaryOrgId, sortMode })
|
|
101
|
+
: null
|
|
102
|
+
|
|
103
|
+
if (cache && cacheKey) {
|
|
104
|
+
const cached = await runWithCacheTenant(dictionaryTenantId, () => cache.get(cacheKey))
|
|
105
|
+
if (cached) {
|
|
106
|
+
return NextResponse.json(cached)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
71
110
|
const entries = await findWithDecryption(
|
|
72
111
|
context.em,
|
|
73
112
|
DictionaryEntry,
|
|
@@ -79,9 +118,9 @@ export async function GET(req: Request, ctx: { params?: { dictionaryId?: string
|
|
|
79
118
|
{},
|
|
80
119
|
{ tenantId: dictionary.tenantId, organizationId: dictionary.organizationId },
|
|
81
120
|
)
|
|
82
|
-
const sortedEntries = sortDictionaryEntries(entries,
|
|
121
|
+
const sortedEntries = sortDictionaryEntries(entries, sortMode)
|
|
83
122
|
|
|
84
|
-
|
|
123
|
+
const payload = {
|
|
85
124
|
items: sortedEntries.map((entry) => ({
|
|
86
125
|
id: entry.id,
|
|
87
126
|
value: entry.value,
|
|
@@ -93,7 +132,23 @@ export async function GET(req: Request, ctx: { params?: { dictionaryId?: string
|
|
|
93
132
|
createdAt: entry.createdAt,
|
|
94
133
|
updatedAt: entry.updatedAt,
|
|
95
134
|
})),
|
|
96
|
-
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (cache && cacheKey) {
|
|
138
|
+
try {
|
|
139
|
+
await runWithCacheTenant(dictionaryTenantId, () =>
|
|
140
|
+
cache.set(cacheKey, payload, {
|
|
141
|
+
ttl: DICTIONARY_ENTRIES_TTL_MS,
|
|
142
|
+
tags: [
|
|
143
|
+
...buildCollectionTags(DICTIONARY_ENTRY_RESOURCE, dictionaryTenantId, [dictionaryOrgId]),
|
|
144
|
+
buildRecordTag(DICTIONARY_DEFINITION_RESOURCE, dictionaryTenantId, dictionaryId),
|
|
145
|
+
],
|
|
146
|
+
}),
|
|
147
|
+
)
|
|
148
|
+
} catch {}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return NextResponse.json(payload)
|
|
97
152
|
} catch (err) {
|
|
98
153
|
if (isCrudHttpError(err)) {
|
|
99
154
|
return NextResponse.json(err.body, { status: err.status })
|
|
@@ -5,6 +5,7 @@ import type { CrudCustomFieldRenderProps } from '@open-mercato/ui/backend/CrudFo
|
|
|
5
5
|
import { FieldRegistry } from '@open-mercato/ui/backend/fields/registry'
|
|
6
6
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
7
7
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
8
|
+
import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
|
|
8
9
|
import {
|
|
9
10
|
Select,
|
|
10
11
|
SelectContent,
|
|
@@ -70,7 +71,7 @@ function DictionaryDefaultSelector({
|
|
|
70
71
|
</p>
|
|
71
72
|
) : null}
|
|
72
73
|
{isStale ? (
|
|
73
|
-
<p className="text-xs text-
|
|
74
|
+
<p className="text-xs text-status-warning-text">
|
|
74
75
|
{t('dictionaries.customFields.defaultValueStale', 'Default entry not found — it may have been deleted or renamed.')}
|
|
75
76
|
</p>
|
|
76
77
|
) : null}
|
|
@@ -83,6 +84,7 @@ function DictionaryFieldDefEditor({ def, onChange }: { def: { configJson?: Dicti
|
|
|
83
84
|
const [items, setItems] = React.useState<DictionarySummary[]>([])
|
|
84
85
|
const [loading, setLoading] = React.useState(false)
|
|
85
86
|
const [error, setError] = React.useState<string | null>(null)
|
|
87
|
+
const inlineCreateId = React.useId()
|
|
86
88
|
const selectedId = typeof def?.configJson?.dictionaryId === 'string' ? def?.configJson?.dictionaryId : ''
|
|
87
89
|
const inlineCreate = def?.configJson?.dictionaryInlineCreate !== false
|
|
88
90
|
|
|
@@ -157,7 +159,7 @@ function DictionaryFieldDefEditor({ def, onChange }: { def: { configJson?: Dicti
|
|
|
157
159
|
{t('dictionaries.customFields.loading', 'Loading dictionaries…')}
|
|
158
160
|
</p>
|
|
159
161
|
) : null}
|
|
160
|
-
{error ? <p className="text-xs text-
|
|
162
|
+
{error ? <p className="text-xs text-status-error-text">{error}</p> : null}
|
|
161
163
|
{!loading && !error && items.length === 0 ? (
|
|
162
164
|
<p className="text-xs text-muted-foreground">
|
|
163
165
|
{t('dictionaries.customFields.empty', 'No dictionaries available yet. Create one first.')}
|
|
@@ -172,15 +174,20 @@ function DictionaryFieldDefEditor({ def, onChange }: { def: { configJson?: Dicti
|
|
|
172
174
|
</a>
|
|
173
175
|
</div>
|
|
174
176
|
) : null}
|
|
175
|
-
<
|
|
176
|
-
<
|
|
177
|
-
|
|
177
|
+
<div className="inline-flex items-center gap-2 text-xs">
|
|
178
|
+
<Checkbox
|
|
179
|
+
id={inlineCreateId}
|
|
178
180
|
checked={inlineCreate}
|
|
179
|
-
|
|
181
|
+
onCheckedChange={(checked) => onChange({ dictionaryInlineCreate: checked === true })}
|
|
180
182
|
disabled={!selectedId}
|
|
181
183
|
/>
|
|
182
|
-
|
|
183
|
-
|
|
184
|
+
<label
|
|
185
|
+
htmlFor={inlineCreateId}
|
|
186
|
+
className={selectedId ? 'cursor-pointer select-none' : 'cursor-not-allowed opacity-60'}
|
|
187
|
+
>
|
|
188
|
+
{t('dictionaries.customFields.allowInlineCreate', 'Allow inline creation inside forms')}
|
|
189
|
+
</label>
|
|
190
|
+
</div>
|
|
184
191
|
{selectedId ? (
|
|
185
192
|
<DictionaryDefaultSelector
|
|
186
193
|
dictionaryId={selectedId}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/core'
|
|
2
|
+
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
3
|
+
import {
|
|
4
|
+
buildCollectionTags,
|
|
5
|
+
isCrudCacheEnabled,
|
|
6
|
+
resolveCrudCache,
|
|
7
|
+
} from '@open-mercato/shared/lib/crud/cache'
|
|
2
8
|
import { Notification } from '../../data/entities'
|
|
3
9
|
import { unreadCountResponseSchema } from '../openapi'
|
|
4
10
|
import { resolveNotificationContext } from '../../lib/routeHelpers'
|
|
@@ -7,16 +13,45 @@ export const metadata = {
|
|
|
7
13
|
GET: { requireAuth: true },
|
|
8
14
|
}
|
|
9
15
|
|
|
16
|
+
const UNREAD_COUNT_RESOURCE = 'notifications.notification'
|
|
17
|
+
const UNREAD_COUNT_TTL_MS = 10_000
|
|
18
|
+
|
|
19
|
+
function buildUnreadCountCacheKey(userId: string): string {
|
|
20
|
+
return `notifications:unread-count:u=${userId}`
|
|
21
|
+
}
|
|
22
|
+
|
|
10
23
|
export async function GET(req: Request) {
|
|
11
24
|
const { scope, ctx } = await resolveNotificationContext(req)
|
|
12
25
|
const em = ctx.container.resolve('em') as EntityManager
|
|
13
26
|
|
|
27
|
+
const userId = scope.userId
|
|
28
|
+
const cache = userId && isCrudCacheEnabled() ? resolveCrudCache(ctx.container) : null
|
|
29
|
+
const cacheKey = cache && userId ? buildUnreadCountCacheKey(userId) : null
|
|
30
|
+
|
|
31
|
+
if (cache && cacheKey) {
|
|
32
|
+
const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey))
|
|
33
|
+
if (typeof cached === 'number') {
|
|
34
|
+
return Response.json({ unreadCount: cached })
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
14
38
|
const count = await em.count(Notification, {
|
|
15
|
-
recipientUserId:
|
|
39
|
+
recipientUserId: userId,
|
|
16
40
|
tenantId: scope.tenantId,
|
|
17
41
|
status: 'unread',
|
|
18
42
|
})
|
|
19
43
|
|
|
44
|
+
if (cache && cacheKey) {
|
|
45
|
+
try {
|
|
46
|
+
await runWithCacheTenant(scope.tenantId, () =>
|
|
47
|
+
cache.set(cacheKey, count, {
|
|
48
|
+
ttl: UNREAD_COUNT_TTL_MS,
|
|
49
|
+
tags: buildCollectionTags(UNREAD_COUNT_RESOURCE, scope.tenantId, [null]),
|
|
50
|
+
}),
|
|
51
|
+
)
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
|
|
20
55
|
return Response.json({ unreadCount: count })
|
|
21
56
|
}
|
|
22
57
|
|