@open-mercato/core 0.6.6-develop.6345.1.b236ea3d4b → 0.6.6-develop.6351.1.c140d703c9

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.
@@ -1,4 +1,4 @@
1
- [build:core] found 3428 entry points
1
+ [build:core] found 3429 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 186 entry points
4
4
  [build:core:generated] built successfully
@@ -10,6 +10,14 @@ import {
10
10
  beginEntitiesMutationGuard,
11
11
  FIELD_DEFINITION_RESOURCE_KIND
12
12
  } from "./definitions.mutation-guard.js";
13
+ import {
14
+ createExactDefinitionWhere,
15
+ createScopedDefinitionTombstone,
16
+ createVisibleDefinitionWhere,
17
+ markDefinitionTombstoned,
18
+ resolveDefinitionMutationScope,
19
+ selectVisibleDefinitionWinner
20
+ } from "../lib/definition-scope.js";
13
21
  const metadata = {
14
22
  POST: { requireAuth: true, requireFeatures: ["entities.definitions.manage"] }
15
23
  };
@@ -49,6 +57,7 @@ async function POST(req) {
49
57
  if (!parsed.success) return NextResponse.json({ error: "Validation failed", details: parsed.error.flatten() }, { status: 400 });
50
58
  const { entityId, definitions, fieldsets, singleFieldsetPerRecord } = parsed.data;
51
59
  const container = await createRequestContainer();
60
+ const scope = await resolveDefinitionMutationScope({ auth, container, request: req });
52
61
  const { resolve } = container;
53
62
  const em = resolve("em");
54
63
  let cache;
@@ -72,26 +81,31 @@ async function POST(req) {
72
81
  const keys = definitions.map((d) => d.key);
73
82
  if (keys.length > 0) {
74
83
  const existingDefs = await em.find(CustomFieldDef, {
75
- entityId,
76
- key: { $in: keys },
77
- organizationId: auth.orgId ?? null,
78
- tenantId: auth.tenantId ?? null
84
+ ...createExactDefinitionWhere(entityId, { $in: keys }, scope)
79
85
  });
80
86
  for (const existing of existingDefs) defByKey.set(existing.key, existing);
81
87
  }
82
- for (const [idx, d] of definitions.entries()) {
83
- const where = {
88
+ const inheritedByKey = /* @__PURE__ */ new Map();
89
+ const inactiveKeys = definitions.filter((d) => d.isActive === false).map((d) => d.key);
90
+ if (inactiveKeys.length > 0) {
91
+ const visibleDefs = await em.find(CustomFieldDef, createVisibleDefinitionWhere(
84
92
  entityId,
85
- key: d.key,
86
- organizationId: auth.orgId ?? null,
87
- tenantId: auth.tenantId ?? null
88
- };
89
- let def = defByKey.get(d.key);
90
- if (!def) {
91
- def = em.create(CustomFieldDef, { ...where, createdAt: /* @__PURE__ */ new Date() });
92
- defByKey.set(d.key, def);
93
+ { $in: inactiveKeys },
94
+ scope,
95
+ { deletedAt: null, isActive: true }
96
+ ));
97
+ const grouped = /* @__PURE__ */ new Map();
98
+ for (const visible of visibleDefs) {
99
+ if (!grouped.has(visible.key)) grouped.set(visible.key, []);
100
+ grouped.get(visible.key).push(visible);
93
101
  }
94
- def.kind = d.kind;
102
+ for (const [key, group] of grouped) {
103
+ inheritedByKey.set(key, selectVisibleDefinitionWinner(group));
104
+ }
105
+ }
106
+ for (const [idx, d] of definitions.entries()) {
107
+ const where = createExactDefinitionWhere(entityId, d.key, scope);
108
+ let def = defByKey.get(d.key);
95
109
  const inCfg = d.configJson ?? {};
96
110
  const cfg = { ...inCfg };
97
111
  if (cfg.label == null || String(cfg.label).trim() === "") cfg.label = d.key;
@@ -99,15 +113,48 @@ async function POST(req) {
99
113
  if (cfg.listVisible === void 0) cfg.listVisible = true;
100
114
  if (d.kind === "multiline" && (cfg.editor == null || String(cfg.editor).trim() === "")) cfg.editor = "markdown";
101
115
  cfg.priority = idx;
116
+ if (d.isActive === false) {
117
+ const now = /* @__PURE__ */ new Date();
118
+ const inherited = inheritedByKey.get(d.key);
119
+ if (!def) {
120
+ def = createScopedDefinitionTombstone(
121
+ em,
122
+ {
123
+ entityId,
124
+ key: d.key,
125
+ kind: d.kind,
126
+ configJson: inherited?.configJson ?? cfg
127
+ },
128
+ scope,
129
+ now
130
+ );
131
+ defByKey.set(d.key, def);
132
+ } else {
133
+ markDefinitionTombstoned(def, now);
134
+ }
135
+ def.kind = d.kind;
136
+ def.configJson = cfg;
137
+ def.isActive = false;
138
+ def.deletedAt = def.deletedAt ?? now;
139
+ def.updatedAt = now;
140
+ em.persist(def);
141
+ continue;
142
+ }
143
+ if (!def) {
144
+ def = em.create(CustomFieldDef, { ...where, createdAt: /* @__PURE__ */ new Date() });
145
+ defByKey.set(d.key, def);
146
+ }
147
+ def.kind = d.kind;
102
148
  def.configJson = cfg;
103
- def.isActive = d.isActive ?? true;
149
+ def.isActive = true;
150
+ def.deletedAt = null;
104
151
  def.updatedAt = /* @__PURE__ */ new Date();
105
152
  em.persist(def);
106
153
  }
107
154
  if (fieldsets !== void 0 || singleFieldsetPerRecord !== void 0) {
108
- const scope = { entityId, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null };
109
- let cfg = await em.findOne(CustomFieldEntityConfig, scope);
110
- if (!cfg) cfg = em.create(CustomFieldEntityConfig, { ...scope, createdAt: /* @__PURE__ */ new Date() });
155
+ const entityConfigScope = { entityId, organizationId: scope.organizationId, tenantId: scope.tenantId };
156
+ let cfg = await em.findOne(CustomFieldEntityConfig, entityConfigScope);
157
+ if (!cfg) cfg = em.create(CustomFieldEntityConfig, { ...entityConfigScope, createdAt: /* @__PURE__ */ new Date() });
111
158
  const existing = normalizeEntityFieldsetConfig(cfg.configJson ?? {});
112
159
  const patch = mergeEntityFieldsetConfig(existing, {
113
160
  fieldsets: fieldsets !== void 0 ? cloneFieldsets(fieldsets) ?? [] : void 0,
@@ -132,8 +179,8 @@ async function POST(req) {
132
179
  }
133
180
  await guard.runAfterSuccess();
134
181
  await invalidateDefinitionsCache(cache, {
135
- tenantId: auth.tenantId ?? null,
136
- organizationId: auth.orgId ?? null,
182
+ tenantId: scope.tenantId,
183
+ organizationId: scope.organizationId,
137
184
  entityIds: [entityId]
138
185
  });
139
186
  return NextResponse.json({ ok: true });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/entities/api/definitions.batch.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { CustomFieldDef, CustomFieldEntityConfig } from '@open-mercato/core/modules/entities/data/entities'\nimport { customFieldEntityConfigSchema, upsertCustomFieldDefSchema } from '@open-mercato/core/modules/entities/data/validators'\nimport { z } from 'zod'\nimport { invalidateDefinitionsCache } from './definitions.cache'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { mergeEntityFieldsetConfig, normalizeEntityFieldsetConfig } from '../lib/fieldsets'\nimport {\n beginEntitiesMutationGuard,\n FIELD_DEFINITION_RESOURCE_KIND,\n} from './definitions.mutation-guard'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n}\n\nconst MAX_DEFINITIONS_PER_BATCH = 1000\n\nconst batchSchema = z\n .object({\n entityId: z.string().regex(/^[a-z0-9_]+:[a-z0-9_]+$/),\n definitions: z\n .array(\n upsertCustomFieldDefSchema\n .omit({ entityId: true })\n .extend({\n configJson: z.any().optional(),\n })\n )\n .max(MAX_DEFINITIONS_PER_BATCH),\n })\n .extend(customFieldEntityConfigSchema.shape)\n\ntype IncomingFieldset = z.infer<typeof customFieldEntityConfigSchema>['fieldsets']\n\nfunction cloneFieldsets(fieldsets?: IncomingFieldset): IncomingFieldset {\n if (!Array.isArray(fieldsets)) return undefined\n return fieldsets.map((fieldset) => ({\n code: fieldset.code,\n label: fieldset.label,\n icon: fieldset.icon,\n description: fieldset.description,\n groups: Array.isArray(fieldset.groups)\n ? fieldset.groups.map((group) => ({\n code: group.code,\n title: group.title,\n hint: group.hint,\n }))\n : undefined,\n }))\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n let body: any\n try { body = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n\n const parsed = batchSchema.safeParse(body)\n if (!parsed.success) return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n const { entityId, definitions, fieldsets, singleFieldsetPerRecord } = parsed.data\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as any\n let cache: CacheStrategy | undefined\n try {\n cache = resolve('cache') as CacheStrategy\n } catch {}\n\n const guard = await beginEntitiesMutationGuard({\n container,\n auth,\n req,\n resourceKind: FIELD_DEFINITION_RESOURCE_KIND,\n resourceId: entityId,\n operation: 'custom',\n mutationPayload: { entityId, definitionCount: definitions.length },\n })\n if (guard.blockedResponse) return guard.blockedResponse\n\n await em.begin()\n try {\n // Prefetch every existing definition for this entity in a single query, then index\n // by key so the per-definition loop resolves create/update without round trips.\n const defByKey = new Map<string, any>()\n const keys = definitions.map((d) => d.key)\n if (keys.length > 0) {\n const existingDefs = await em.find(CustomFieldDef, {\n entityId,\n key: { $in: keys },\n organizationId: auth.orgId ?? null,\n tenantId: auth.tenantId ?? null,\n })\n for (const existing of existingDefs) defByKey.set(existing.key, existing)\n }\n\n for (const [idx, d] of definitions.entries()) {\n const where: any = {\n entityId,\n key: d.key,\n organizationId: auth.orgId ?? null,\n tenantId: auth.tenantId ?? null,\n }\n let def = defByKey.get(d.key)\n if (!def) {\n def = em.create(CustomFieldDef, { ...where, createdAt: new Date() })\n defByKey.set(d.key, def)\n }\n def.kind = d.kind\n\n const inCfg = (d as any).configJson ?? {}\n const cfg: Record<string, any> = { ...inCfg }\n if (cfg.label == null || String(cfg.label).trim() === '') cfg.label = d.key\n if (cfg.formEditable === undefined) cfg.formEditable = true\n if (cfg.listVisible === undefined) cfg.listVisible = true\n if (d.kind === 'multiline' && (cfg.editor == null || String(cfg.editor).trim() === '')) cfg.editor = 'markdown'\n cfg.priority = idx\n\n def.configJson = cfg\n def.isActive = d.isActive ?? true\n def.updatedAt = new Date()\n em.persist(def)\n }\n if (fieldsets !== undefined || singleFieldsetPerRecord !== undefined) {\n const scope: any = { entityId, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null }\n let cfg = await em.findOne(CustomFieldEntityConfig, scope)\n if (!cfg) cfg = em.create(CustomFieldEntityConfig, { ...scope, createdAt: new Date() })\n const existing = normalizeEntityFieldsetConfig(cfg.configJson ?? {})\n const patch = mergeEntityFieldsetConfig(existing, {\n fieldsets: fieldsets !== undefined ? cloneFieldsets(fieldsets) ?? [] : undefined,\n singleFieldsetPerRecord,\n })\n cfg.configJson = {\n fieldsets: patch.fieldsets,\n singleFieldsetPerRecord: patch.singleFieldsetPerRecord,\n }\n cfg.updatedAt = new Date()\n cfg.isActive = true\n em.persist(cfg)\n }\n await em.flush()\n await em.commit()\n } catch (e) {\n try { await em.rollback() } catch {}\n return NextResponse.json({ error: 'Failed to save definitions batch' }, { status: 500 })\n }\n\n await guard.runAfterSuccess()\n\n await invalidateDefinitionsCache(cache, {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n entityIds: [entityId],\n })\n\n return NextResponse.json({ ok: true })\n}\n\nconst batchResponseSchema = z.object({\n ok: z.literal(true),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'Batch upsert custom field definitions',\n methods: {\n POST: {\n summary: 'Save multiple custom field definitions',\n description: 'Creates or updates multiple definitions for a single entity in one transaction.',\n requestBody: {\n contentType: 'application/json',\n schema: batchSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Definitions saved',\n schema: batchResponseSchema,\n },\n {\n status: 400,\n description: 'Validation error',\n schema: z.object({\n error: z.string(),\n details: z.any().optional(),\n }),\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: z.object({ error: z.string() }),\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { CustomFieldDef, CustomFieldEntityConfig } from '@open-mercato/core/modules/entities/data/entities'\nimport { customFieldEntityConfigSchema, upsertCustomFieldDefSchema } from '@open-mercato/core/modules/entities/data/validators'\nimport { z } from 'zod'\nimport { invalidateDefinitionsCache } from './definitions.cache'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { mergeEntityFieldsetConfig, normalizeEntityFieldsetConfig } from '../lib/fieldsets'\nimport {\n beginEntitiesMutationGuard,\n FIELD_DEFINITION_RESOURCE_KIND,\n} from './definitions.mutation-guard'\nimport {\n createExactDefinitionWhere,\n createScopedDefinitionTombstone,\n createVisibleDefinitionWhere,\n markDefinitionTombstoned,\n resolveDefinitionMutationScope,\n selectVisibleDefinitionWinner,\n} from '../lib/definition-scope'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n}\n\nconst MAX_DEFINITIONS_PER_BATCH = 1000\n\nconst batchSchema = z\n .object({\n entityId: z.string().regex(/^[a-z0-9_]+:[a-z0-9_]+$/),\n definitions: z\n .array(\n upsertCustomFieldDefSchema\n .omit({ entityId: true })\n .extend({\n configJson: z.any().optional(),\n })\n )\n .max(MAX_DEFINITIONS_PER_BATCH),\n })\n .extend(customFieldEntityConfigSchema.shape)\n\ntype IncomingFieldset = z.infer<typeof customFieldEntityConfigSchema>['fieldsets']\n\nfunction cloneFieldsets(fieldsets?: IncomingFieldset): IncomingFieldset {\n if (!Array.isArray(fieldsets)) return undefined\n return fieldsets.map((fieldset) => ({\n code: fieldset.code,\n label: fieldset.label,\n icon: fieldset.icon,\n description: fieldset.description,\n groups: Array.isArray(fieldset.groups)\n ? fieldset.groups.map((group) => ({\n code: group.code,\n title: group.title,\n hint: group.hint,\n }))\n : undefined,\n }))\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n let body: any\n try { body = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n\n const parsed = batchSchema.safeParse(body)\n if (!parsed.success) return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n const { entityId, definitions, fieldsets, singleFieldsetPerRecord } = parsed.data\n\n const container = await createRequestContainer()\n const scope = await resolveDefinitionMutationScope({ auth, container, request: req })\n const { resolve } = container\n const em = resolve('em') as any\n let cache: CacheStrategy | undefined\n try {\n cache = resolve('cache') as CacheStrategy\n } catch {}\n\n const guard = await beginEntitiesMutationGuard({\n container,\n auth,\n req,\n resourceKind: FIELD_DEFINITION_RESOURCE_KIND,\n resourceId: entityId,\n operation: 'custom',\n mutationPayload: { entityId, definitionCount: definitions.length },\n })\n if (guard.blockedResponse) return guard.blockedResponse\n\n await em.begin()\n try {\n // Prefetch every existing definition for this entity in a single query, then index\n // by key so the per-definition loop resolves create/update without round trips.\n const defByKey = new Map<string, any>()\n const keys = definitions.map((d) => d.key)\n if (keys.length > 0) {\n const existingDefs = await em.find(CustomFieldDef, {\n ...createExactDefinitionWhere(entityId, { $in: keys }, scope),\n })\n for (const existing of existingDefs) defByKey.set(existing.key, existing)\n }\n\n const inheritedByKey = new Map<string, any>()\n const inactiveKeys = definitions.filter((d) => d.isActive === false).map((d) => d.key)\n if (inactiveKeys.length > 0) {\n const visibleDefs = await em.find(CustomFieldDef, createVisibleDefinitionWhere(\n entityId,\n { $in: inactiveKeys },\n scope,\n { deletedAt: null, isActive: true },\n ))\n const grouped = new Map<string, any[]>()\n for (const visible of visibleDefs) {\n if (!grouped.has(visible.key)) grouped.set(visible.key, [])\n grouped.get(visible.key)!.push(visible)\n }\n for (const [key, group] of grouped) {\n inheritedByKey.set(key, selectVisibleDefinitionWinner(group))\n }\n }\n\n for (const [idx, d] of definitions.entries()) {\n const where: any = createExactDefinitionWhere(entityId, d.key, scope)\n let def = defByKey.get(d.key)\n\n const inCfg = (d as any).configJson ?? {}\n const cfg: Record<string, any> = { ...inCfg }\n if (cfg.label == null || String(cfg.label).trim() === '') cfg.label = d.key\n if (cfg.formEditable === undefined) cfg.formEditable = true\n if (cfg.listVisible === undefined) cfg.listVisible = true\n if (d.kind === 'multiline' && (cfg.editor == null || String(cfg.editor).trim() === '')) cfg.editor = 'markdown'\n cfg.priority = idx\n\n if (d.isActive === false) {\n const now = new Date()\n const inherited = inheritedByKey.get(d.key)\n if (!def) {\n def = createScopedDefinitionTombstone(\n em,\n {\n entityId,\n key: d.key,\n kind: d.kind,\n configJson: inherited?.configJson ?? cfg,\n },\n scope,\n now,\n )\n defByKey.set(d.key, def)\n } else {\n markDefinitionTombstoned(def, now)\n }\n def.kind = d.kind\n def.configJson = cfg\n def.isActive = false\n def.deletedAt = def.deletedAt ?? now\n def.updatedAt = now\n em.persist(def)\n continue\n }\n\n if (!def) {\n def = em.create(CustomFieldDef, { ...where, createdAt: new Date() })\n defByKey.set(d.key, def)\n }\n def.kind = d.kind\n def.configJson = cfg\n def.isActive = true\n def.deletedAt = null\n def.updatedAt = new Date()\n em.persist(def)\n }\n if (fieldsets !== undefined || singleFieldsetPerRecord !== undefined) {\n const entityConfigScope: any = { entityId, organizationId: scope.organizationId, tenantId: scope.tenantId }\n let cfg = await em.findOne(CustomFieldEntityConfig, entityConfigScope)\n if (!cfg) cfg = em.create(CustomFieldEntityConfig, { ...entityConfigScope, createdAt: new Date() })\n const existing = normalizeEntityFieldsetConfig(cfg.configJson ?? {})\n const patch = mergeEntityFieldsetConfig(existing, {\n fieldsets: fieldsets !== undefined ? cloneFieldsets(fieldsets) ?? [] : undefined,\n singleFieldsetPerRecord,\n })\n cfg.configJson = {\n fieldsets: patch.fieldsets,\n singleFieldsetPerRecord: patch.singleFieldsetPerRecord,\n }\n cfg.updatedAt = new Date()\n cfg.isActive = true\n em.persist(cfg)\n }\n await em.flush()\n await em.commit()\n } catch (e) {\n try { await em.rollback() } catch {}\n return NextResponse.json({ error: 'Failed to save definitions batch' }, { status: 500 })\n }\n\n await guard.runAfterSuccess()\n\n await invalidateDefinitionsCache(cache, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n entityIds: [entityId],\n })\n\n return NextResponse.json({ ok: true })\n}\n\nconst batchResponseSchema = z.object({\n ok: z.literal(true),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'Batch upsert custom field definitions',\n methods: {\n POST: {\n summary: 'Save multiple custom field definitions',\n description: 'Creates or updates multiple definitions for a single entity in one transaction.',\n requestBody: {\n contentType: 'application/json',\n schema: batchSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Definitions saved',\n schema: batchResponseSchema,\n },\n {\n status: 400,\n description: 'Validation error',\n schema: z.object({\n error: z.string(),\n details: z.any().optional(),\n }),\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: z.object({ error: z.string() }),\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -20,6 +20,15 @@ import {
20
20
  beginEntitiesMutationGuard,
21
21
  FIELD_DEFINITION_RESOURCE_KIND
22
22
  } from "./definitions.mutation-guard.js";
23
+ import {
24
+ createExactDefinitionWhere,
25
+ createScopedDefinitionTombstone,
26
+ createVisibleDefinitionWhere,
27
+ markDefinitionTombstoned,
28
+ resolveDefinitionScopeFromOrganizationScope,
29
+ resolveDefinitionMutationScope,
30
+ selectVisibleDefinitionWinner
31
+ } from "../lib/definition-scope.js";
23
32
  async function validateDefaultValueByKind(value, kind, cfg, em, tenantContext) {
24
33
  switch (kind) {
25
34
  case "boolean":
@@ -158,6 +167,13 @@ function normalizeFieldGroup(raw) {
158
167
  if (typeof entry.hint === "string" && entry.hint.trim()) group.hint = entry.hint.trim();
159
168
  return group;
160
169
  }
170
+ function definitionMatchesReadScope(definition, scope) {
171
+ const definitionTenantId = definition.tenantId ?? null;
172
+ const definitionOrganizationId = definition.organizationId ?? null;
173
+ const tenantMatches = definitionTenantId === null || definitionTenantId === scope.tenantId;
174
+ const organizationMatches = definitionOrganizationId === null || scope.organizationId !== null && definitionOrganizationId === scope.organizationId;
175
+ return tenantMatches && organizationMatches;
176
+ }
161
177
  async function GET(req) {
162
178
  const url = new URL(req.url);
163
179
  const requestedEntityIds = parseEntityIds(url);
@@ -174,11 +190,12 @@ async function GET(req) {
174
190
  }
175
191
  const container = await createRequestContainer();
176
192
  const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req });
177
- const tenantId = scope.tenantId ?? auth.tenantId ?? null;
193
+ const definitionScope = resolveDefinitionScopeFromOrganizationScope(auth, scope);
194
+ const tenantId = definitionScope.tenantId;
178
195
  if (!tenantId) {
179
196
  return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
180
197
  }
181
- const organizationId = scope.selectedId ?? auth.orgId ?? null;
198
+ const organizationId = definitionScope.organizationId;
182
199
  const { resolve } = container;
183
200
  const em = resolve("em");
184
201
  let cache;
@@ -235,21 +252,27 @@ async function GET(req) {
235
252
  organizationId,
236
253
  mode: "public"
237
254
  });
255
+ const tenantCandidates = [{ tenantId }, { tenantId: null }];
256
+ const organizationCandidates = [{ organizationId: null }];
257
+ if (organizationId) organizationCandidates.unshift({ organizationId });
258
+ const readScope = { tenantId, organizationId };
238
259
  const whereActive = {
239
260
  entityId: { $in: entityIds },
240
261
  deletedAt: null,
241
262
  $and: [
242
- { $or: [{ tenantId: tenantId ?? void 0 }, { tenantId: null }] }
263
+ { $or: tenantCandidates },
264
+ { $or: organizationCandidates }
243
265
  ]
244
266
  };
245
- const defs = await em.find(CustomFieldDef, whereActive);
246
- const tombstones = await em.find(CustomFieldDef, {
267
+ const defs = (await em.find(CustomFieldDef, whereActive)).filter((definition) => definitionMatchesReadScope(definition, readScope));
268
+ const tombstones = (await em.find(CustomFieldDef, {
247
269
  entityId: { $in: entityIds },
248
270
  deletedAt: { $ne: null },
249
271
  $and: [
250
- { $or: [{ tenantId: tenantId ?? void 0 }, { tenantId: null }] }
272
+ { $or: tenantCandidates },
273
+ { $or: organizationCandidates }
251
274
  ]
252
- });
275
+ })).filter((definition) => definitionMatchesReadScope(definition, readScope));
253
276
  const tombstonedByEntity = /* @__PURE__ */ new Map();
254
277
  for (const entry of tombstones) {
255
278
  const eid = String(entry.entityId);
@@ -407,6 +430,7 @@ async function POST(req) {
407
430
  }
408
431
  }
409
432
  const container = await createRequestContainer();
433
+ const scope = await resolveDefinitionMutationScope({ auth, container, request: req });
410
434
  const { resolve } = container;
411
435
  const em = resolve("em");
412
436
  let cache;
@@ -414,7 +438,7 @@ async function POST(req) {
414
438
  cache = resolve("cache");
415
439
  } catch {
416
440
  }
417
- const where = { entityId: input.entityId, key: input.key, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null };
441
+ const where = createExactDefinitionWhere(input.entityId, input.key, scope);
418
442
  let def = await em.findOne(CustomFieldDef, where);
419
443
  const guard = await beginEntitiesMutationGuard({
420
444
  container,
@@ -451,7 +475,7 @@ async function POST(req) {
451
475
  input.kind,
452
476
  cfg,
453
477
  em,
454
- { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null }
478
+ { tenantId: scope.tenantId, organizationId: scope.organizationId }
455
479
  );
456
480
  if (validationError) {
457
481
  return NextResponse.json({ error: validationError }, { status: 400 });
@@ -460,13 +484,14 @@ async function POST(req) {
460
484
  }
461
485
  def.configJson = cfg;
462
486
  def.isActive = input.isActive ?? true;
487
+ def.deletedAt = def.isActive === false ? def.deletedAt ?? /* @__PURE__ */ new Date() : null;
463
488
  def.updatedAt = /* @__PURE__ */ new Date();
464
489
  em.persist(def);
465
490
  await em.flush();
466
491
  await guard.runAfterSuccess();
467
492
  await invalidateDefinitionsCache(cache, {
468
- tenantId: auth.tenantId ?? null,
469
- organizationId: auth.orgId ?? null,
493
+ tenantId: scope.tenantId,
494
+ organizationId: scope.organizationId,
470
495
  entityIds: [input.entityId]
471
496
  });
472
497
  return NextResponse.json({ ok: true, item: { id: def.id, key: def.key, kind: def.kind, configJson: def.configJson, isActive: def.isActive } });
@@ -483,6 +508,7 @@ async function DELETE(req) {
483
508
  const { entityId, key } = body || {};
484
509
  if (!entityId || !key) return NextResponse.json({ error: "entityId and key are required" }, { status: 400 });
485
510
  const container = await createRequestContainer();
511
+ const scope = await resolveDefinitionMutationScope({ auth, container, request: req });
486
512
  const { resolve } = container;
487
513
  const em = resolve("em");
488
514
  let cache;
@@ -490,28 +516,39 @@ async function DELETE(req) {
490
516
  cache = resolve("cache");
491
517
  } catch {
492
518
  }
493
- const where = { entityId, key, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null };
494
- const def = await em.findOne(CustomFieldDef, where);
495
- if (!def) return NextResponse.json({ error: "Not found" }, { status: 404 });
519
+ const where = createExactDefinitionWhere(entityId, key, scope);
520
+ let def = await em.findOne(CustomFieldDef, where);
521
+ let inherited = null;
522
+ if (!def) {
523
+ inherited = selectVisibleDefinitionWinner(await em.find(CustomFieldDef, createVisibleDefinitionWhere(
524
+ entityId,
525
+ key,
526
+ scope,
527
+ { deletedAt: null, isActive: true }
528
+ )));
529
+ if (!inherited) return NextResponse.json({ error: "Not found" }, { status: 404 });
530
+ }
496
531
  const guard = await beginEntitiesMutationGuard({
497
532
  container,
498
533
  auth,
499
534
  req,
500
535
  resourceKind: FIELD_DEFINITION_RESOURCE_KIND,
501
- resourceId: def.id,
536
+ resourceId: def?.id ?? inherited?.id ?? `${entityId}:${key}`,
502
537
  operation: "delete",
503
538
  mutationPayload: { entityId, key }
504
539
  });
505
540
  if (guard.blockedResponse) return guard.blockedResponse;
506
- def.isActive = false;
507
- def.updatedAt = /* @__PURE__ */ new Date();
508
- def.deletedAt = def.deletedAt ?? /* @__PURE__ */ new Date();
541
+ if (!def) {
542
+ def = createScopedDefinitionTombstone(em, inherited, scope);
543
+ } else {
544
+ markDefinitionTombstoned(def);
545
+ }
509
546
  em.persist(def);
510
547
  await em.flush();
511
548
  await guard.runAfterSuccess();
512
549
  await invalidateDefinitionsCache(cache, {
513
- tenantId: auth.tenantId ?? null,
514
- organizationId: auth.orgId ?? null,
550
+ tenantId: scope.tenantId,
551
+ organizationId: scope.organizationId,
515
552
  entityIds: [entityId]
516
553
  });
517
554
  return NextResponse.json({ ok: true });