@open-mercato/core 0.6.5-develop.5382.1.f542de69af → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/bootstrap.js +46 -6
- package/dist/bootstrap.js.map +2 -2
- package/dist/generated/entities/organization/index.js +2 -0
- package/dist/generated/entities/organization/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/crmFixtures.js +4 -0
- package/dist/helpers/integration/crmFixtures.js.map +2 -2
- package/dist/modules/attachments/api/route.js +2 -0
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/lib/access.js +18 -0
- package/dist/modules/attachments/lib/access.js.map +2 -2
- package/dist/modules/audit_logs/data/entities.js +2 -1
- package/dist/modules/audit_logs/data/entities.js.map +2 -2
- package/dist/modules/audit_logs/migrations/Migration20260611104500.js +13 -0
- package/dist/modules/audit_logs/migrations/Migration20260611104500.js.map +7 -0
- package/dist/modules/audit_logs/services/accessLogService.js +10 -0
- package/dist/modules/audit_logs/services/accessLogService.js.map +2 -2
- package/dist/modules/auth/api/admin/nav.js +9 -0
- package/dist/modules/auth/api/admin/nav.js.map +2 -2
- package/dist/modules/auth/api/login.js +4 -13
- package/dist/modules/auth/api/login.js.map +2 -2
- package/dist/modules/auth/data/entities.js +3 -1
- package/dist/modules/auth/data/entities.js.map +2 -2
- package/dist/modules/auth/lib/backendChrome.js +35 -2
- package/dist/modules/auth/lib/backendChrome.js.map +2 -2
- package/dist/modules/auth/lib/consentIntegrity.js +3 -3
- package/dist/modules/auth/lib/consentIntegrity.js.map +2 -2
- package/dist/modules/auth/migrations/Migration20260611103000.js +15 -0
- package/dist/modules/auth/migrations/Migration20260611103000.js.map +7 -0
- package/dist/modules/auth/services/authService.js +5 -3
- package/dist/modules/auth/services/authService.js.map +2 -2
- package/dist/modules/auth/services/rbacService.js +3 -2
- package/dist/modules/auth/services/rbacService.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js +0 -3
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js.map +2 -2
- package/dist/modules/customers/api/deals/route.js +43 -2
- package/dist/modules/customers/api/deals/route.js.map +2 -2
- package/dist/modules/customers/api/deals/summary/route.js +402 -0
- package/dist/modules/customers/api/deals/summary/route.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js +16 -5
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js +22 -5
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +12 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +221 -56
- package/dist/modules/customers/backend/customers/deals/page.js.map +3 -3
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +1 -1
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +18 -0
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/cli.js +15 -9
- package/dist/modules/customers/cli.js.map +2 -2
- package/dist/modules/customers/components/DealsKpiStrip.js +282 -0
- package/dist/modules/customers/components/DealsKpiStrip.js.map +7 -0
- package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js +0 -1
- package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +100 -17
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonDetailTabs.js +11 -3
- package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +1 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/kpi/PipelineStageBar.js +63 -0
- package/dist/modules/customers/components/kpi/PipelineStageBar.js.map +7 -0
- package/dist/modules/customers/lib/dealsMetrics.js +82 -0
- package/dist/modules/customers/lib/dealsMetrics.js.map +7 -0
- package/dist/modules/directory/api/organization-branding/route.js +214 -0
- package/dist/modules/directory/api/organization-branding/route.js.map +7 -0
- package/dist/modules/directory/api/organizations/route.js +7 -0
- package/dist/modules/directory/api/organizations/route.js.map +3 -3
- package/dist/modules/directory/backend/directory/branding/page.js +214 -0
- package/dist/modules/directory/backend/directory/branding/page.js.map +7 -0
- package/dist/modules/directory/backend/directory/branding/page.meta.js +26 -0
- package/dist/modules/directory/backend/directory/branding/page.meta.js.map +7 -0
- package/dist/modules/directory/commands/organizations.js +8 -1
- package/dist/modules/directory/commands/organizations.js.map +2 -2
- package/dist/modules/directory/data/entities.js +3 -0
- package/dist/modules/directory/data/entities.js.map +2 -2
- package/dist/modules/directory/data/validators.js +9 -0
- package/dist/modules/directory/data/validators.js.map +2 -2
- package/dist/modules/directory/migrations/Migration20260607222259_directory.js +13 -0
- package/dist/modules/directory/migrations/Migration20260607222259_directory.js.map +7 -0
- package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js +2 -1
- package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js.map +2 -2
- package/dist/modules/directory/utils/organizationScope.js +59 -27
- package/dist/modules/directory/utils/organizationScope.js.map +2 -2
- package/dist/modules/entities/api/definitions.batch.js +2 -1
- package/dist/modules/entities/api/definitions.batch.js.map +2 -2
- package/dist/modules/entities/api/entities.js +7 -0
- package/dist/modules/entities/api/entities.js.map +2 -2
- package/dist/modules/entities/api/records.js +26 -15
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js +14 -0
- package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/create/page.js +14 -0
- package/dist/modules/entities/backend/entities/user/[entityId]/records/create/page.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +12 -0
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
- package/dist/modules/entities/components/useRecordsEntityGuard.js +30 -0
- package/dist/modules/entities/components/useRecordsEntityGuard.js.map +7 -0
- package/dist/modules/query_index/data/entities.js +2 -1
- package/dist/modules/query_index/data/entities.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +4 -2
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/migrations/Migration20260611103000_query_index.js +16 -0
- package/dist/modules/query_index/migrations/Migration20260611103000_query_index.js.map +7 -0
- package/dist/modules/sales/commands/documents.js +7 -5
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentsTable.js +2 -1
- package/dist/modules/sales/components/documents/SalesDocumentsTable.js.map +2 -2
- package/dist/modules/sales/components/documents/salesDocumentsColumns.js +10 -0
- package/dist/modules/sales/components/documents/salesDocumentsColumns.js.map +7 -0
- package/dist/modules/staff/api/team-members.js +9 -2
- package/dist/modules/staff/api/team-members.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js +24 -1
- package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js +11 -6
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
- package/dist/modules/staff/commands/team-members.js +1 -1
- package/dist/modules/staff/commands/team-members.js.map +2 -2
- package/dist/modules/staff/components/TeamMemberForm.js +1 -1
- package/dist/modules/staff/components/TeamMemberForm.js.map +2 -2
- package/dist/modules/staff/lib/scheduleSwitch.js +23 -0
- package/dist/modules/staff/lib/scheduleSwitch.js.map +7 -0
- package/dist/modules/workflows/backend/definitions/create/page.js +1 -2
- package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js +1 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js +1 -2
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +2 -2
- package/dist/modules/workflows/components/NodeEditDialog.js +4 -13
- package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
- package/dist/modules/workflows/components/NodeEditDialogCrudForm.js +4 -13
- package/dist/modules/workflows/components/NodeEditDialogCrudForm.js.map +2 -2
- package/dist/modules/workflows/components/WorkflowGraphImpl.js +1 -4
- package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +2 -2
- package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js +2 -5
- package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js.map +2 -2
- package/generated/entities/organization/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +11 -12
- package/src/bootstrap.ts +65 -7
- package/src/helpers/integration/crmFixtures.ts +21 -1
- package/src/modules/attachments/AGENTS.md +79 -0
- package/src/modules/attachments/api/route.ts +2 -0
- package/src/modules/attachments/lib/access.ts +36 -0
- package/src/modules/audit_logs/data/entities.ts +1 -0
- package/src/modules/audit_logs/migrations/.snapshot-open-mercato.json +10 -0
- package/src/modules/audit_logs/migrations/Migration20260611104500.ts +13 -0
- package/src/modules/audit_logs/services/accessLogService.ts +15 -0
- package/src/modules/auth/api/admin/nav.ts +9 -0
- package/src/modules/auth/api/login.ts +13 -13
- package/src/modules/auth/data/entities.ts +2 -0
- package/src/modules/auth/i18n/de.json +0 -1
- package/src/modules/auth/i18n/en.json +0 -1
- package/src/modules/auth/i18n/es.json +0 -1
- package/src/modules/auth/i18n/pl.json +0 -1
- package/src/modules/auth/lib/backendChrome.tsx +37 -1
- package/src/modules/auth/lib/consentIntegrity.ts +6 -3
- package/src/modules/auth/migrations/.snapshot-open-mercato.json +20 -0
- package/src/modules/auth/migrations/Migration20260611103000.ts +21 -0
- package/src/modules/auth/services/authService.ts +24 -4
- package/src/modules/auth/services/rbacService.ts +11 -2
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx +0 -3
- package/src/modules/customers/api/deals/route.ts +51 -2
- package/src/modules/customers/api/deals/summary/route.ts +496 -0
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.ts +28 -6
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealData.ts +33 -6
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +17 -2
- package/src/modules/customers/backend/customers/deals/page.tsx +254 -66
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +1 -2
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +18 -0
- package/src/modules/customers/cli.ts +15 -15
- package/src/modules/customers/components/DealsKpiStrip.tsx +389 -0
- package/src/modules/customers/components/detail/ConfirmDealLostDialog.tsx +0 -1
- package/src/modules/customers/components/detail/DealForm.tsx +121 -19
- package/src/modules/customers/components/detail/PersonDetailTabs.tsx +12 -2
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +1 -2
- package/src/modules/customers/components/kpi/PipelineStageBar.tsx +77 -0
- package/src/modules/customers/i18n/de.json +43 -0
- package/src/modules/customers/i18n/en.json +43 -0
- package/src/modules/customers/i18n/es.json +43 -0
- package/src/modules/customers/i18n/pl.json +43 -0
- package/src/modules/customers/lib/dealsMetrics.ts +159 -0
- package/src/modules/directory/api/organization-branding/route.ts +238 -0
- package/src/modules/directory/api/organizations/route.ts +7 -0
- package/src/modules/directory/backend/directory/branding/page.meta.ts +24 -0
- package/src/modules/directory/backend/directory/branding/page.tsx +248 -0
- package/src/modules/directory/commands/organizations.ts +9 -1
- package/src/modules/directory/data/entities.ts +3 -0
- package/src/modules/directory/data/validators.ts +12 -0
- package/src/modules/directory/i18n/de.json +21 -0
- package/src/modules/directory/i18n/en.json +21 -0
- package/src/modules/directory/i18n/es.json +21 -0
- package/src/modules/directory/i18n/pl.json +21 -0
- package/src/modules/directory/migrations/.snapshot-open-mercato.json +40 -0
- package/src/modules/directory/migrations/Migration20260607222259_directory.ts +13 -0
- package/src/modules/directory/subscribers/invalidateOrgScopeCache.ts +3 -1
- package/src/modules/directory/utils/organizationScope.ts +85 -30
- package/src/modules/entities/api/definitions.batch.ts +11 -7
- package/src/modules/entities/api/entities.ts +11 -0
- package/src/modules/entities/api/records.ts +46 -25
- package/src/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.tsx +15 -0
- package/src/modules/entities/backend/entities/user/[entityId]/records/create/page.tsx +15 -0
- package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +23 -0
- package/src/modules/entities/components/useRecordsEntityGuard.ts +41 -0
- package/src/modules/entities/i18n/de.json +1 -0
- package/src/modules/entities/i18n/en.json +1 -0
- package/src/modules/entities/i18n/es.json +1 -0
- package/src/modules/entities/i18n/pl.json +1 -0
- package/src/modules/query_index/data/entities.ts +1 -0
- package/src/modules/query_index/lib/engine.ts +11 -5
- package/src/modules/query_index/migrations/.snapshot-open-mercato.json +11 -0
- package/src/modules/query_index/migrations/Migration20260611103000_query_index.ts +29 -0
- package/src/modules/sales/commands/documents.ts +7 -5
- package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +2 -1
- package/src/modules/sales/components/documents/salesDocumentsColumns.ts +6 -0
- package/src/modules/staff/api/team-members.ts +9 -2
- package/src/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.ts +31 -1
- package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +18 -8
- package/src/modules/staff/commands/team-members.ts +5 -2
- package/src/modules/staff/components/TeamMemberForm.tsx +4 -1
- package/src/modules/staff/i18n/de.json +1 -0
- package/src/modules/staff/i18n/en.json +1 -0
- package/src/modules/staff/i18n/es.json +1 -0
- package/src/modules/staff/i18n/pl.json +1 -0
- package/src/modules/staff/lib/scheduleSwitch.ts +46 -0
- package/src/modules/workflows/backend/definitions/create/page.tsx +1 -2
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +1 -2
- package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +1 -2
- package/src/modules/workflows/components/NodeEditDialog.tsx +1 -4
- package/src/modules/workflows/components/NodeEditDialogCrudForm.tsx +4 -7
- package/src/modules/workflows/components/WorkflowGraphImpl.tsx +1 -2
- package/src/modules/workflows/components/fields/FormFieldArrayEditor.tsx +2 -3
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
export class Migration20260607222259_directory extends Migration {
|
|
4
|
+
|
|
5
|
+
override up(): void | Promise<void> {
|
|
6
|
+
this.addSql(`alter table "organizations" add "logo_url" text null;`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
override down(): void | Promise<void> {
|
|
10
|
+
this.addSql(`alter table "organizations" drop column "logo_url";`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
}
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// drop every cache entry tagged for that tenant; the TTL is the backstop
|
|
8
8
|
// for races where the event fires after a request reads the cache.
|
|
9
9
|
|
|
10
|
+
import { buildOrgScopeTenantCacheTag } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
11
|
+
|
|
10
12
|
type CacheService = {
|
|
11
13
|
deleteByTags(tags: string[]): Promise<number>
|
|
12
14
|
}
|
|
@@ -32,7 +34,7 @@ export default async function handle(
|
|
|
32
34
|
}
|
|
33
35
|
if (!cache) return
|
|
34
36
|
try {
|
|
35
|
-
await cache.deleteByTags([
|
|
37
|
+
await cache.deleteByTags([buildOrgScopeTenantCacheTag(tenantId)])
|
|
36
38
|
} catch {
|
|
37
39
|
// best-effort; TTL is the backstop.
|
|
38
40
|
}
|
|
@@ -21,9 +21,11 @@ export type OrganizationScope = {
|
|
|
21
21
|
// OrganizationScope is a pure function of (userId, tenantId, selectedOrgId,
|
|
22
22
|
// requestedTenant) between membership changes; caching it bypasses 1
|
|
23
23
|
// SELECT on `organizations` per CRUD request. TTL is short (60s default)
|
|
24
|
-
// to keep staleness bounded
|
|
25
|
-
//
|
|
26
|
-
//
|
|
24
|
+
// to keep staleness bounded as a backstop. Tag-based invalidation also fires
|
|
25
|
+
// eagerly: per-user entries are dropped by RbacService.invalidateUserCache
|
|
26
|
+
// (every ACL/role grant change goes through it — see buildOrgScopeUserCacheTag)
|
|
27
|
+
// and per-tenant entries by the directory.organization.* subscriber plus
|
|
28
|
+
// RbacService.invalidateTenantCache (role-ACL changes).
|
|
27
29
|
const ORG_SCOPE_CACHE_KEY_PREFIX = 'org-scope'
|
|
28
30
|
// Phase 4 default-off until the same readiness probe (`GET /api/customers/people`)
|
|
29
31
|
// stays green with the cache layer engaged. Set `OM_ORG_SCOPE_CACHE_TTL_MS=60000`
|
|
@@ -49,10 +51,23 @@ function buildOrgScopeCacheKey(parts: {
|
|
|
49
51
|
return `${ORG_SCOPE_CACHE_KEY_PREFIX}:${parts.userId}:${parts.effectiveTenantId}:${selected}:${requested}`
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
// Tag builders are exported so the modules that own the "this user's scope
|
|
55
|
+
// changed" / "this tenant's org tree changed" signals (auth RBAC invalidation,
|
|
56
|
+
// the directory.organization.* subscriber) can drop the matching cross-request
|
|
57
|
+
// cache entries without re-deriving the tag format. Keeping the format in one
|
|
58
|
+
// place is what lets the TTL be enabled safely (issue #2259).
|
|
59
|
+
export function buildOrgScopeUserCacheTag(userId: string): string {
|
|
60
|
+
return `${ORG_SCOPE_CACHE_KEY_PREFIX}:user:${userId}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function buildOrgScopeTenantCacheTag(tenantId: string): string {
|
|
64
|
+
return `${ORG_SCOPE_CACHE_KEY_PREFIX}:tenant:${tenantId}`
|
|
65
|
+
}
|
|
66
|
+
|
|
52
67
|
function buildOrgScopeCacheTags(parts: { userId: string; effectiveTenantId: string }): string[] {
|
|
53
68
|
return [
|
|
54
|
-
|
|
55
|
-
|
|
69
|
+
buildOrgScopeUserCacheTag(parts.userId),
|
|
70
|
+
buildOrgScopeTenantCacheTag(parts.effectiveTenantId),
|
|
56
71
|
]
|
|
57
72
|
}
|
|
58
73
|
|
|
@@ -82,7 +97,7 @@ export async function invalidateOrganizationScopeCacheForUser(
|
|
|
82
97
|
const cache = resolveCacheFromContainer(container)
|
|
83
98
|
if (!cache?.deleteByTags) return
|
|
84
99
|
try {
|
|
85
|
-
await cache.deleteByTags([
|
|
100
|
+
await cache.deleteByTags([buildOrgScopeUserCacheTag(userId)])
|
|
86
101
|
} catch (err) {
|
|
87
102
|
console.warn('[org-scope:cache] invalidate user failed', err)
|
|
88
103
|
}
|
|
@@ -95,12 +110,36 @@ export async function invalidateOrganizationScopeCacheForTenant(
|
|
|
95
110
|
const cache = resolveCacheFromContainer(container)
|
|
96
111
|
if (!cache?.deleteByTags) return
|
|
97
112
|
try {
|
|
98
|
-
await cache.deleteByTags([
|
|
113
|
+
await cache.deleteByTags([buildOrgScopeTenantCacheTag(tenantId)])
|
|
99
114
|
} catch (err) {
|
|
100
115
|
console.warn('[org-scope:cache] invalidate tenant failed', err)
|
|
101
116
|
}
|
|
102
117
|
}
|
|
103
118
|
|
|
119
|
+
// Issue #2259 — per-request memoization. resolveOrganizationScopeForRequest
|
|
120
|
+
// runs at least twice per CRUD request: once for the route-level feature check
|
|
121
|
+
// (resolveFeatureCheckContext) and once inside the shared factory's withCtx.
|
|
122
|
+
// Those two call sites use different request-scoped DI containers but are handed
|
|
123
|
+
// the SAME Request instance, so memoizing the resolved scope on a WeakMap keyed
|
|
124
|
+
// by that request collapses the duplicate work — and the duplicate
|
|
125
|
+
// `organizations` SELECT — into a single resolution. The inner map is keyed by
|
|
126
|
+
// the same identity tuple as the cross-request cache key, so distinct explicit
|
|
127
|
+
// selectedId/tenant overrides on one request stay independent. There is no
|
|
128
|
+
// staleness risk: the memo lives only for the lifetime of one request and is
|
|
129
|
+
// dropped with the request object by the GC.
|
|
130
|
+
const orgScopeRequestMemo = new WeakMap<object, Map<string, Promise<OrganizationScope>>>()
|
|
131
|
+
|
|
132
|
+
function getRequestScopeMemo(request: unknown): Map<string, Promise<OrganizationScope>> | null {
|
|
133
|
+
if (!request || (typeof request !== 'object' && typeof request !== 'function')) return null
|
|
134
|
+
const key = request as object
|
|
135
|
+
let memo = orgScopeRequestMemo.get(key)
|
|
136
|
+
if (!memo) {
|
|
137
|
+
memo = new Map<string, Promise<OrganizationScope>>()
|
|
138
|
+
orgScopeRequestMemo.set(key, memo)
|
|
139
|
+
}
|
|
140
|
+
return memo
|
|
141
|
+
}
|
|
142
|
+
|
|
104
143
|
function normalizeOrganizationId(value: unknown): string | null {
|
|
105
144
|
if (typeof value !== 'string') return null
|
|
106
145
|
const trimmed = value.trim()
|
|
@@ -402,35 +441,51 @@ export async function resolveOrganizationScopeForRequest({
|
|
|
402
441
|
})
|
|
403
442
|
: null
|
|
404
443
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
} catch (err) {
|
|
410
|
-
console.warn('[org-scope:cache] read failed', err)
|
|
411
|
-
}
|
|
444
|
+
const requestMemo = getRequestScopeMemo(request)
|
|
445
|
+
if (requestMemo && cacheKey) {
|
|
446
|
+
const memoized = requestMemo.get(cacheKey)
|
|
447
|
+
if (memoized) return memoized
|
|
412
448
|
}
|
|
413
449
|
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
450
|
+
const resolveScope = async (): Promise<OrganizationScope> => {
|
|
451
|
+
if (cache && cacheKey && typeof cache.get === 'function') {
|
|
452
|
+
try {
|
|
453
|
+
const cached = await cache.get(cacheKey)
|
|
454
|
+
if (isValidCachedScope(cached)) return cached
|
|
455
|
+
} catch (err) {
|
|
456
|
+
console.warn('[org-scope:cache] read failed', err)
|
|
457
|
+
}
|
|
458
|
+
}
|
|
421
459
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
|
|
460
|
+
const baseScope = await resolveOrganizationScope({
|
|
461
|
+
em,
|
|
462
|
+
rbac,
|
|
463
|
+
auth: scopedAuth,
|
|
464
|
+
selectedId: rawSelected,
|
|
465
|
+
tenantId: effectiveTenantId,
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
if (cache && cacheKey && userId && typeof cache.set === 'function') {
|
|
469
|
+
try {
|
|
470
|
+
await cache.set(cacheKey, baseScope, {
|
|
471
|
+
ttl: ttlMs,
|
|
472
|
+
tags: buildOrgScopeCacheTags({ userId, effectiveTenantId }),
|
|
473
|
+
})
|
|
474
|
+
} catch (err) {
|
|
475
|
+
console.warn('[org-scope:cache] write failed', err)
|
|
476
|
+
}
|
|
430
477
|
}
|
|
478
|
+
|
|
479
|
+
return baseScope
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (requestMemo && cacheKey) {
|
|
483
|
+
const pending = resolveScope()
|
|
484
|
+
requestMemo.set(cacheKey, pending)
|
|
485
|
+
return pending
|
|
431
486
|
}
|
|
432
487
|
|
|
433
|
-
return
|
|
488
|
+
return resolveScope()
|
|
434
489
|
}
|
|
435
490
|
|
|
436
491
|
export type FeatureCheckContext = {
|
|
@@ -13,16 +13,20 @@ export const metadata = {
|
|
|
13
13
|
POST: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
const MAX_DEFINITIONS_PER_BATCH = 1000
|
|
17
|
+
|
|
16
18
|
const batchSchema = z
|
|
17
19
|
.object({
|
|
18
20
|
entityId: z.string().regex(/^[a-z0-9_]+:[a-z0-9_]+$/),
|
|
19
|
-
definitions: z
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
definitions: z
|
|
22
|
+
.array(
|
|
23
|
+
upsertCustomFieldDefSchema
|
|
24
|
+
.omit({ entityId: true })
|
|
25
|
+
.extend({
|
|
26
|
+
configJson: z.any().optional(),
|
|
27
|
+
})
|
|
28
|
+
)
|
|
29
|
+
.max(MAX_DEFINITIONS_PER_BATCH),
|
|
26
30
|
})
|
|
27
31
|
.extend(customFieldEntityConfigSchema.shape)
|
|
28
32
|
|
|
@@ -7,6 +7,7 @@ import { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'
|
|
|
7
7
|
import { upsertCustomEntitySchema } from '@open-mercato/core/modules/entities/data/validators'
|
|
8
8
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
9
9
|
import { isSystemEntitySelectable } from '@open-mercato/shared/lib/entities/system-entities'
|
|
10
|
+
import { SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, isOrmBackedSystemEntityId } from '@open-mercato/shared/lib/data/engine'
|
|
10
11
|
|
|
11
12
|
export const metadata = {
|
|
12
13
|
GET: { requireAuth: true },
|
|
@@ -107,6 +108,16 @@ export async function POST(req: Request) {
|
|
|
107
108
|
const { resolve } = await createRequestContainer()
|
|
108
109
|
const em = resolve('em') as any
|
|
109
110
|
|
|
111
|
+
// A registration for a module-declared, table-backed system entity would flip
|
|
112
|
+
// query-engine classification to doc storage for the whole entity type (#2939's
|
|
113
|
+
// failure mode via another door) — refuse to create one.
|
|
114
|
+
if (isOrmBackedSystemEntityId(em, input.entityId)) {
|
|
115
|
+
return NextResponse.json(
|
|
116
|
+
{ error: 'System entities cannot be registered as custom entities', code: SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, entityId: input.entityId },
|
|
117
|
+
{ status: 400 },
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
110
121
|
const where: any = { entityId: input.entityId, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null }
|
|
111
122
|
let ent = await em.findOne(CustomEntity, where)
|
|
112
123
|
if (!ent) ent = em.create(CustomEntity, { ...where, createdAt: new Date() })
|
|
@@ -6,6 +6,7 @@ import type { QueryEngine, QueryOptions, Where, Sort } from '@open-mercato/share
|
|
|
6
6
|
import { normalizeExportFormat, serializeExport, defaultExportFilename, ensureColumns } from '@open-mercato/shared/lib/crud/exporters'
|
|
7
7
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
8
8
|
import { resolveOrganizationScope, getSelectedOrganizationFromRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
9
|
+
import { SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, isOrmBackedSystemEntityId } from '@open-mercato/shared/lib/data/engine'
|
|
9
10
|
import { parseBooleanToken, parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
|
|
10
11
|
import { setRecordCustomFields } from '../lib/helpers'
|
|
11
12
|
import { CustomFieldValue } from '../data/entities'
|
|
@@ -36,24 +37,34 @@ function isDeclaredCustomEntity(entityId: string): boolean {
|
|
|
36
37
|
|
|
37
38
|
const CUSTOM_ENTITY_RECORD_RESOURCE_KIND = 'entities.record'
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
type RecordsEntityKind = 'system' | 'custom' | 'unknown'
|
|
41
|
+
|
|
42
|
+
// This surface manages doc-storage records, which exist for CUSTOM entities only.
|
|
43
|
+
// Module-declared ids backed by a registered ORM table are system entities — their
|
|
44
|
+
// records live in their own module tables/APIs, and stray doc rows for them poisoned
|
|
45
|
+
// read-path classification platform-wide (#2939) — so they are rejected outright. The
|
|
46
|
+
// previous fallback that classified an entity by the mere presence of
|
|
47
|
+
// `custom_entities_storage` rows is gone: within the allowed set, declaration (ce.ts)
|
|
48
|
+
// or an active `custom_entities` registration is authoritative.
|
|
49
|
+
async function classifyRecordsEntity(em: any, entityId: string): Promise<RecordsEntityKind> {
|
|
50
|
+
if (isOrmBackedSystemEntityId(em, entityId)) return 'system'
|
|
51
|
+
if (isDeclaredCustomEntity(entityId)) return 'custom'
|
|
41
52
|
try {
|
|
42
53
|
const { CustomEntity } = await import('../data/entities')
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
// Any registration row — active or soft-deleted — proves the id is a custom
|
|
55
|
+
// entity. Records persist beyond the definition's soft delete (TC-ENTITIES-006)
|
|
56
|
+
// and must stay readable/deletable, e.g. for the restore flow and cleanup.
|
|
57
|
+
const found = await em.findOne(CustomEntity as any, { entityId })
|
|
58
|
+
if (found) return 'custom'
|
|
45
59
|
} catch {}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return !!row
|
|
55
|
-
} catch {}
|
|
56
|
-
return false
|
|
60
|
+
return 'unknown'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function systemEntityRecordsRejection(entityId: string) {
|
|
64
|
+
return NextResponse.json(
|
|
65
|
+
{ error: 'Records are available for custom entities only', code: SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, entityId },
|
|
66
|
+
{ status: 400 },
|
|
67
|
+
)
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
async function readCustomEntityRecordUpdatedAt(
|
|
@@ -148,13 +159,13 @@ export async function GET(req: Request) {
|
|
|
148
159
|
const rbac = resolve('rbacService') as RbacService
|
|
149
160
|
const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })
|
|
150
161
|
let organizationIds: string[] | null = scope.filterIds
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const isCustomEntity =
|
|
162
|
+
// Module-declared custom entities (ce.ts) carry frozen system-style ids and are never
|
|
163
|
+
// registered in `custom_entities`, so classification checks the declared registry plus
|
|
164
|
+
// active registrations. System (table-backed) ids are rejected above; for the allowed
|
|
165
|
+
// set `isCustomEntity` drives mapRow's cf_ stripping so the edit form reads back values.
|
|
166
|
+
const entityKind = await classifyRecordsEntity(em, entityId)
|
|
167
|
+
if (entityKind === 'system') return systemEntityRecordsRejection(entityId)
|
|
168
|
+
const isCustomEntity = entityKind === 'custom'
|
|
158
169
|
await assertEntityAclForRequest({ auth, entityId, action: 'view', isCustomEntity, rbac })
|
|
159
170
|
if (organizationIds && organizationIds.length === 0) {
|
|
160
171
|
return NextResponse.json({ items: [], total: 0, page, pageSize, totalPages: 0 })
|
|
@@ -230,6 +241,10 @@ export async function GET(req: Request) {
|
|
|
230
241
|
if (organizationIds && organizationIds.length) {
|
|
231
242
|
qopts.organizationIds = organizationIds
|
|
232
243
|
}
|
|
244
|
+
// Allowed entities are doc-storage-backed by definition (system ids were rejected
|
|
245
|
+
// above) — direct the engine to doc storage explicitly so reads stay deterministic
|
|
246
|
+
// even before the first record exists.
|
|
247
|
+
if (isCustomEntity) qopts.forceCustomEntityStorage = true
|
|
233
248
|
for (const [k, v] of qpEntries) buildFilter(k, v, isCustomEntity)
|
|
234
249
|
const res = await qe.query(entityId as any, qopts)
|
|
235
250
|
const rawItems = res.items || []
|
|
@@ -363,7 +378,9 @@ export async function POST(req: Request) {
|
|
|
363
378
|
const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })
|
|
364
379
|
const targetOrgId = scope.selectedId ?? auth.orgId
|
|
365
380
|
if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })
|
|
366
|
-
const
|
|
381
|
+
const entityKind = await classifyRecordsEntity(em, entityId)
|
|
382
|
+
if (entityKind === 'system') return systemEntityRecordsRejection(entityId)
|
|
383
|
+
const isCustomEntity = entityKind === 'custom'
|
|
367
384
|
await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, rbac })
|
|
368
385
|
const norm = normalizeValues(values)
|
|
369
386
|
|
|
@@ -427,7 +444,9 @@ export async function PUT(req: Request) {
|
|
|
427
444
|
const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })
|
|
428
445
|
const targetOrgId = scope.selectedId ?? auth.orgId
|
|
429
446
|
if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })
|
|
430
|
-
const
|
|
447
|
+
const entityKind = await classifyRecordsEntity(em, entityId)
|
|
448
|
+
if (entityKind === 'system') return systemEntityRecordsRejection(entityId)
|
|
449
|
+
const isCustomEntity = entityKind === 'custom'
|
|
431
450
|
await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, rbac })
|
|
432
451
|
const norm = normalizeValues(values)
|
|
433
452
|
|
|
@@ -516,7 +535,9 @@ export async function DELETE(req: Request) {
|
|
|
516
535
|
const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })
|
|
517
536
|
const targetOrgId = scope.selectedId ?? auth.orgId
|
|
518
537
|
if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })
|
|
519
|
-
const
|
|
538
|
+
const entityKind = await classifyRecordsEntity(em, entityId)
|
|
539
|
+
if (entityKind === 'system') return systemEntityRecordsRejection(entityId)
|
|
540
|
+
const isCustomEntity = entityKind === 'custom'
|
|
520
541
|
await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, rbac })
|
|
521
542
|
await de.deleteCustomEntityRecord({ entityId, recordId, organizationId: targetOrgId, tenantId: auth.tenantId!, soft: true })
|
|
522
543
|
return NextResponse.json({ ok: true })
|
|
@@ -6,6 +6,8 @@ import { z } from 'zod'
|
|
|
6
6
|
import { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
7
7
|
import { updateCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
8
8
|
import { createCrudFormError, raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
9
|
+
import { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'
|
|
10
|
+
import { useRecordsEntityGuard } from '@open-mercato/core/modules/entities/components/useRecordsEntityGuard'
|
|
9
11
|
|
|
10
12
|
type UpdateRecordRequest = (payload: { entityId: string; recordId: string; values: Record<string, unknown> }) => Promise<void>
|
|
11
13
|
|
|
@@ -38,6 +40,19 @@ export async function submitCustomEntityRecordUpdate(options: {
|
|
|
38
40
|
type RecordsResponse = { items: any[] }
|
|
39
41
|
|
|
40
42
|
export default function EditRecordPage({ params }: { params: { entityId?: string; recordId?: string } }) {
|
|
43
|
+
const t = useT()
|
|
44
|
+
const entityId = decodeURIComponent(params?.entityId || '')
|
|
45
|
+
const guard = useRecordsEntityGuard(entityId)
|
|
46
|
+
if (guard === 'blocked') {
|
|
47
|
+
return <ErrorMessage label={t('entities.userEntities.records.errors.systemEntity', 'This entity is system-managed. Records are available for custom entities only.')} />
|
|
48
|
+
}
|
|
49
|
+
if (guard === 'checking') {
|
|
50
|
+
return <LoadingMessage label={t('entities.userEntities.records.loading', 'Loading records...')} />
|
|
51
|
+
}
|
|
52
|
+
return <EditRecordPageInner params={params} />
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function EditRecordPageInner({ params }: { params: { entityId?: string; recordId?: string } }) {
|
|
41
56
|
const t = useT()
|
|
42
57
|
const entityId = decodeURIComponent(params?.entityId || '')
|
|
43
58
|
const recordId = decodeURIComponent(params?.recordId || '')
|
|
@@ -6,6 +6,8 @@ import { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'
|
|
|
6
6
|
import { z } from 'zod'
|
|
7
7
|
import { createCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
8
8
|
import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
9
|
+
import { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'
|
|
10
|
+
import { useRecordsEntityGuard } from '@open-mercato/core/modules/entities/components/useRecordsEntityGuard'
|
|
9
11
|
|
|
10
12
|
type CreateRecordRequest = (payload: { entityId: string; values: Record<string, unknown> }) => Promise<void>
|
|
11
13
|
|
|
@@ -30,6 +32,19 @@ export async function submitCustomEntityRecord(options: {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export default function CreateRecordPage({ params }: { params: { entityId?: string } }) {
|
|
35
|
+
const t = useT()
|
|
36
|
+
const entityId = decodeURIComponent(params?.entityId || '')
|
|
37
|
+
const guard = useRecordsEntityGuard(entityId)
|
|
38
|
+
if (guard === 'blocked') {
|
|
39
|
+
return <ErrorMessage label={t('entities.userEntities.records.errors.systemEntity', 'This entity is system-managed. Records are available for custom entities only.')} />
|
|
40
|
+
}
|
|
41
|
+
if (guard === 'checking') {
|
|
42
|
+
return <LoadingMessage label={t('entities.userEntities.records.loading', 'Loading records...')} />
|
|
43
|
+
}
|
|
44
|
+
return <CreateRecordPageInner params={params} />
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function CreateRecordPageInner({ params }: { params: { entityId?: string } }) {
|
|
33
48
|
const t = useT()
|
|
34
49
|
const router = useRouter()
|
|
35
50
|
const entityId = decodeURIComponent(params?.entityId || '')
|
|
@@ -17,6 +17,9 @@ import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
|
17
17
|
import { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
18
18
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
19
19
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
20
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
21
|
+
import { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'
|
|
22
|
+
import { useRecordsEntityGuard } from '@open-mercato/core/modules/entities/components/useRecordsEntityGuard'
|
|
20
23
|
|
|
21
24
|
type RecordsResponse = {
|
|
22
25
|
items: any[]
|
|
@@ -44,6 +47,26 @@ function normalizeCell(v: any): string {
|
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
export default function RecordsPage({ params }: { params: { entityId?: string } }) {
|
|
50
|
+
const t = useT()
|
|
51
|
+
const entityId = decodeURIComponent(params?.entityId || '')
|
|
52
|
+
const guard = useRecordsEntityGuard(entityId)
|
|
53
|
+
if (guard !== 'allowed') {
|
|
54
|
+
return (
|
|
55
|
+
<Page>
|
|
56
|
+
<PageBody>
|
|
57
|
+
{guard === 'blocked' ? (
|
|
58
|
+
<ErrorMessage label={t('entities.userEntities.records.errors.systemEntity', 'This entity is system-managed. Records are available for custom entities only.')} />
|
|
59
|
+
) : (
|
|
60
|
+
<LoadingMessage label={t('entities.userEntities.records.loading', 'Loading records...')} />
|
|
61
|
+
)}
|
|
62
|
+
</PageBody>
|
|
63
|
+
</Page>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
return <RecordsPageInner params={params} />
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function RecordsPageInner({ params }: { params: { entityId?: string } }) {
|
|
47
70
|
const entityId = decodeURIComponent(params?.entityId || '')
|
|
48
71
|
const [sorting, setSorting] = React.useState<SortingState>([{ id: 'id', desc: false }])
|
|
49
72
|
const [page, setPage] = React.useState(1)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
4
|
+
|
|
5
|
+
export type RecordsEntityGuardState = 'checking' | 'blocked' | 'allowed'
|
|
6
|
+
|
|
7
|
+
// Mirrors SYSTEM_ENTITY_RECORDS_BLOCKED_CODE from @open-mercato/shared/lib/data/engine —
|
|
8
|
+
// kept as a literal so client bundles do not pull the server-side data engine in.
|
|
9
|
+
const SYSTEM_ENTITY_RECORDS_BLOCKED_CODE = 'system_entity_records_blocked'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The records surface serves custom entities only; the API rejects system
|
|
13
|
+
* (table-backed) entity ids with 400 + `system_entity_records_blocked` (#2939
|
|
14
|
+
* hardening). Records pages are URL-addressable for any entity id, so they probe
|
|
15
|
+
* once and render a dedicated error state instead of a broken table/form.
|
|
16
|
+
* Fails open on transport errors — the page's own data calls surface those.
|
|
17
|
+
*/
|
|
18
|
+
export function useRecordsEntityGuard(entityId: string): RecordsEntityGuardState {
|
|
19
|
+
const [state, setState] = React.useState<RecordsEntityGuardState>(entityId ? 'checking' : 'allowed')
|
|
20
|
+
React.useEffect(() => {
|
|
21
|
+
if (!entityId) {
|
|
22
|
+
setState('allowed')
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
let cancelled = false
|
|
26
|
+
setState('checking')
|
|
27
|
+
apiCall<{ code?: string }>(`/api/entities/records?entityId=${encodeURIComponent(entityId)}&page=1&pageSize=1`)
|
|
28
|
+
.then((res) => {
|
|
29
|
+
if (cancelled) return
|
|
30
|
+
const blocked = res.status === 400 && res.result?.code === SYSTEM_ENTITY_RECORDS_BLOCKED_CODE
|
|
31
|
+
setState(blocked ? 'blocked' : 'allowed')
|
|
32
|
+
})
|
|
33
|
+
.catch(() => {
|
|
34
|
+
if (!cancelled) setState('allowed')
|
|
35
|
+
})
|
|
36
|
+
return () => {
|
|
37
|
+
cancelled = true
|
|
38
|
+
}
|
|
39
|
+
}, [entityId])
|
|
40
|
+
return state
|
|
41
|
+
}
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"entities.userEntities.records.errors.deleteFailed": "Datensatz konnte nicht gelöscht werden",
|
|
83
83
|
"entities.userEntities.records.errors.entityIdRequired": "Entitätskennung ist erforderlich",
|
|
84
84
|
"entities.userEntities.records.errors.recordIdRequired": "Datensatzkennung ist erforderlich",
|
|
85
|
+
"entities.userEntities.records.errors.systemEntity": "Diese Entität wird vom System verwaltet. Datensätze sind nur für benutzerdefinierte Entitäten verfügbar.",
|
|
85
86
|
"entities.userEntities.records.form.createTitle": "Datensatz erstellen",
|
|
86
87
|
"entities.userEntities.records.form.editTitle": "Datensatz bearbeiten",
|
|
87
88
|
"entities.userEntities.records.form.submitCreate": "Erstellen",
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"entities.userEntities.records.errors.deleteFailed": "Failed to delete record",
|
|
83
83
|
"entities.userEntities.records.errors.entityIdRequired": "Entity identifier is required",
|
|
84
84
|
"entities.userEntities.records.errors.recordIdRequired": "Record identifier is required",
|
|
85
|
+
"entities.userEntities.records.errors.systemEntity": "This entity is system-managed. Records are available for custom entities only.",
|
|
85
86
|
"entities.userEntities.records.form.createTitle": "Create record",
|
|
86
87
|
"entities.userEntities.records.form.editTitle": "Edit record",
|
|
87
88
|
"entities.userEntities.records.form.submitCreate": "Create",
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"entities.userEntities.records.errors.deleteFailed": "No se pudo eliminar el registro",
|
|
83
83
|
"entities.userEntities.records.errors.entityIdRequired": "Se requiere el identificador de la entidad",
|
|
84
84
|
"entities.userEntities.records.errors.recordIdRequired": "Se requiere el identificador del registro",
|
|
85
|
+
"entities.userEntities.records.errors.systemEntity": "Esta entidad está gestionada por el sistema. Los registros solo están disponibles para entidades personalizadas.",
|
|
85
86
|
"entities.userEntities.records.form.createTitle": "Crear registro",
|
|
86
87
|
"entities.userEntities.records.form.editTitle": "Editar registro",
|
|
87
88
|
"entities.userEntities.records.form.submitCreate": "Crear",
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"entities.userEntities.records.errors.deleteFailed": "Nie udało się usunąć rekordu",
|
|
83
83
|
"entities.userEntities.records.errors.entityIdRequired": "Wymagany jest identyfikator encji",
|
|
84
84
|
"entities.userEntities.records.errors.recordIdRequired": "Wymagany jest identyfikator rekordu",
|
|
85
|
+
"entities.userEntities.records.errors.systemEntity": "Ta encja jest zarządzana systemowo. Rekordy są dostępne wyłącznie dla encji niestandardowych.",
|
|
85
86
|
"entities.userEntities.records.form.createTitle": "Utwórz rekord",
|
|
86
87
|
"entities.userEntities.records.form.editTitle": "Edytuj rekord",
|
|
87
88
|
"entities.userEntities.records.form.submitCreate": "Utwórz",
|
|
@@ -254,6 +254,7 @@ export class IndexerStatusLog {
|
|
|
254
254
|
@Entity({ tableName: 'search_tokens' })
|
|
255
255
|
@Index({ name: 'search_tokens_lookup_idx', properties: ['entityType', 'field', 'tokenHash', 'tenantId', 'organizationId'] })
|
|
256
256
|
@Index({ name: 'search_tokens_entity_idx', properties: ['entityType', 'entityId'] })
|
|
257
|
+
@Index({ name: 'search_tokens_tenant_token_hash_idx', properties: ['tenantId', 'tokenHash'] })
|
|
257
258
|
export class SearchToken {
|
|
258
259
|
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
259
260
|
id!: string
|
|
@@ -2,7 +2,7 @@ import type { QueryEngine, QueryOptions, QueryResult, FilterOp, Filter, QueryCus
|
|
|
2
2
|
import { SortDir } from '@open-mercato/shared/lib/query/types'
|
|
3
3
|
import type { EntityId } from '@open-mercato/shared/modules/entities'
|
|
4
4
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
5
|
-
import { BasicQueryEngine, resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'
|
|
5
|
+
import { BasicQueryEngine, resolveEntityTableName, resolveRegisteredEntityTableName } from '@open-mercato/shared/lib/query/engine'
|
|
6
6
|
import { type Kysely, sql, type RawBuilder } from 'kysely'
|
|
7
7
|
import type { EventBus } from '@open-mercato/events'
|
|
8
8
|
import { readCoverageSnapshot, refreshCoverageSnapshot } from './coverage'
|
|
@@ -219,7 +219,7 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
219
219
|
const debugEnabled = this.isDebugVerbosity()
|
|
220
220
|
if (debugEnabled) this.debug('query:start', { entity })
|
|
221
221
|
|
|
222
|
-
const isCustom = await this.isCustomEntity(entity)
|
|
222
|
+
const isCustom = opts.forceCustomEntityStorage === true || await this.isCustomEntity(entity)
|
|
223
223
|
if (isCustom) {
|
|
224
224
|
if (debugEnabled) this.debug('query:custom-entity', { entity })
|
|
225
225
|
const section = profiler.section('custom_entity')
|
|
@@ -1005,6 +1005,14 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
1005
1005
|
.executeTakeFirst()
|
|
1006
1006
|
if (row) {
|
|
1007
1007
|
result = true
|
|
1008
|
+
} else if (resolveRegisteredEntityTableName(this.em, entity) !== null) {
|
|
1009
|
+
// An id backed by a registered ORM table is never doc-storage-backed by
|
|
1010
|
+
// inference: stray `custom_entities_storage` rows for such an id (e.g. written
|
|
1011
|
+
// through the generic entities data engine) must not hijack every list/detail
|
|
1012
|
+
// read for the whole entity type away from its base table (#2939). Surfaces
|
|
1013
|
+
// that intentionally read doc records for a dual-declared id pass
|
|
1014
|
+
// `forceCustomEntityStorage` in QueryOptions instead.
|
|
1015
|
+
result = false
|
|
1008
1016
|
} else {
|
|
1009
1017
|
// Read/write symmetry. Records written through the entities data engine
|
|
1010
1018
|
// (`de.createCustomEntityRecord`) always land in `custom_entities_storage`,
|
|
@@ -1012,9 +1020,7 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
1012
1020
|
// id — those are NEVER registered in `custom_entities` (install treats a
|
|
1013
1021
|
// system id as non-registrable). Without this fallback the query routes to
|
|
1014
1022
|
// the empty ORM/index path and those records are write-only (created with
|
|
1015
|
-
// 200 but unreadable on the edit form).
|
|
1016
|
-
// to `custom_entities_storage`, so this can only ever re-classify genuine
|
|
1017
|
-
// doc-storage entities — it cannot misroute table-backed entities.
|
|
1023
|
+
// 200 but unreadable on the edit form).
|
|
1018
1024
|
result = await this.hasCustomEntityStorageRows(entity)
|
|
1019
1025
|
}
|
|
1020
1026
|
} catch {
|
|
@@ -1354,6 +1354,17 @@
|
|
|
1354
1354
|
"primary": false,
|
|
1355
1355
|
"unique": false
|
|
1356
1356
|
},
|
|
1357
|
+
{
|
|
1358
|
+
"columnNames": [
|
|
1359
|
+
"tenant_id",
|
|
1360
|
+
"token_hash"
|
|
1361
|
+
],
|
|
1362
|
+
"composite": true,
|
|
1363
|
+
"constraint": false,
|
|
1364
|
+
"keyName": "search_tokens_tenant_token_hash_idx",
|
|
1365
|
+
"primary": false,
|
|
1366
|
+
"unique": false
|
|
1367
|
+
},
|
|
1357
1368
|
{
|
|
1358
1369
|
"columnNames": [
|
|
1359
1370
|
"id"
|