@open-mercato/core 0.6.7-develop.6593.1.0ecf630f1e → 0.6.7-develop.6594.1.5581f765f4
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/generated/entities/custom_entity/index.js +2 -0
- package/dist/generated/entities/custom_entity/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/modules/auth/api/features.js +14 -0
- package/dist/modules/auth/api/features.js.map +2 -2
- package/dist/modules/entities/api/entities.js +20 -0
- package/dist/modules/entities/api/entities.js.map +2 -2
- package/dist/modules/entities/api/entity-settings.js +89 -0
- package/dist/modules/entities/api/entity-settings.js.map +7 -0
- package/dist/modules/entities/api/records.js +46 -25
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js +13 -4
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/create/page.js +27 -2
- package/dist/modules/entities/backend/entities/user/create/page.js.map +2 -2
- package/dist/modules/entities/data/entities.js +4 -0
- package/dist/modules/entities/data/entities.js.map +2 -2
- package/dist/modules/entities/lib/entityAcl.js +17 -4
- package/dist/modules/entities/lib/entityAcl.js.map +3 -3
- package/dist/modules/entities/lib/install-from-ce.js +2 -0
- package/dist/modules/entities/lib/install-from-ce.js.map +2 -2
- package/dist/modules/entities/lib/recordFeatures.js +24 -0
- package/dist/modules/entities/lib/recordFeatures.js.map +7 -0
- package/dist/modules/entities/lib/register.js +3 -0
- package/dist/modules/entities/lib/register.js.map +2 -2
- package/dist/modules/entities/lib/restrictedEntityFeatures.js +62 -0
- package/dist/modules/entities/lib/restrictedEntityFeatures.js.map +7 -0
- package/dist/modules/entities/migrations/Migration20260716120000.js +13 -0
- package/dist/modules/entities/migrations/Migration20260716120000.js.map +7 -0
- package/generated/entities/custom_entity/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/modules/auth/api/features.ts +17 -0
- package/src/modules/entities/api/entities.ts +28 -0
- package/src/modules/entities/api/entity-settings.ts +103 -0
- package/src/modules/entities/api/records.ts +70 -29
- package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +12 -3
- package/src/modules/entities/backend/entities/user/create/page.tsx +33 -1
- package/src/modules/entities/data/entities.ts +6 -0
- package/src/modules/entities/i18n/de.json +3 -0
- package/src/modules/entities/i18n/en.json +3 -0
- package/src/modules/entities/i18n/es.json +3 -0
- package/src/modules/entities/i18n/pl.json +3 -0
- package/src/modules/entities/lib/entityAcl.ts +27 -5
- package/src/modules/entities/lib/install-from-ce.ts +2 -0
- package/src/modules/entities/lib/recordFeatures.ts +48 -0
- package/src/modules/entities/lib/register.ts +4 -0
- package/src/modules/entities/lib/restrictedEntityFeatures.ts +98 -0
- package/src/modules/entities/migrations/.snapshot-open-mercato.json +10 -0
- package/src/modules/entities/migrations/Migration20260716120000.ts +13 -0
|
@@ -9,11 +9,27 @@ import { upsertCustomEntitySchema } from "@open-mercato/core/modules/entities/da
|
|
|
9
9
|
import { useRouter } from "next/navigation";
|
|
10
10
|
import { pushWithFlash } from "@open-mercato/ui/backend/utils/flash";
|
|
11
11
|
import { readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
12
|
+
import { LoadingMessage } from "@open-mercato/ui/backend/detail";
|
|
12
13
|
const schema = upsertCustomEntitySchema;
|
|
13
14
|
import { Page, PageBody } from "@open-mercato/ui/backend/Page";
|
|
14
15
|
function CreateEntityPage() {
|
|
15
16
|
const t = useT();
|
|
16
17
|
const router = useRouter();
|
|
18
|
+
const [defaultRestricted, setDefaultRestricted] = React.useState(null);
|
|
19
|
+
React.useEffect(() => {
|
|
20
|
+
let active = true;
|
|
21
|
+
(async () => {
|
|
22
|
+
const data = await readApiResultOrThrow(
|
|
23
|
+
"/api/entities/entity-settings",
|
|
24
|
+
void 0,
|
|
25
|
+
{ errorMessage: "[internal] Failed to load entity settings", fallback: { newEntitiesRestrictedByDefault: false } }
|
|
26
|
+
).catch(() => ({ newEntitiesRestrictedByDefault: false }));
|
|
27
|
+
if (active) setDefaultRestricted(data?.newEntitiesRestrictedByDefault === true);
|
|
28
|
+
})();
|
|
29
|
+
return () => {
|
|
30
|
+
active = false;
|
|
31
|
+
};
|
|
32
|
+
}, []);
|
|
17
33
|
const fields = React.useMemo(() => [
|
|
18
34
|
{
|
|
19
35
|
id: "entityId",
|
|
@@ -35,8 +51,17 @@ function CreateEntityPage() {
|
|
|
35
51
|
{ value: "htmlRichText", label: t("entities.userEntities.form.defaultEditor.options.htmlRichText", "HTML Rich Text") }
|
|
36
52
|
]
|
|
37
53
|
},
|
|
38
|
-
{ id: "showInSidebar", label: t("entities.userEntities.form.showInSidebar.label", "Show in sidebar"), type: "checkbox" }
|
|
54
|
+
{ id: "showInSidebar", label: t("entities.userEntities.form.showInSidebar.label", "Show in sidebar"), type: "checkbox" },
|
|
55
|
+
{
|
|
56
|
+
id: "accessRestricted",
|
|
57
|
+
label: t("entities.userEntities.form.accessRestricted.label", "Restrict record access"),
|
|
58
|
+
type: "checkbox",
|
|
59
|
+
description: t("entities.userEntities.form.accessRestricted.help", "Require an explicit per-entity permission to view or manage this entity\u2019s records. Leave off to allow anyone with the general records permission.")
|
|
60
|
+
}
|
|
39
61
|
], [t]);
|
|
62
|
+
if (defaultRestricted === null) {
|
|
63
|
+
return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { children: /* @__PURE__ */ jsx(LoadingMessage, { label: t("entities.userEntities.form.loading", "Loading\u2026") }) }) });
|
|
64
|
+
}
|
|
40
65
|
return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { children: /* @__PURE__ */ jsx(
|
|
41
66
|
CrudForm,
|
|
42
67
|
{
|
|
@@ -44,7 +69,7 @@ function CreateEntityPage() {
|
|
|
44
69
|
backHref: "/backend/entities/user",
|
|
45
70
|
schema,
|
|
46
71
|
fields,
|
|
47
|
-
initialValues: { entityId: "user:your_entity", label: "", showInSidebar: false },
|
|
72
|
+
initialValues: { entityId: "user:your_entity", label: "", showInSidebar: false, accessRestricted: defaultRestricted },
|
|
48
73
|
submitLabel: t("entities.userEntities.form.submit", "Create"),
|
|
49
74
|
cancelHref: "/backend/entities/user",
|
|
50
75
|
onSubmit: async (vals) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/entities/backend/entities/user/create/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { z } from 'zod'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { createCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { upsertCustomEntitySchema } from '@open-mercato/core/modules/entities/data/validators'\nimport { useRouter } from 'next/navigation'\nimport { pushWithFlash } from '@open-mercato/ui/backend/utils/flash'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\nconst schema = upsertCustomEntitySchema\n\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\n\nexport default function CreateEntityPage() {\n const t = useT()\n const router = useRouter()\n const fields = React.useMemo<CrudField[]>(() => ([\n {\n id: 'entityId',\n label: t('entities.userEntities.form.entityId.label', 'Entity ID'),\n type: 'text',\n required: true,\n placeholder: t('entities.userEntities.form.entityId.placeholder', 'module_name:entity_id'),\n },\n { id: 'label', label: t('entities.userEntities.form.label.label', 'Label'), type: 'text', required: true },\n { id: 'description', label: t('entities.userEntities.form.description.label', 'Description'), type: 'textarea' },\n {\n id: 'defaultEditor',\n label: t('entities.userEntities.form.defaultEditor.label', 'Default Editor (multiline)'),\n type: 'select',\n options: [\n { value: '', label: t('entities.userEntities.form.defaultEditor.options.default', 'Default (Markdown)') },\n { value: 'markdown', label: t('entities.userEntities.form.defaultEditor.options.markdown', 'Markdown (UIW)') },\n { value: 'simpleMarkdown', label: t('entities.userEntities.form.defaultEditor.options.simpleMarkdown', 'Simple Markdown') },\n { value: 'htmlRichText', label: t('entities.userEntities.form.defaultEditor.options.htmlRichText', 'HTML Rich Text') },\n ],\n } as unknown as CrudField,\n { id: 'showInSidebar', label: t('entities.userEntities.form.showInSidebar.label', 'Show in sidebar'), type: 'checkbox' } as CrudField,\n ]), [t])\n\n return (\n <Page>\n <PageBody>\n <CrudForm\n title={t('entities.userEntities.form.title', 'Create Entity')}\n backHref=\"/backend/entities/user\"\n schema={schema}\n fields={fields}\n initialValues={{ entityId: 'user:your_entity', label: '', showInSidebar: false }}\n submitLabel={t('entities.userEntities.form.submit', 'Create')}\n cancelHref=\"/backend/entities/user\"\n onSubmit={async (vals) => {\n const entityId = await submitCreateEntity({\n values: vals as Record<string, unknown>,\n messages: {\n entityIdRequired: t('entities.userEntities.errors.entityIdRequired', 'Entity ID is required'),\n entityIdExists: t('entities.userEntities.errors.entityIdExists', 'Entity ID already exists'),\n loadFailed: t('entities.userEntities.errors.loadFailed', 'Failed to load entities'),\n },\n })\n try {\n window.dispatchEvent(new Event('om:refresh-sidebar'))\n } catch {}\n const successMessage = t('entities.userEntities.flash.created', 'Entity created')\n pushWithFlash(router, `/backend/entities/user/${encodeURIComponent(entityId)}`, successMessage, 'success')\n }}\n />\n </PageBody>\n </Page>\n )\n}\n\ntype EntityListEntry = {\n entityId?: string\n source?: string\n}\n\ntype FetchCustomEntities = (errorMessage?: string) => Promise<EntityListEntry[]>\n\nasync function defaultFetchCustomEntities(errorMessage?: string): Promise<EntityListEntry[]> {\n const data = await readApiResultOrThrow<{ items?: EntityListEntry[] }>(\n '/api/entities/entities',\n undefined,\n { errorMessage: errorMessage ?? 'Failed to load entities', fallback: { items: [] } },\n )\n return Array.isArray(data?.items) ? data.items! : []\n}\n\ntype CreateEntityRequest = (payload: Record<string, unknown>) => Promise<void>\n\nasync function defaultCreateEntityRequest(payload: Record<string, unknown>) {\n await createCrud('entities/entities', payload)\n}\n\nexport async function submitCreateEntity(options: {\n values: Record<string, unknown>\n fetchEntities?: FetchCustomEntities\n createEntity?: CreateEntityRequest\n messages?: {\n entityIdRequired?: string\n entityIdExists?: string\n loadFailed?: string\n }\n}): Promise<string> {\n const {\n values,\n fetchEntities = defaultFetchCustomEntities,\n createEntity = defaultCreateEntityRequest,\n messages,\n } = options\n const rawEntityId = typeof values.entityId === 'string' ? values.entityId.trim() : ''\n if (!rawEntityId) {\n const message = messages?.entityIdRequired ?? 'Entity ID is required'\n throw createCrudFormError(message, { entityId: message })\n }\n\n const existing = await fetchEntities(messages?.loadFailed)\n const exists = existing.some(\n (entry) => entry?.entityId === rawEntityId && (entry?.source === 'custom' || entry?.source === undefined),\n )\n if (exists) {\n const message = messages?.entityIdExists ?? 'Entity ID already exists'\n throw createCrudFormError(message, { entityId: message })\n }\n\n const payload: Record<string, unknown> = {\n ...values,\n entityId: rawEntityId,\n labelField: 'name',\n defaultEditor:\n typeof (values as Record<string, unknown>).defaultEditor === 'string' &&\n (values as Record<string, unknown>).defaultEditor\n ? (values as Record<string, unknown>).defaultEditor\n : undefined,\n }\n\n await createEntity(payload)\n return rawEntityId\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { z } from 'zod'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { createCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { upsertCustomEntitySchema } from '@open-mercato/core/modules/entities/data/validators'\nimport { useRouter } from 'next/navigation'\nimport { pushWithFlash } from '@open-mercato/ui/backend/utils/flash'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { LoadingMessage } from '@open-mercato/ui/backend/detail'\n\nconst schema = upsertCustomEntitySchema\n\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\n\nexport default function CreateEntityPage() {\n const t = useT()\n const router = useRouter()\n // Pre-fill the restriction toggle from the tenant default-restricted policy so\n // the form reflects the workspace posture; null while loading.\n const [defaultRestricted, setDefaultRestricted] = React.useState<boolean | null>(null)\n React.useEffect(() => {\n let active = true\n ;(async () => {\n const data = await readApiResultOrThrow<{ newEntitiesRestrictedByDefault?: boolean }>(\n '/api/entities/entity-settings',\n undefined,\n { errorMessage: '[internal] Failed to load entity settings', fallback: { newEntitiesRestrictedByDefault: false } },\n ).catch(() => ({ newEntitiesRestrictedByDefault: false }))\n if (active) setDefaultRestricted(data?.newEntitiesRestrictedByDefault === true)\n })()\n return () => { active = false }\n }, [])\n const fields = React.useMemo<CrudField[]>(() => ([\n {\n id: 'entityId',\n label: t('entities.userEntities.form.entityId.label', 'Entity ID'),\n type: 'text',\n required: true,\n placeholder: t('entities.userEntities.form.entityId.placeholder', 'module_name:entity_id'),\n },\n { id: 'label', label: t('entities.userEntities.form.label.label', 'Label'), type: 'text', required: true },\n { id: 'description', label: t('entities.userEntities.form.description.label', 'Description'), type: 'textarea' },\n {\n id: 'defaultEditor',\n label: t('entities.userEntities.form.defaultEditor.label', 'Default Editor (multiline)'),\n type: 'select',\n options: [\n { value: '', label: t('entities.userEntities.form.defaultEditor.options.default', 'Default (Markdown)') },\n { value: 'markdown', label: t('entities.userEntities.form.defaultEditor.options.markdown', 'Markdown (UIW)') },\n { value: 'simpleMarkdown', label: t('entities.userEntities.form.defaultEditor.options.simpleMarkdown', 'Simple Markdown') },\n { value: 'htmlRichText', label: t('entities.userEntities.form.defaultEditor.options.htmlRichText', 'HTML Rich Text') },\n ],\n } as unknown as CrudField,\n { id: 'showInSidebar', label: t('entities.userEntities.form.showInSidebar.label', 'Show in sidebar'), type: 'checkbox' } as CrudField,\n {\n id: 'accessRestricted',\n label: t('entities.userEntities.form.accessRestricted.label', 'Restrict record access'),\n type: 'checkbox',\n description: t('entities.userEntities.form.accessRestricted.help', 'Require an explicit per-entity permission to view or manage this entity\u2019s records. Leave off to allow anyone with the general records permission.'),\n } as CrudField,\n ]), [t])\n\n if (defaultRestricted === null) {\n return (\n <Page>\n <PageBody>\n <LoadingMessage label={t('entities.userEntities.form.loading', 'Loading\u2026')} />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <CrudForm\n title={t('entities.userEntities.form.title', 'Create Entity')}\n backHref=\"/backend/entities/user\"\n schema={schema}\n fields={fields}\n initialValues={{ entityId: 'user:your_entity', label: '', showInSidebar: false, accessRestricted: defaultRestricted }}\n submitLabel={t('entities.userEntities.form.submit', 'Create')}\n cancelHref=\"/backend/entities/user\"\n onSubmit={async (vals) => {\n const entityId = await submitCreateEntity({\n values: vals as Record<string, unknown>,\n messages: {\n entityIdRequired: t('entities.userEntities.errors.entityIdRequired', 'Entity ID is required'),\n entityIdExists: t('entities.userEntities.errors.entityIdExists', 'Entity ID already exists'),\n loadFailed: t('entities.userEntities.errors.loadFailed', 'Failed to load entities'),\n },\n })\n try {\n window.dispatchEvent(new Event('om:refresh-sidebar'))\n } catch {}\n const successMessage = t('entities.userEntities.flash.created', 'Entity created')\n pushWithFlash(router, `/backend/entities/user/${encodeURIComponent(entityId)}`, successMessage, 'success')\n }}\n />\n </PageBody>\n </Page>\n )\n}\n\ntype EntityListEntry = {\n entityId?: string\n source?: string\n}\n\ntype FetchCustomEntities = (errorMessage?: string) => Promise<EntityListEntry[]>\n\nasync function defaultFetchCustomEntities(errorMessage?: string): Promise<EntityListEntry[]> {\n const data = await readApiResultOrThrow<{ items?: EntityListEntry[] }>(\n '/api/entities/entities',\n undefined,\n { errorMessage: errorMessage ?? 'Failed to load entities', fallback: { items: [] } },\n )\n return Array.isArray(data?.items) ? data.items! : []\n}\n\ntype CreateEntityRequest = (payload: Record<string, unknown>) => Promise<void>\n\nasync function defaultCreateEntityRequest(payload: Record<string, unknown>) {\n await createCrud('entities/entities', payload)\n}\n\nexport async function submitCreateEntity(options: {\n values: Record<string, unknown>\n fetchEntities?: FetchCustomEntities\n createEntity?: CreateEntityRequest\n messages?: {\n entityIdRequired?: string\n entityIdExists?: string\n loadFailed?: string\n }\n}): Promise<string> {\n const {\n values,\n fetchEntities = defaultFetchCustomEntities,\n createEntity = defaultCreateEntityRequest,\n messages,\n } = options\n const rawEntityId = typeof values.entityId === 'string' ? values.entityId.trim() : ''\n if (!rawEntityId) {\n const message = messages?.entityIdRequired ?? 'Entity ID is required'\n throw createCrudFormError(message, { entityId: message })\n }\n\n const existing = await fetchEntities(messages?.loadFailed)\n const exists = existing.some(\n (entry) => entry?.entityId === rawEntityId && (entry?.source === 'custom' || entry?.source === undefined),\n )\n if (exists) {\n const message = messages?.entityIdExists ?? 'Entity ID already exists'\n throw createCrudFormError(message, { entityId: message })\n }\n\n const payload: Record<string, unknown> = {\n ...values,\n entityId: rawEntityId,\n labelField: 'name',\n defaultEditor:\n typeof (values as Record<string, unknown>).defaultEditor === 'string' &&\n (values as Record<string, unknown>).defaultEditor\n ? (values as Record<string, unknown>).defaultEditor\n : undefined,\n }\n\n await createEntity(payload)\n return rawEntityId\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -125,6 +125,7 @@ CustomFieldEntityConfig = __decorateClass([
|
|
|
125
125
|
let CustomEntity = class {
|
|
126
126
|
constructor() {
|
|
127
127
|
this.showInSidebar = false;
|
|
128
|
+
this.accessRestricted = false;
|
|
128
129
|
this.isActive = true;
|
|
129
130
|
this.createdAt = /* @__PURE__ */ new Date();
|
|
130
131
|
this.updatedAt = /* @__PURE__ */ new Date();
|
|
@@ -151,6 +152,9 @@ __decorateClass([
|
|
|
151
152
|
__decorateClass([
|
|
152
153
|
Property({ name: "show_in_sidebar", type: "boolean", default: false })
|
|
153
154
|
], CustomEntity.prototype, "showInSidebar", 2);
|
|
155
|
+
__decorateClass([
|
|
156
|
+
Property({ name: "access_restricted", type: "boolean", default: false })
|
|
157
|
+
], CustomEntity.prototype, "accessRestricted", 2);
|
|
154
158
|
__decorateClass([
|
|
155
159
|
Property({ name: "organization_id", type: "uuid", nullable: true })
|
|
156
160
|
], CustomEntity.prototype, "organizationId", 2);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/entities/data/entities.ts"],
|
|
4
|
-
"sourcesContent": ["import { Entity, Index, PrimaryKey, Property } from '@mikro-orm/decorators/legacy'\n\n// Definitions of custom fields scoped to an entity type and organization\n@Entity({ tableName: 'custom_field_defs' })\n@Index({\n name: 'cf_defs_entity_tenant_org_idx',\n properties: ['entityId', 'tenantId', 'organizationId'],\n})\n@Index({\n name: 'cf_defs_entity_tenant_idx',\n properties: ['entityId', 'tenantId'],\n})\n@Index({\n name: 'cf_defs_entity_org_idx',\n properties: ['entityId', 'organizationId'],\n})\n@Index({\n name: 'cf_defs_entity_global_idx',\n properties: ['entityId'],\n})\n@Index({\n name: 'cf_defs_entity_key_scope_idx',\n properties: ['entityId', 'key', 'tenantId', 'organizationId'],\n})\nexport class CustomFieldDef {\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n // Entity identifier: '<module>:<entity>'\n @Property({ name: 'entity_id', type: 'text' })\n entityId!: string\n\n // Organization scope (nullable for global)\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n // Tenant scope (nullable for global)\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n // Unique key within entity scope\n @Property({ type: 'text' })\n @Index({ name: 'cf_defs_entity_key_idx' })\n key!: string\n\n // Field kind: text|multiline|integer|float|boolean|select|currency\n @Property({ type: 'text' })\n kind!: string\n\n // Optional select options or metadata in JSON\n @Property({ name: 'config_json', type: 'json', nullable: true })\n configJson?: any\n\n @Property({ name: 'is_active', type: 'boolean', default: true })\n isActive: boolean = true\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n\n@Entity({ tableName: 'custom_field_entity_configs' })\n@Index({\n name: 'cf_entity_cfgs_entity_scope_idx',\n properties: ['entityId', 'tenantId', 'organizationId'],\n})\n@Index({\n name: 'cf_entity_cfgs_entity_tenant_idx',\n properties: ['entityId', 'tenantId'],\n})\n@Index({\n name: 'cf_entity_cfgs_entity_org_idx',\n properties: ['entityId', 'organizationId'],\n})\nexport class CustomFieldEntityConfig {\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'entity_id', type: 'text' })\n entityId!: string\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n @Property({ name: 'config_json', type: 'jsonb', nullable: true })\n configJson?: Record<string, unknown> | null\n\n @Property({ name: 'is_active', type: 'boolean', default: true })\n isActive: boolean = true\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n\n// User-defined logical entities registry (for dynamic data types)\n@Entity({ tableName: 'custom_entities' })\n@Index({ name: 'custom_entities_unique_idx', properties: ['entityId', 'organizationId', 'tenantId'], options: { unique: true } })\nexport class CustomEntity {\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n // Identifier: '<module>:<entity>' (snake_case entity part preferred)\n @Property({ name: 'entity_id', type: 'text' })\n entityId!: string\n\n @Property({ type: 'text' })\n label!: string\n\n @Property({ type: 'text', nullable: true })\n description?: string | null\n\n // Preferred display label field for relation options (e.g., 'name')\n @Property({ name: 'label_field', type: 'text', nullable: true })\n labelField?: string | null\n\n // Default editor preference for multiline custom fields\n // Allowed: 'markdown' | 'simpleMarkdown' | 'htmlRichText'\n @Property({ name: 'default_editor', type: 'text', nullable: true })\n defaultEditor?: string | null\n\n // Whether to show this entity in the sidebar navigation\n @Property({ name: 'show_in_sidebar', type: 'boolean', default: false })\n showInSidebar: boolean = false\n\n // Note: Per-field UI preferences (list visibility, filter visibility, form editability)\n // are stored in CustomFieldDef.configJson, not at entity level.\n\n // Optional org/tenant scoping\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n @Property({ name: 'is_active', type: 'boolean', default: true })\n isActive: boolean = true\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n\n// Storage for custom entity records (JSONB document store)\n@Entity({ tableName: 'custom_entities_storage' })\n@Index({ name: 'custom_entities_storage_unique_idx', properties: ['entityType', 'entityId', 'organizationId'], options: { unique: true } })\nexport class CustomEntityStorage {\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'entity_type', type: 'text' })\n entityType!: string\n\n @Property({ name: 'entity_id', type: 'text' })\n entityId!: string\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n @Property({ name: 'doc', type: 'json' })\n doc!: any\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n\n// Values for custom fields (EAV); recordId is a text to support any PK\n@Entity({ tableName: 'custom_field_values' })\n@Index({\n name: 'cf_values_entity_record_tenant_idx',\n properties: ['entityId', 'recordId', 'tenantId'],\n})\nexport class CustomFieldValue {\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'entity_id', type: 'text' })\n entityId!: string\n\n // Text to support int/uuid PKs equally\n @Property({ name: 'record_id', type: 'text' })\n recordId!: string\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n // Field key for lookup; resolves to a CustomFieldDef\n @Property({ name: 'field_key', type: 'text' })\n @Index({ name: 'cf_values_entity_record_field_idx' })\n fieldKey!: string\n\n // One of the following value columns is used based on kind\n @Property({ name: 'value_text', type: 'text', nullable: true })\n valueText?: string | null\n\n @Property({ name: 'value_multiline', type: 'text', nullable: true })\n valueMultiline?: string | null\n\n @Property({ name: 'value_int', type: 'int', nullable: true })\n valueInt?: number | null\n\n @Property({ name: 'value_float', type: 'float', nullable: true })\n valueFloat?: number | null\n\n @Property({ name: 'value_bool', type: 'boolean', nullable: true })\n valueBool?: boolean | null\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n\n// Encryption maps declared per entity/tenant/organization\n@Entity({ tableName: 'encryption_maps' })\n@Index({\n name: 'encryption_maps_entity_scope_idx',\n properties: ['entityId', 'tenantId', 'organizationId'],\n})\nexport class EncryptionMap {\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'entity_id', type: 'text' })\n entityId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'fields_json', type: 'jsonb', nullable: true })\n fieldsJson?: Array<{ field: string; hashField?: string | null }> | null\n\n @Property({ name: 'is_active', type: 'boolean', default: true })\n isActive: boolean = true\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { Entity, Index, PrimaryKey, Property } from '@mikro-orm/decorators/legacy'\n\n// Definitions of custom fields scoped to an entity type and organization\n@Entity({ tableName: 'custom_field_defs' })\n@Index({\n name: 'cf_defs_entity_tenant_org_idx',\n properties: ['entityId', 'tenantId', 'organizationId'],\n})\n@Index({\n name: 'cf_defs_entity_tenant_idx',\n properties: ['entityId', 'tenantId'],\n})\n@Index({\n name: 'cf_defs_entity_org_idx',\n properties: ['entityId', 'organizationId'],\n})\n@Index({\n name: 'cf_defs_entity_global_idx',\n properties: ['entityId'],\n})\n@Index({\n name: 'cf_defs_entity_key_scope_idx',\n properties: ['entityId', 'key', 'tenantId', 'organizationId'],\n})\nexport class CustomFieldDef {\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n // Entity identifier: '<module>:<entity>'\n @Property({ name: 'entity_id', type: 'text' })\n entityId!: string\n\n // Organization scope (nullable for global)\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n // Tenant scope (nullable for global)\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n // Unique key within entity scope\n @Property({ type: 'text' })\n @Index({ name: 'cf_defs_entity_key_idx' })\n key!: string\n\n // Field kind: text|multiline|integer|float|boolean|select|currency\n @Property({ type: 'text' })\n kind!: string\n\n // Optional select options or metadata in JSON\n @Property({ name: 'config_json', type: 'json', nullable: true })\n configJson?: any\n\n @Property({ name: 'is_active', type: 'boolean', default: true })\n isActive: boolean = true\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n\n@Entity({ tableName: 'custom_field_entity_configs' })\n@Index({\n name: 'cf_entity_cfgs_entity_scope_idx',\n properties: ['entityId', 'tenantId', 'organizationId'],\n})\n@Index({\n name: 'cf_entity_cfgs_entity_tenant_idx',\n properties: ['entityId', 'tenantId'],\n})\n@Index({\n name: 'cf_entity_cfgs_entity_org_idx',\n properties: ['entityId', 'organizationId'],\n})\nexport class CustomFieldEntityConfig {\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'entity_id', type: 'text' })\n entityId!: string\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n @Property({ name: 'config_json', type: 'jsonb', nullable: true })\n configJson?: Record<string, unknown> | null\n\n @Property({ name: 'is_active', type: 'boolean', default: true })\n isActive: boolean = true\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n\n// User-defined logical entities registry (for dynamic data types)\n@Entity({ tableName: 'custom_entities' })\n@Index({ name: 'custom_entities_unique_idx', properties: ['entityId', 'organizationId', 'tenantId'], options: { unique: true } })\nexport class CustomEntity {\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n // Identifier: '<module>:<entity>' (snake_case entity part preferred)\n @Property({ name: 'entity_id', type: 'text' })\n entityId!: string\n\n @Property({ type: 'text' })\n label!: string\n\n @Property({ type: 'text', nullable: true })\n description?: string | null\n\n // Preferred display label field for relation options (e.g., 'name')\n @Property({ name: 'label_field', type: 'text', nullable: true })\n labelField?: string | null\n\n // Default editor preference for multiline custom fields\n // Allowed: 'markdown' | 'simpleMarkdown' | 'htmlRichText'\n @Property({ name: 'default_editor', type: 'text', nullable: true })\n defaultEditor?: string | null\n\n // Whether to show this entity in the sidebar navigation\n @Property({ name: 'show_in_sidebar', type: 'boolean', default: false })\n showInSidebar: boolean = false\n\n // When true, records require an explicit per-entity ACL grant\n // (entities.records.<entity_id>.view/.manage) beyond the coarse\n // entities.records.* feature. Defaults to unrestricted for backward compat.\n @Property({ name: 'access_restricted', type: 'boolean', default: false })\n accessRestricted: boolean = false\n\n // Note: Per-field UI preferences (list visibility, filter visibility, form editability)\n // are stored in CustomFieldDef.configJson, not at entity level.\n\n // Optional org/tenant scoping\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n @Property({ name: 'is_active', type: 'boolean', default: true })\n isActive: boolean = true\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n\n// Storage for custom entity records (JSONB document store)\n@Entity({ tableName: 'custom_entities_storage' })\n@Index({ name: 'custom_entities_storage_unique_idx', properties: ['entityType', 'entityId', 'organizationId'], options: { unique: true } })\nexport class CustomEntityStorage {\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'entity_type', type: 'text' })\n entityType!: string\n\n @Property({ name: 'entity_id', type: 'text' })\n entityId!: string\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n @Property({ name: 'doc', type: 'json' })\n doc!: any\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n\n// Values for custom fields (EAV); recordId is a text to support any PK\n@Entity({ tableName: 'custom_field_values' })\n@Index({\n name: 'cf_values_entity_record_tenant_idx',\n properties: ['entityId', 'recordId', 'tenantId'],\n})\nexport class CustomFieldValue {\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'entity_id', type: 'text' })\n entityId!: string\n\n // Text to support int/uuid PKs equally\n @Property({ name: 'record_id', type: 'text' })\n recordId!: string\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n // Field key for lookup; resolves to a CustomFieldDef\n @Property({ name: 'field_key', type: 'text' })\n @Index({ name: 'cf_values_entity_record_field_idx' })\n fieldKey!: string\n\n // One of the following value columns is used based on kind\n @Property({ name: 'value_text', type: 'text', nullable: true })\n valueText?: string | null\n\n @Property({ name: 'value_multiline', type: 'text', nullable: true })\n valueMultiline?: string | null\n\n @Property({ name: 'value_int', type: 'int', nullable: true })\n valueInt?: number | null\n\n @Property({ name: 'value_float', type: 'float', nullable: true })\n valueFloat?: number | null\n\n @Property({ name: 'value_bool', type: 'boolean', nullable: true })\n valueBool?: boolean | null\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n\n// Encryption maps declared per entity/tenant/organization\n@Entity({ tableName: 'encryption_maps' })\n@Index({\n name: 'encryption_maps_entity_scope_idx',\n properties: ['entityId', 'tenantId', 'organizationId'],\n})\nexport class EncryptionMap {\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'entity_id', type: 'text' })\n entityId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'fields_json', type: 'jsonb', nullable: true })\n fieldsJson?: Array<{ field: string; hashField?: string | null }> | null\n\n @Property({ name: 'is_active', type: 'boolean', default: true })\n isActive: boolean = true\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
2
2
|
import { hasAllFeatures } from "@open-mercato/shared/security/features";
|
|
3
|
+
import { deriveCustomEntityRecordFeature } from "./recordFeatures.js";
|
|
3
4
|
const ENTITY_ACL_REQUIREMENTS = {
|
|
4
5
|
"directory:tenant": {
|
|
5
6
|
view: ["directory.tenants.view"],
|
|
@@ -61,13 +62,25 @@ function resolveEntityAclRequirement(entityId) {
|
|
|
61
62
|
function forbiddenEntityAccess() {
|
|
62
63
|
return new CrudHttpError(403, { error: "Forbidden" });
|
|
63
64
|
}
|
|
64
|
-
async function
|
|
65
|
-
|
|
66
|
-
const requirement = resolveEntityAclRequirement(args.entityId);
|
|
67
|
-
const acl = await args.rbac.loadAcl(args.auth.sub ?? "", {
|
|
65
|
+
async function loadActorAcl(args) {
|
|
66
|
+
return args.rbac.loadAcl(args.auth.sub ?? "", {
|
|
68
67
|
tenantId: args.auth.tenantId ?? null,
|
|
69
68
|
organizationId: args.auth.orgId ?? null
|
|
70
69
|
});
|
|
70
|
+
}
|
|
71
|
+
async function assertEntityAclForRequest(args) {
|
|
72
|
+
if (args.isCustomEntity) {
|
|
73
|
+
if (!args.isRestricted) return;
|
|
74
|
+
const acl2 = await loadActorAcl(args);
|
|
75
|
+
if (acl2?.isSuperAdmin) return;
|
|
76
|
+
const required = deriveCustomEntityRecordFeature(args.entityId, args.action);
|
|
77
|
+
if (!hasAllFeatures(acl2?.features, [required])) {
|
|
78
|
+
throw forbiddenEntityAccess();
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const requirement = resolveEntityAclRequirement(args.entityId);
|
|
83
|
+
const acl = await loadActorAcl(args);
|
|
71
84
|
if (acl?.isSuperAdmin) return;
|
|
72
85
|
if (!requirement) throw forbiddenEntityAccess();
|
|
73
86
|
if (requirement.platformOnly) throw forbiddenEntityAccess();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/entities/lib/entityAcl.ts"],
|
|
4
|
-
"sourcesContent": ["import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\n\nexport type EntityAclRequirement = {\n view: string[]\n manage: string[]\n platformOnly?: boolean\n}\n\nconst ENTITY_ACL_REQUIREMENTS: Record<string, EntityAclRequirement> = {\n 'directory:tenant': {\n view: ['directory.tenants.view'],\n manage: ['directory.tenants.manage'],\n platformOnly: true,\n },\n 'directory:organization': {\n view: ['directory.organizations.view'],\n manage: ['directory.organizations.manage'],\n },\n 'customers:customer_person_profile': {\n view: ['customers.people.view'],\n manage: ['customers.people.manage'],\n },\n 'customers:customer_company_profile': {\n view: ['customers.companies.view'],\n manage: ['customers.companies.manage'],\n },\n 'customers:customer_deal': {\n view: ['customers.deals.view'],\n manage: ['customers.deals.manage'],\n },\n 'customers:customer_activity': {\n view: ['customers.activities.view'],\n manage: ['customers.activities.manage'],\n },\n 'customers:customer_interaction': {\n view: ['customers.interactions.view'],\n manage: ['customers.interactions.manage'],\n },\n 'catalog:catalog_product': {\n view: ['catalog.products.view'],\n manage: ['catalog.products.manage'],\n },\n 'catalog:catalog_product_category': {\n view: ['catalog.categories.view'],\n manage: ['catalog.categories.manage'],\n },\n 'sales:sales_order': {\n view: ['sales.orders.view'],\n manage: ['sales.orders.manage'],\n },\n 'sales:sales_quote': {\n view: ['sales.quotes.view'],\n manage: ['sales.quotes.manage'],\n },\n 'auth:user': {\n view: ['auth.users.list'],\n manage: ['auth.users.edit'],\n },\n 'auth:role': {\n view: ['auth.roles.list'],\n manage: ['auth.roles.manage'],\n },\n}\n\nexport function resolveEntityAclRequirement(entityId: string): EntityAclRequirement | null {\n return ENTITY_ACL_REQUIREMENTS[entityId] ?? null\n}\n\ntype EntityAclActor = {\n sub?: string | null\n tenantId?: string | null\n orgId?: string | null\n isSuperAdmin?: boolean\n}\n\ntype AssertEntityAclArgs = {\n auth: EntityAclActor\n entityId: string\n action: 'view' | 'manage'\n isCustomEntity: boolean\n rbac: RbacService\n}\n\nfunction forbiddenEntityAccess(): CrudHttpError {\n return new CrudHttpError(403, { error: 'Forbidden' })\n}\n\nexport async function assertEntityAclForRequest(args: AssertEntityAclArgs): Promise<void> {\n if (args.isCustomEntity)
|
|
5
|
-
"mappings": "AAAA,SAAS,qBAAqB;AAC9B,SAAS,sBAAsB;
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { deriveCustomEntityRecordFeature } from './recordFeatures'\n\nexport type EntityAclRequirement = {\n view: string[]\n manage: string[]\n platformOnly?: boolean\n}\n\nconst ENTITY_ACL_REQUIREMENTS: Record<string, EntityAclRequirement> = {\n 'directory:tenant': {\n view: ['directory.tenants.view'],\n manage: ['directory.tenants.manage'],\n platformOnly: true,\n },\n 'directory:organization': {\n view: ['directory.organizations.view'],\n manage: ['directory.organizations.manage'],\n },\n 'customers:customer_person_profile': {\n view: ['customers.people.view'],\n manage: ['customers.people.manage'],\n },\n 'customers:customer_company_profile': {\n view: ['customers.companies.view'],\n manage: ['customers.companies.manage'],\n },\n 'customers:customer_deal': {\n view: ['customers.deals.view'],\n manage: ['customers.deals.manage'],\n },\n 'customers:customer_activity': {\n view: ['customers.activities.view'],\n manage: ['customers.activities.manage'],\n },\n 'customers:customer_interaction': {\n view: ['customers.interactions.view'],\n manage: ['customers.interactions.manage'],\n },\n 'catalog:catalog_product': {\n view: ['catalog.products.view'],\n manage: ['catalog.products.manage'],\n },\n 'catalog:catalog_product_category': {\n view: ['catalog.categories.view'],\n manage: ['catalog.categories.manage'],\n },\n 'sales:sales_order': {\n view: ['sales.orders.view'],\n manage: ['sales.orders.manage'],\n },\n 'sales:sales_quote': {\n view: ['sales.quotes.view'],\n manage: ['sales.quotes.manage'],\n },\n 'auth:user': {\n view: ['auth.users.list'],\n manage: ['auth.users.edit'],\n },\n 'auth:role': {\n view: ['auth.roles.list'],\n manage: ['auth.roles.manage'],\n },\n}\n\nexport function resolveEntityAclRequirement(entityId: string): EntityAclRequirement | null {\n return ENTITY_ACL_REQUIREMENTS[entityId] ?? null\n}\n\ntype EntityAclActor = {\n sub?: string | null\n tenantId?: string | null\n orgId?: string | null\n isSuperAdmin?: boolean\n}\n\ntype AssertEntityAclArgs = {\n auth: EntityAclActor\n entityId: string\n action: 'view' | 'manage'\n isCustomEntity: boolean\n // Set for a custom entity flagged `access_restricted`. When true, the coarse\n // route-level entities.records.* feature is no longer sufficient \u2014 the caller\n // must additionally hold the synthesized per-entity feature.\n isRestricted?: boolean\n rbac: RbacService\n}\n\nfunction forbiddenEntityAccess(): CrudHttpError {\n return new CrudHttpError(403, { error: 'Forbidden' })\n}\n\nasync function loadActorAcl(args: AssertEntityAclArgs) {\n return args.rbac.loadAcl(args.auth.sub ?? '', {\n tenantId: args.auth.tenantId ?? null,\n organizationId: args.auth.orgId ?? null,\n })\n}\n\nexport async function assertEntityAclForRequest(args: AssertEntityAclArgs): Promise<void> {\n if (args.isCustomEntity) {\n // Unrestricted custom entities keep the historical behavior: the coarse\n // entities.records.view/.manage route guard is the whole authorization.\n if (!args.isRestricted) return\n\n const acl = await loadActorAcl(args)\n if (acl?.isSuperAdmin) return\n\n const required = deriveCustomEntityRecordFeature(args.entityId, args.action)\n if (!hasAllFeatures(acl?.features, [required])) {\n throw forbiddenEntityAccess()\n }\n return\n }\n\n const requirement = resolveEntityAclRequirement(args.entityId)\n\n const acl = await loadActorAcl(args)\n\n if (acl?.isSuperAdmin) return\n\n if (!requirement) throw forbiddenEntityAccess()\n if (requirement.platformOnly) throw forbiddenEntityAccess()\n\n if (!hasAllFeatures(acl?.features, requirement[args.action])) {\n throw forbiddenEntityAccess()\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,qBAAqB;AAC9B,SAAS,sBAAsB;AAE/B,SAAS,uCAAuC;AAQhD,MAAM,0BAAgE;AAAA,EACpE,oBAAoB;AAAA,IAClB,MAAM,CAAC,wBAAwB;AAAA,IAC/B,QAAQ,CAAC,0BAA0B;AAAA,IACnC,cAAc;AAAA,EAChB;AAAA,EACA,0BAA0B;AAAA,IACxB,MAAM,CAAC,8BAA8B;AAAA,IACrC,QAAQ,CAAC,gCAAgC;AAAA,EAC3C;AAAA,EACA,qCAAqC;AAAA,IACnC,MAAM,CAAC,uBAAuB;AAAA,IAC9B,QAAQ,CAAC,yBAAyB;AAAA,EACpC;AAAA,EACA,sCAAsC;AAAA,IACpC,MAAM,CAAC,0BAA0B;AAAA,IACjC,QAAQ,CAAC,4BAA4B;AAAA,EACvC;AAAA,EACA,2BAA2B;AAAA,IACzB,MAAM,CAAC,sBAAsB;AAAA,IAC7B,QAAQ,CAAC,wBAAwB;AAAA,EACnC;AAAA,EACA,+BAA+B;AAAA,IAC7B,MAAM,CAAC,2BAA2B;AAAA,IAClC,QAAQ,CAAC,6BAA6B;AAAA,EACxC;AAAA,EACA,kCAAkC;AAAA,IAChC,MAAM,CAAC,6BAA6B;AAAA,IACpC,QAAQ,CAAC,+BAA+B;AAAA,EAC1C;AAAA,EACA,2BAA2B;AAAA,IACzB,MAAM,CAAC,uBAAuB;AAAA,IAC9B,QAAQ,CAAC,yBAAyB;AAAA,EACpC;AAAA,EACA,oCAAoC;AAAA,IAClC,MAAM,CAAC,yBAAyB;AAAA,IAChC,QAAQ,CAAC,2BAA2B;AAAA,EACtC;AAAA,EACA,qBAAqB;AAAA,IACnB,MAAM,CAAC,mBAAmB;AAAA,IAC1B,QAAQ,CAAC,qBAAqB;AAAA,EAChC;AAAA,EACA,qBAAqB;AAAA,IACnB,MAAM,CAAC,mBAAmB;AAAA,IAC1B,QAAQ,CAAC,qBAAqB;AAAA,EAChC;AAAA,EACA,aAAa;AAAA,IACX,MAAM,CAAC,iBAAiB;AAAA,IACxB,QAAQ,CAAC,iBAAiB;AAAA,EAC5B;AAAA,EACA,aAAa;AAAA,IACX,MAAM,CAAC,iBAAiB;AAAA,IACxB,QAAQ,CAAC,mBAAmB;AAAA,EAC9B;AACF;AAEO,SAAS,4BAA4B,UAA+C;AACzF,SAAO,wBAAwB,QAAQ,KAAK;AAC9C;AAqBA,SAAS,wBAAuC;AAC9C,SAAO,IAAI,cAAc,KAAK,EAAE,OAAO,YAAY,CAAC;AACtD;AAEA,eAAe,aAAa,MAA2B;AACrD,SAAO,KAAK,KAAK,QAAQ,KAAK,KAAK,OAAO,IAAI;AAAA,IAC5C,UAAU,KAAK,KAAK,YAAY;AAAA,IAChC,gBAAgB,KAAK,KAAK,SAAS;AAAA,EACrC,CAAC;AACH;AAEA,eAAsB,0BAA0B,MAA0C;AACxF,MAAI,KAAK,gBAAgB;AAGvB,QAAI,CAAC,KAAK,aAAc;AAExB,UAAMA,OAAM,MAAM,aAAa,IAAI;AACnC,QAAIA,MAAK,aAAc;AAEvB,UAAM,WAAW,gCAAgC,KAAK,UAAU,KAAK,MAAM;AAC3E,QAAI,CAAC,eAAeA,MAAK,UAAU,CAAC,QAAQ,CAAC,GAAG;AAC9C,YAAM,sBAAsB;AAAA,IAC9B;AACA;AAAA,EACF;AAEA,QAAM,cAAc,4BAA4B,KAAK,QAAQ;AAE7D,QAAM,MAAM,MAAM,aAAa,IAAI;AAEnC,MAAI,KAAK,aAAc;AAEvB,MAAI,CAAC,YAAa,OAAM,sBAAsB;AAC9C,MAAI,YAAY,aAAc,OAAM,sBAAsB;AAE1D,MAAI,CAAC,eAAe,KAAK,UAAU,YAAY,KAAK,MAAM,CAAC,GAAG;AAC5D,UAAM,sBAAsB;AAAA,EAC9B;AACF;",
|
|
6
|
+
"names": ["acl"]
|
|
7
7
|
}
|
|
@@ -101,6 +101,7 @@ function buildChecksumPayload(params) {
|
|
|
101
101
|
labelField: spec?.labelField ?? null,
|
|
102
102
|
defaultEditor: spec?.defaultEditor ?? null,
|
|
103
103
|
showInSidebar: spec?.showInSidebar ?? false,
|
|
104
|
+
accessRestricted: spec?.accessRestricted ?? false,
|
|
104
105
|
fields: fields.map((f) => normalizeField(f))
|
|
105
106
|
};
|
|
106
107
|
}
|
|
@@ -182,6 +183,7 @@ async function installCustomEntitiesFromModules(em, cache, options = {}) {
|
|
|
182
183
|
organizationId: null,
|
|
183
184
|
tenantId: scope.tenantId,
|
|
184
185
|
showInSidebar: spec?.showInSidebar ?? false,
|
|
186
|
+
accessRestricted: spec?.accessRestricted ?? false,
|
|
185
187
|
labelField: spec?.labelField ?? null,
|
|
186
188
|
defaultEditor: spec?.defaultEditor ?? null,
|
|
187
189
|
isActive: true,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/entities/lib/install-from-ce.ts"],
|
|
4
|
-
"sourcesContent": ["import crypto from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache/types'\nimport type { CustomFieldDefinition, CustomFieldSet, CustomEntitySpec } from '@open-mercato/shared/modules/entities'\nimport { Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport { getModules } from '@open-mercato/shared/lib/i18n/server'\nimport { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'\nimport { ensureCustomFieldDefinitions } from './field-definitions'\nimport { upsertCustomEntity, type UpsertCustomEntityResult } from './register'\nimport { invalidateDefinitionsCache } from '../api/definitions.cache'\n\ntype InstallScope = {\n tenantId: string | null\n}\n\nexport type InstallEntitiesOptions = {\n tenantIds?: string[]\n entityIds?: string[]\n includeGlobal?: boolean\n dryRun?: boolean\n createOnly?: boolean\n force?: boolean\n logger?: (message: string) => void\n}\n\nexport type InstallEntitiesResult = {\n processed: number\n synchronized: number\n skipped: number\n fieldChanges: number\n}\n\nexport type AggregatedEntityConfig = {\n entityId: string\n moduleIds: Set<string>\n spec?: CustomEntitySpec\n fieldSets: CustomFieldSet[]\n}\n\nconst FIELD_DETAIL_KEYS: Array<keyof CustomFieldDefinition> = [\n 'label',\n 'description',\n 'options',\n 'optionsUrl',\n 'defaultValue',\n 'required',\n 'multi',\n 'filterable',\n 'formEditable',\n 'listVisible',\n 'indexed',\n 'editor',\n 'input',\n 'relatedEntityId',\n 'validation',\n 'maxAttachmentSizeMb',\n 'acceptExtensions',\n]\n\nfunction normalizeValue(value: unknown): unknown {\n if (Array.isArray(value)) return value.map((item) => normalizeValue(item))\n if (value && typeof value === 'object') {\n return Object.keys(value as Record<string, unknown>)\n .sort((a, b) => a.localeCompare(b))\n .reduce<Record<string, unknown>>((acc, key) => {\n acc[key] = normalizeValue((value as Record<string, unknown>)[key])\n return acc\n }, {})\n }\n return value\n}\n\nfunction computeChecksum(payload: unknown): string {\n return crypto.createHash('md5').update(JSON.stringify(normalizeValue(payload))).digest('hex')\n}\n\nfunction systemEntityIds(): Set<string> {\n const ids = new Set<string>()\n const GeneratedEntities = getEntityIds()\n for (const moduleEntities of Object.values(GeneratedEntities)) {\n for (const id of Object.values(moduleEntities as Record<string, string>)) {\n ids.add(id)\n }\n }\n return ids\n}\n\nfunction buildAggregatedConfigs(): AggregatedEntityConfig[] {\n const map = new Map<string, AggregatedEntityConfig>()\n const modules = getModules()\n for (const mod of modules) {\n const moduleId = mod.id\n const entitySpecs = ((mod as any).customEntities as CustomEntitySpec[] | undefined) ?? []\n for (const spec of entitySpecs) {\n const existing = map.get(spec.id) ?? { entityId: spec.id, moduleIds: new Set<string>(), spec: undefined, fieldSets: [] }\n existing.moduleIds.add(moduleId)\n if (!existing.spec) existing.spec = spec\n map.set(spec.id, existing)\n }\n const fieldSets = ((mod as any).customFieldSets as CustomFieldSet[] | undefined) ?? []\n for (const set of fieldSets) {\n const existing = map.get(set.entity) ?? { entityId: set.entity, moduleIds: new Set<string>(), spec: undefined, fieldSets: [] }\n existing.moduleIds.add(moduleId)\n existing.fieldSets.push(set)\n map.set(set.entity, existing)\n }\n }\n return Array.from(map.values())\n}\n\nfunction resolveFields(fieldSets: CustomFieldSet[]): CustomFieldDefinition[] {\n const byKey = new Map<string, CustomFieldDefinition>()\n for (const set of fieldSets) {\n for (const field of set.fields ?? []) {\n byKey.set(field.key, { ...field })\n }\n }\n return Array.from(byKey.values()).sort((a, b) => a.key.localeCompare(b.key))\n}\n\nfunction normalizeField(field: CustomFieldDefinition) {\n const payload: Record<string, unknown> = {\n key: field.key,\n kind: field.kind,\n }\n for (const key of FIELD_DETAIL_KEYS) {\n const value = field[key]\n if (value !== undefined) payload[key] = value as unknown\n }\n if (field.id) payload.id = field.id\n return payload\n}\n\nfunction buildChecksumPayload(params: {\n entityId: string\n scope: InstallScope\n spec: CustomEntitySpec | undefined\n global: boolean\n fields: CustomFieldDefinition[]\n}) {\n const { entityId, scope, spec, global, fields } = params\n return {\n entityId,\n scope,\n global,\n label: spec?.label ?? null,\n description: spec?.description ?? null,\n labelField: spec?.labelField ?? null,\n defaultEditor: spec?.defaultEditor ?? null,\n showInSidebar: spec?.showInSidebar ?? false,\n fields: fields.map((f) => normalizeField(f)),\n }\n}\n\nexport async function installCustomEntitiesFromModules(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: InstallEntitiesOptions = {}\n): Promise<InstallEntitiesResult> {\n const requestedEntityIds = options.entityIds\n ? new Set(options.entityIds.filter((id): id is string => typeof id === 'string' && id.length > 0))\n : null\n const aggregated = buildAggregatedConfigs().filter((entry) => {\n if (!requestedEntityIds) return true\n return requestedEntityIds.has(entry.entityId)\n })\n const systemIds = systemEntityIds()\n const includeGlobal = options.includeGlobal !== false\n const dryRun = options.dryRun === true\n const createOnly = options.createOnly === true\n const force = options.force === true\n const logger = options.logger\n\n let tenantIds: string[] | undefined\n if (options.tenantIds !== undefined) {\n tenantIds = Array.from(new Set(options.tenantIds.filter((id): id is string => typeof id === 'string' && id.length > 0)))\n }\n\n const ensureTenantIds = async (): Promise<string[]> => {\n if (tenantIds !== undefined) return tenantIds\n const rows = await em.find(Tenant, { deletedAt: null } as any, { fields: ['id'] as any })\n tenantIds = rows.map((row) => row.id)\n return tenantIds ?? []\n }\n\n let processed = 0\n let synchronized = 0\n let skipped = 0\n let fieldChanges = 0\n\n for (const entry of aggregated) {\n const { entityId } = entry\n const spec = entry.spec\n const fields = resolveFields(entry.fieldSets)\n const isSystem = systemIds.has(entityId)\n const registerEntity = !isSystem && !!spec\n const isGlobal = spec?.global === true\n\n const scopes: InstallScope[] = []\n if (isGlobal) {\n if (includeGlobal) scopes.push({ tenantId: null })\n } else {\n const ids = await ensureTenantIds()\n if (!ids.length) {\n skipped++\n continue\n }\n for (const tenantId of ids) scopes.push({ tenantId })\n }\n\n if (!scopes.length && fields.length === 0 && !registerEntity) {\n skipped++\n continue\n }\n\n for (const scope of scopes) {\n processed++\n const scopeKey = scope.tenantId ? `tenant:${scope.tenantId}` : 'global'\n const checksumPayload = buildChecksumPayload({\n entityId,\n scope,\n spec,\n global: isGlobal,\n fields,\n })\n const checksum = computeChecksum(checksumPayload)\n const cacheKey = `custom-entities:v1:${scopeKey}:${entityId}`\n\n if (!dryRun && !force && cache) {\n try {\n const cached = await cache.get(cacheKey)\n if (typeof cached === 'string' && cached === checksum) {\n skipped++\n continue\n }\n } catch {}\n }\n\n let entityResult: UpsertCustomEntityResult = 'unchanged'\n if (registerEntity) {\n entityResult = await upsertCustomEntity(em, entityId, {\n label: spec?.label ?? entityId,\n description: spec?.description ?? null,\n organizationId: null,\n tenantId: scope.tenantId,\n showInSidebar: spec?.showInSidebar ?? false,\n labelField: spec?.labelField ?? null,\n defaultEditor: spec?.defaultEditor ?? null,\n isActive: true,\n dryRun,\n createOnly,\n })\n }\n\n let fieldResult = { created: 0, updated: 0, unchanged: 0 }\n if (fields.length) {\n fieldResult = await ensureCustomFieldDefinitions(\n em,\n [{ entity: entityId, fields }],\n { organizationId: null, tenantId: scope.tenantId, dryRun, createOnly }\n )\n }\n\n const changed = (entityResult !== 'unchanged') || fieldResult.created > 0 || fieldResult.updated > 0\n const shouldInvalidateDefinitions = !dryRun && (changed || force)\n if (shouldInvalidateDefinitions) {\n await invalidateDefinitionsCache(cache ?? undefined, {\n tenantId: scope.tenantId,\n organizationId: null,\n entityIds: [entityId],\n })\n }\n\n if (changed) {\n synchronized++\n fieldChanges += fieldResult.created + fieldResult.updated\n if (!dryRun && cache) {\n try {\n await cache.set(cacheKey, checksum, { tags: [`custom-entity:${entityId}`, `custom-entity-scope:${scopeKey}`] })\n } catch {}\n }\n if (logger) {\n const parts: string[] = []\n if (entityResult !== 'unchanged') parts.push(`entity ${entityResult}`)\n if (fieldResult.created || fieldResult.updated) {\n parts.push(`fields +${fieldResult.created} / ~${fieldResult.updated}`)\n }\n logger(`Synced ${entityId} for ${scopeKey}${parts.length ? ` (${parts.join(', ')})` : ''}`)\n }\n } else {\n skipped++\n if (!dryRun && cache) {\n try {\n await cache.set(cacheKey, checksum, { tags: [`custom-entity:${entityId}`, `custom-entity-scope:${scopeKey}`] })\n } catch {}\n }\n }\n }\n }\n\n return { processed, synchronized, skipped, fieldChanges }\n}\n\nexport function listCustomEntityIds(): string[] {\n return buildAggregatedConfigs().map((entry) => entry.entityId)\n}\n\nexport function getAggregatedCustomEntityConfigs(): AggregatedEntityConfig[] {\n return buildAggregatedConfigs()\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import crypto from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache/types'\nimport type { CustomFieldDefinition, CustomFieldSet, CustomEntitySpec } from '@open-mercato/shared/modules/entities'\nimport { Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport { getModules } from '@open-mercato/shared/lib/i18n/server'\nimport { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'\nimport { ensureCustomFieldDefinitions } from './field-definitions'\nimport { upsertCustomEntity, type UpsertCustomEntityResult } from './register'\nimport { invalidateDefinitionsCache } from '../api/definitions.cache'\n\ntype InstallScope = {\n tenantId: string | null\n}\n\nexport type InstallEntitiesOptions = {\n tenantIds?: string[]\n entityIds?: string[]\n includeGlobal?: boolean\n dryRun?: boolean\n createOnly?: boolean\n force?: boolean\n logger?: (message: string) => void\n}\n\nexport type InstallEntitiesResult = {\n processed: number\n synchronized: number\n skipped: number\n fieldChanges: number\n}\n\nexport type AggregatedEntityConfig = {\n entityId: string\n moduleIds: Set<string>\n spec?: CustomEntitySpec\n fieldSets: CustomFieldSet[]\n}\n\nconst FIELD_DETAIL_KEYS: Array<keyof CustomFieldDefinition> = [\n 'label',\n 'description',\n 'options',\n 'optionsUrl',\n 'defaultValue',\n 'required',\n 'multi',\n 'filterable',\n 'formEditable',\n 'listVisible',\n 'indexed',\n 'editor',\n 'input',\n 'relatedEntityId',\n 'validation',\n 'maxAttachmentSizeMb',\n 'acceptExtensions',\n]\n\nfunction normalizeValue(value: unknown): unknown {\n if (Array.isArray(value)) return value.map((item) => normalizeValue(item))\n if (value && typeof value === 'object') {\n return Object.keys(value as Record<string, unknown>)\n .sort((a, b) => a.localeCompare(b))\n .reduce<Record<string, unknown>>((acc, key) => {\n acc[key] = normalizeValue((value as Record<string, unknown>)[key])\n return acc\n }, {})\n }\n return value\n}\n\nfunction computeChecksum(payload: unknown): string {\n return crypto.createHash('md5').update(JSON.stringify(normalizeValue(payload))).digest('hex')\n}\n\nfunction systemEntityIds(): Set<string> {\n const ids = new Set<string>()\n const GeneratedEntities = getEntityIds()\n for (const moduleEntities of Object.values(GeneratedEntities)) {\n for (const id of Object.values(moduleEntities as Record<string, string>)) {\n ids.add(id)\n }\n }\n return ids\n}\n\nfunction buildAggregatedConfigs(): AggregatedEntityConfig[] {\n const map = new Map<string, AggregatedEntityConfig>()\n const modules = getModules()\n for (const mod of modules) {\n const moduleId = mod.id\n const entitySpecs = ((mod as any).customEntities as CustomEntitySpec[] | undefined) ?? []\n for (const spec of entitySpecs) {\n const existing = map.get(spec.id) ?? { entityId: spec.id, moduleIds: new Set<string>(), spec: undefined, fieldSets: [] }\n existing.moduleIds.add(moduleId)\n if (!existing.spec) existing.spec = spec\n map.set(spec.id, existing)\n }\n const fieldSets = ((mod as any).customFieldSets as CustomFieldSet[] | undefined) ?? []\n for (const set of fieldSets) {\n const existing = map.get(set.entity) ?? { entityId: set.entity, moduleIds: new Set<string>(), spec: undefined, fieldSets: [] }\n existing.moduleIds.add(moduleId)\n existing.fieldSets.push(set)\n map.set(set.entity, existing)\n }\n }\n return Array.from(map.values())\n}\n\nfunction resolveFields(fieldSets: CustomFieldSet[]): CustomFieldDefinition[] {\n const byKey = new Map<string, CustomFieldDefinition>()\n for (const set of fieldSets) {\n for (const field of set.fields ?? []) {\n byKey.set(field.key, { ...field })\n }\n }\n return Array.from(byKey.values()).sort((a, b) => a.key.localeCompare(b.key))\n}\n\nfunction normalizeField(field: CustomFieldDefinition) {\n const payload: Record<string, unknown> = {\n key: field.key,\n kind: field.kind,\n }\n for (const key of FIELD_DETAIL_KEYS) {\n const value = field[key]\n if (value !== undefined) payload[key] = value as unknown\n }\n if (field.id) payload.id = field.id\n return payload\n}\n\nfunction buildChecksumPayload(params: {\n entityId: string\n scope: InstallScope\n spec: CustomEntitySpec | undefined\n global: boolean\n fields: CustomFieldDefinition[]\n}) {\n const { entityId, scope, spec, global, fields } = params\n return {\n entityId,\n scope,\n global,\n label: spec?.label ?? null,\n description: spec?.description ?? null,\n labelField: spec?.labelField ?? null,\n defaultEditor: spec?.defaultEditor ?? null,\n showInSidebar: spec?.showInSidebar ?? false,\n accessRestricted: spec?.accessRestricted ?? false,\n fields: fields.map((f) => normalizeField(f)),\n }\n}\n\nexport async function installCustomEntitiesFromModules(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: InstallEntitiesOptions = {}\n): Promise<InstallEntitiesResult> {\n const requestedEntityIds = options.entityIds\n ? new Set(options.entityIds.filter((id): id is string => typeof id === 'string' && id.length > 0))\n : null\n const aggregated = buildAggregatedConfigs().filter((entry) => {\n if (!requestedEntityIds) return true\n return requestedEntityIds.has(entry.entityId)\n })\n const systemIds = systemEntityIds()\n const includeGlobal = options.includeGlobal !== false\n const dryRun = options.dryRun === true\n const createOnly = options.createOnly === true\n const force = options.force === true\n const logger = options.logger\n\n let tenantIds: string[] | undefined\n if (options.tenantIds !== undefined) {\n tenantIds = Array.from(new Set(options.tenantIds.filter((id): id is string => typeof id === 'string' && id.length > 0)))\n }\n\n const ensureTenantIds = async (): Promise<string[]> => {\n if (tenantIds !== undefined) return tenantIds\n const rows = await em.find(Tenant, { deletedAt: null } as any, { fields: ['id'] as any })\n tenantIds = rows.map((row) => row.id)\n return tenantIds ?? []\n }\n\n let processed = 0\n let synchronized = 0\n let skipped = 0\n let fieldChanges = 0\n\n for (const entry of aggregated) {\n const { entityId } = entry\n const spec = entry.spec\n const fields = resolveFields(entry.fieldSets)\n const isSystem = systemIds.has(entityId)\n const registerEntity = !isSystem && !!spec\n const isGlobal = spec?.global === true\n\n const scopes: InstallScope[] = []\n if (isGlobal) {\n if (includeGlobal) scopes.push({ tenantId: null })\n } else {\n const ids = await ensureTenantIds()\n if (!ids.length) {\n skipped++\n continue\n }\n for (const tenantId of ids) scopes.push({ tenantId })\n }\n\n if (!scopes.length && fields.length === 0 && !registerEntity) {\n skipped++\n continue\n }\n\n for (const scope of scopes) {\n processed++\n const scopeKey = scope.tenantId ? `tenant:${scope.tenantId}` : 'global'\n const checksumPayload = buildChecksumPayload({\n entityId,\n scope,\n spec,\n global: isGlobal,\n fields,\n })\n const checksum = computeChecksum(checksumPayload)\n const cacheKey = `custom-entities:v1:${scopeKey}:${entityId}`\n\n if (!dryRun && !force && cache) {\n try {\n const cached = await cache.get(cacheKey)\n if (typeof cached === 'string' && cached === checksum) {\n skipped++\n continue\n }\n } catch {}\n }\n\n let entityResult: UpsertCustomEntityResult = 'unchanged'\n if (registerEntity) {\n entityResult = await upsertCustomEntity(em, entityId, {\n label: spec?.label ?? entityId,\n description: spec?.description ?? null,\n organizationId: null,\n tenantId: scope.tenantId,\n showInSidebar: spec?.showInSidebar ?? false,\n accessRestricted: spec?.accessRestricted ?? false,\n labelField: spec?.labelField ?? null,\n defaultEditor: spec?.defaultEditor ?? null,\n isActive: true,\n dryRun,\n createOnly,\n })\n }\n\n let fieldResult = { created: 0, updated: 0, unchanged: 0 }\n if (fields.length) {\n fieldResult = await ensureCustomFieldDefinitions(\n em,\n [{ entity: entityId, fields }],\n { organizationId: null, tenantId: scope.tenantId, dryRun, createOnly }\n )\n }\n\n const changed = (entityResult !== 'unchanged') || fieldResult.created > 0 || fieldResult.updated > 0\n const shouldInvalidateDefinitions = !dryRun && (changed || force)\n if (shouldInvalidateDefinitions) {\n await invalidateDefinitionsCache(cache ?? undefined, {\n tenantId: scope.tenantId,\n organizationId: null,\n entityIds: [entityId],\n })\n }\n\n if (changed) {\n synchronized++\n fieldChanges += fieldResult.created + fieldResult.updated\n if (!dryRun && cache) {\n try {\n await cache.set(cacheKey, checksum, { tags: [`custom-entity:${entityId}`, `custom-entity-scope:${scopeKey}`] })\n } catch {}\n }\n if (logger) {\n const parts: string[] = []\n if (entityResult !== 'unchanged') parts.push(`entity ${entityResult}`)\n if (fieldResult.created || fieldResult.updated) {\n parts.push(`fields +${fieldResult.created} / ~${fieldResult.updated}`)\n }\n logger(`Synced ${entityId} for ${scopeKey}${parts.length ? ` (${parts.join(', ')})` : ''}`)\n }\n } else {\n skipped++\n if (!dryRun && cache) {\n try {\n await cache.set(cacheKey, checksum, { tags: [`custom-entity:${entityId}`, `custom-entity-scope:${scopeKey}`] })\n } catch {}\n }\n }\n }\n }\n\n return { processed, synchronized, skipped, fieldChanges }\n}\n\nexport function listCustomEntityIds(): string[] {\n return buildAggregatedConfigs().map((entry) => entry.entityId)\n}\n\nexport function getAggregatedCustomEntityConfigs(): AggregatedEntityConfig[] {\n return buildAggregatedConfigs()\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const RECORDS_VIEW_FEATURE = "entities.records.view";
|
|
2
|
+
const RECORDS_MANAGE_FEATURE = "entities.records.manage";
|
|
3
|
+
function coarseRecordFeature(action) {
|
|
4
|
+
return action === "manage" ? RECORDS_MANAGE_FEATURE : RECORDS_VIEW_FEATURE;
|
|
5
|
+
}
|
|
6
|
+
function deriveCustomEntityRecordFeature(entityId, action) {
|
|
7
|
+
return `entities.records.${entityId}.${action}`;
|
|
8
|
+
}
|
|
9
|
+
function synthesizedRecordFeatures(entityId) {
|
|
10
|
+
const view = deriveCustomEntityRecordFeature(entityId, "view");
|
|
11
|
+
const manage = deriveCustomEntityRecordFeature(entityId, "manage");
|
|
12
|
+
return [
|
|
13
|
+
{ id: view, action: "view", dependsOn: [RECORDS_VIEW_FEATURE] },
|
|
14
|
+
{ id: manage, action: "manage", dependsOn: [view, RECORDS_MANAGE_FEATURE] }
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
RECORDS_MANAGE_FEATURE,
|
|
19
|
+
RECORDS_VIEW_FEATURE,
|
|
20
|
+
coarseRecordFeature,
|
|
21
|
+
deriveCustomEntityRecordFeature,
|
|
22
|
+
synthesizedRecordFeatures
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=recordFeatures.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/entities/lib/recordFeatures.ts"],
|
|
4
|
+
"sourcesContent": ["// Pure, dependency-free helpers describing the ACL features that gate\n// custom-entity records. Kept import-free so both the enforcement path\n// (`entityAcl.ts`) and the feature catalog (`auth/api/features.ts`) can derive\n// the SAME feature id without coupling to each other or to the ORM.\n//\n// Coarse (route-level) features stay entity-agnostic:\n// entities.records.view \u2014 read any non-restricted custom entity's records\n// entities.records.manage \u2014 write any non-restricted custom entity's records\n//\n// A custom entity flagged `access_restricted` ALSO requires a synthesized\n// per-entity feature keyed on its immutable entityId:\n// entities.records.<entityId>.view\n// entities.records.<entityId>.manage\n//\n// entityId always matches `^[a-z0-9_]+:[a-z0-9_]+$` (it contains a `:` and no\n// `.`), so it can never collide with the coarse `view`/`manage` segments and\n// stays a single dotted segment for the wildcard matcher.\n\nexport type RecordsAction = 'view' | 'manage'\n\nexport const RECORDS_VIEW_FEATURE = 'entities.records.view'\nexport const RECORDS_MANAGE_FEATURE = 'entities.records.manage'\n\nexport function coarseRecordFeature(action: RecordsAction): string {\n return action === 'manage' ? RECORDS_MANAGE_FEATURE : RECORDS_VIEW_FEATURE\n}\n\nexport function deriveCustomEntityRecordFeature(entityId: string, action: RecordsAction): string {\n return `entities.records.${entityId}.${action}`\n}\n\nexport type SynthesizedRecordFeature = {\n id: string\n action: RecordsAction\n dependsOn: string[]\n}\n\n// The per-entity features an admin can grant for a restricted custom entity.\n// `dependsOn` mirrors the coarse prerequisite composition: the per-entity grant\n// is meaningful only alongside the coarse route-level feature.\nexport function synthesizedRecordFeatures(entityId: string): SynthesizedRecordFeature[] {\n const view = deriveCustomEntityRecordFeature(entityId, 'view')\n const manage = deriveCustomEntityRecordFeature(entityId, 'manage')\n return [\n { id: view, action: 'view', dependsOn: [RECORDS_VIEW_FEATURE] },\n { id: manage, action: 'manage', dependsOn: [view, RECORDS_MANAGE_FEATURE] },\n ]\n}\n"],
|
|
5
|
+
"mappings": "AAoBO,MAAM,uBAAuB;AAC7B,MAAM,yBAAyB;AAE/B,SAAS,oBAAoB,QAA+B;AACjE,SAAO,WAAW,WAAW,yBAAyB;AACxD;AAEO,SAAS,gCAAgC,UAAkB,QAA+B;AAC/F,SAAO,oBAAoB,QAAQ,IAAI,MAAM;AAC/C;AAWO,SAAS,0BAA0B,UAA8C;AACtF,QAAM,OAAO,gCAAgC,UAAU,MAAM;AAC7D,QAAM,SAAS,gCAAgC,UAAU,QAAQ;AACjE,SAAO;AAAA,IACL,EAAE,IAAI,MAAM,QAAQ,QAAQ,WAAW,CAAC,oBAAoB,EAAE;AAAA,IAC9D,EAAE,IAAI,QAAQ,QAAQ,UAAU,WAAW,CAAC,MAAM,sBAAsB,EAAE;AAAA,EAC5E;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -6,6 +6,7 @@ async function upsertCustomEntity(em, entityId, opts) {
|
|
|
6
6
|
description: opts.description ?? null,
|
|
7
7
|
isActive: opts.isActive ?? true,
|
|
8
8
|
showInSidebar: !!opts.showInSidebar,
|
|
9
|
+
accessRestricted: !!opts.accessRestricted,
|
|
9
10
|
labelField: opts.labelField ?? null,
|
|
10
11
|
defaultEditor: opts.defaultEditor ?? null
|
|
11
12
|
};
|
|
@@ -16,6 +17,7 @@ async function upsertCustomEntity(em, entityId, opts) {
|
|
|
16
17
|
ent.description = desired.description;
|
|
17
18
|
ent.isActive = desired.isActive;
|
|
18
19
|
ent.showInSidebar = desired.showInSidebar;
|
|
20
|
+
ent.accessRestricted = desired.accessRestricted;
|
|
19
21
|
ent.labelField = desired.labelField;
|
|
20
22
|
ent.defaultEditor = desired.defaultEditor;
|
|
21
23
|
ent.updatedAt = /* @__PURE__ */ new Date();
|
|
@@ -26,6 +28,7 @@ async function upsertCustomEntity(em, entityId, opts) {
|
|
|
26
28
|
if ((ent.description ?? null) !== desired.description) return true;
|
|
27
29
|
if ((ent.isActive ?? true) !== desired.isActive) return true;
|
|
28
30
|
if ((ent.showInSidebar ?? false) !== desired.showInSidebar) return true;
|
|
31
|
+
if ((ent.accessRestricted ?? false) !== desired.accessRestricted) return true;
|
|
29
32
|
if ((ent.labelField ?? null) !== desired.labelField) return true;
|
|
30
33
|
if ((ent.defaultEditor ?? null) !== desired.defaultEditor) return true;
|
|
31
34
|
if (ent.deletedAt != null) return true;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/entities/lib/register.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { CustomEntity } from '../data/entities'\n\nexport type UpsertEntityOptions = {\n label: string\n description?: string | null\n organizationId?: string | null\n tenantId?: string | null\n showInSidebar?: boolean\n labelField?: string | null\n defaultEditor?: string | null\n isActive?: boolean\n dryRun?: boolean\n createOnly?: boolean\n}\n\nexport type UpsertCustomEntityResult = 'created' | 'updated' | 'unchanged'\n\n/**\n * Ensure a logical (virtual) entity exists in the DB. If present, updates label/description.\n * Use from module code (e.g., during CLI install/seed or module bootstrap).\n * \n * This function handles race conditions by retrying if a unique constraint violation occurs.\n */\nexport async function upsertCustomEntity(em: EntityManager, entityId: string, opts: UpsertEntityOptions): Promise<UpsertCustomEntityResult> {\n const where: any = { entityId, organizationId: opts.organizationId ?? null, tenantId: opts.tenantId ?? null }\n const desired = {\n label: opts.label,\n description: opts.description ?? null,\n isActive: opts.isActive ?? true,\n showInSidebar: !!opts.showInSidebar,\n labelField: opts.labelField ?? null,\n defaultEditor: opts.defaultEditor ?? null,\n }\n const dryRun = opts.dryRun === true\n const createOnly = opts.createOnly === true\n\n const apply = (ent: any) => {\n ent.label = desired.label\n ent.description = desired.description\n ent.isActive = desired.isActive\n ent.showInSidebar = desired.showInSidebar\n ent.labelField = desired.labelField\n ent.defaultEditor = desired.defaultEditor\n ent.updatedAt = new Date()\n if (ent.deletedAt) ent.deletedAt = null\n }\n\n const isDifferent = (ent: any) => {\n if (ent.label !== desired.label) return true\n if ((ent.description ?? null) !== desired.description) return true\n if ((ent.isActive ?? true) !== desired.isActive) return true\n if ((ent.showInSidebar ?? false) !== desired.showInSidebar) return true\n if ((ent.labelField ?? null) !== desired.labelField) return true\n if ((ent.defaultEditor ?? null) !== desired.defaultEditor) return true\n if (ent.deletedAt != null) return true\n return false\n }\n\n return em.transactional(async (tem) => {\n const now = new Date()\n try {\n let ent = await tem.findOne(CustomEntity as any, where)\n if (!ent) {\n if (dryRun) return 'created'\n ent = tem.create(CustomEntity as any, { ...where, ...desired, createdAt: now, updatedAt: now, deletedAt: null })\n await tem.persist(ent).flush()\n return 'created' as UpsertCustomEntityResult\n }\n if (createOnly) return 'unchanged'\n if (!isDifferent(ent)) return 'unchanged'\n if (dryRun) return 'updated'\n apply(ent)\n await tem.flush()\n return 'updated' as UpsertCustomEntityResult\n } catch (error: any) {\n if (error?.code === '23505' || error?.message?.includes('duplicate key')) {\n const ent = await tem.findOne(CustomEntity as any, where)\n if (!ent) throw error\n if (createOnly) return 'unchanged'\n if (!isDifferent(ent)) return 'unchanged'\n if (dryRun) return 'updated'\n apply(ent)\n await tem.flush()\n return 'updated'\n }\n throw error\n }\n })\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { CustomEntity } from '../data/entities'\n\nexport type UpsertEntityOptions = {\n label: string\n description?: string | null\n organizationId?: string | null\n tenantId?: string | null\n showInSidebar?: boolean\n accessRestricted?: boolean\n labelField?: string | null\n defaultEditor?: string | null\n isActive?: boolean\n dryRun?: boolean\n createOnly?: boolean\n}\n\nexport type UpsertCustomEntityResult = 'created' | 'updated' | 'unchanged'\n\n/**\n * Ensure a logical (virtual) entity exists in the DB. If present, updates label/description.\n * Use from module code (e.g., during CLI install/seed or module bootstrap).\n * \n * This function handles race conditions by retrying if a unique constraint violation occurs.\n */\nexport async function upsertCustomEntity(em: EntityManager, entityId: string, opts: UpsertEntityOptions): Promise<UpsertCustomEntityResult> {\n const where: any = { entityId, organizationId: opts.organizationId ?? null, tenantId: opts.tenantId ?? null }\n const desired = {\n label: opts.label,\n description: opts.description ?? null,\n isActive: opts.isActive ?? true,\n showInSidebar: !!opts.showInSidebar,\n accessRestricted: !!opts.accessRestricted,\n labelField: opts.labelField ?? null,\n defaultEditor: opts.defaultEditor ?? null,\n }\n const dryRun = opts.dryRun === true\n const createOnly = opts.createOnly === true\n\n const apply = (ent: any) => {\n ent.label = desired.label\n ent.description = desired.description\n ent.isActive = desired.isActive\n ent.showInSidebar = desired.showInSidebar\n ent.accessRestricted = desired.accessRestricted\n ent.labelField = desired.labelField\n ent.defaultEditor = desired.defaultEditor\n ent.updatedAt = new Date()\n if (ent.deletedAt) ent.deletedAt = null\n }\n\n const isDifferent = (ent: any) => {\n if (ent.label !== desired.label) return true\n if ((ent.description ?? null) !== desired.description) return true\n if ((ent.isActive ?? true) !== desired.isActive) return true\n if ((ent.showInSidebar ?? false) !== desired.showInSidebar) return true\n if ((ent.accessRestricted ?? false) !== desired.accessRestricted) return true\n if ((ent.labelField ?? null) !== desired.labelField) return true\n if ((ent.defaultEditor ?? null) !== desired.defaultEditor) return true\n if (ent.deletedAt != null) return true\n return false\n }\n\n return em.transactional(async (tem) => {\n const now = new Date()\n try {\n let ent = await tem.findOne(CustomEntity as any, where)\n if (!ent) {\n if (dryRun) return 'created'\n ent = tem.create(CustomEntity as any, { ...where, ...desired, createdAt: now, updatedAt: now, deletedAt: null })\n await tem.persist(ent).flush()\n return 'created' as UpsertCustomEntityResult\n }\n if (createOnly) return 'unchanged'\n if (!isDifferent(ent)) return 'unchanged'\n if (dryRun) return 'updated'\n apply(ent)\n await tem.flush()\n return 'updated' as UpsertCustomEntityResult\n } catch (error: any) {\n if (error?.code === '23505' || error?.message?.includes('duplicate key')) {\n const ent = await tem.findOne(CustomEntity as any, where)\n if (!ent) throw error\n if (createOnly) return 'unchanged'\n if (!isDifferent(ent)) return 'unchanged'\n if (dryRun) return 'updated'\n apply(ent)\n await tem.flush()\n return 'updated'\n }\n throw error\n }\n })\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,oBAAoB;AAwB7B,eAAsB,mBAAmB,IAAmB,UAAkB,MAA8D;AAC1I,QAAM,QAAa,EAAE,UAAU,gBAAgB,KAAK,kBAAkB,MAAM,UAAU,KAAK,YAAY,KAAK;AAC5G,QAAM,UAAU;AAAA,IACd,OAAO,KAAK;AAAA,IACZ,aAAa,KAAK,eAAe;AAAA,IACjC,UAAU,KAAK,YAAY;AAAA,IAC3B,eAAe,CAAC,CAAC,KAAK;AAAA,IACtB,kBAAkB,CAAC,CAAC,KAAK;AAAA,IACzB,YAAY,KAAK,cAAc;AAAA,IAC/B,eAAe,KAAK,iBAAiB;AAAA,EACvC;AACA,QAAM,SAAS,KAAK,WAAW;AAC/B,QAAM,aAAa,KAAK,eAAe;AAEvC,QAAM,QAAQ,CAAC,QAAa;AAC1B,QAAI,QAAQ,QAAQ;AACpB,QAAI,cAAc,QAAQ;AAC1B,QAAI,WAAW,QAAQ;AACvB,QAAI,gBAAgB,QAAQ;AAC5B,QAAI,mBAAmB,QAAQ;AAC/B,QAAI,aAAa,QAAQ;AACzB,QAAI,gBAAgB,QAAQ;AAC5B,QAAI,YAAY,oBAAI,KAAK;AACzB,QAAI,IAAI,UAAW,KAAI,YAAY;AAAA,EACrC;AAEA,QAAM,cAAc,CAAC,QAAa;AAChC,QAAI,IAAI,UAAU,QAAQ,MAAO,QAAO;AACxC,SAAK,IAAI,eAAe,UAAU,QAAQ,YAAa,QAAO;AAC9D,SAAK,IAAI,YAAY,UAAU,QAAQ,SAAU,QAAO;AACxD,SAAK,IAAI,iBAAiB,WAAW,QAAQ,cAAe,QAAO;AACnE,SAAK,IAAI,oBAAoB,WAAW,QAAQ,iBAAkB,QAAO;AACzE,SAAK,IAAI,cAAc,UAAU,QAAQ,WAAY,QAAO;AAC5D,SAAK,IAAI,iBAAiB,UAAU,QAAQ,cAAe,QAAO;AAClE,QAAI,IAAI,aAAa,KAAM,QAAO;AAClC,WAAO;AAAA,EACT;AAEA,SAAO,GAAG,cAAc,OAAO,QAAQ;AACrC,UAAM,MAAM,oBAAI,KAAK;AACrB,QAAI;AACF,UAAI,MAAM,MAAM,IAAI,QAAQ,cAAqB,KAAK;AACtD,UAAI,CAAC,KAAK;AACR,YAAI,OAAQ,QAAO;AACnB,cAAM,IAAI,OAAO,cAAqB,EAAE,GAAG,OAAO,GAAG,SAAS,WAAW,KAAK,WAAW,KAAK,WAAW,KAAK,CAAC;AAC/G,cAAM,IAAI,QAAQ,GAAG,EAAE,MAAM;AAC7B,eAAO;AAAA,MACT;AACA,UAAI,WAAY,QAAO;AACvB,UAAI,CAAC,YAAY,GAAG,EAAG,QAAO;AAC9B,UAAI,OAAQ,QAAO;AACnB,YAAM,GAAG;AACT,YAAM,IAAI,MAAM;AAChB,aAAO;AAAA,IACT,SAAS,OAAY;AACnB,UAAI,OAAO,SAAS,WAAW,OAAO,SAAS,SAAS,eAAe,GAAG;AACxE,cAAM,MAAM,MAAM,IAAI,QAAQ,cAAqB,KAAK;AACxD,YAAI,CAAC,IAAK,OAAM;AAChB,YAAI,WAAY,QAAO;AACvB,YAAI,CAAC,YAAY,GAAG,EAAG,QAAO;AAC9B,YAAI,OAAQ,QAAO;AACnB,cAAM,GAAG;AACT,cAAM,IAAI,MAAM;AAChB,eAAO;AAAA,MACT;AACA,YAAM;AAAA,IACR;AAAA,EACF,CAAC;AACH;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { getModules } from "@open-mercato/shared/lib/i18n/server";
|
|
2
|
+
import { synthesizedRecordFeatures } from "./recordFeatures.js";
|
|
3
|
+
function declaredEntities() {
|
|
4
|
+
const restricted = [];
|
|
5
|
+
const ids = /* @__PURE__ */ new Set();
|
|
6
|
+
try {
|
|
7
|
+
const mods = getModules();
|
|
8
|
+
for (const mod of mods || []) {
|
|
9
|
+
for (const spec of mod?.customEntities ?? []) {
|
|
10
|
+
if (!spec?.id) continue;
|
|
11
|
+
ids.add(spec.id);
|
|
12
|
+
if (spec.accessRestricted === true) restricted.push({ entityId: spec.id, label: spec.label || spec.id });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
} catch {
|
|
16
|
+
}
|
|
17
|
+
return { restricted, ids };
|
|
18
|
+
}
|
|
19
|
+
async function registeredRestrictedEntities(em, tenantId) {
|
|
20
|
+
try {
|
|
21
|
+
const db = em.getKysely();
|
|
22
|
+
const rows = await db.selectFrom("custom_entities").select(["entity_id", "label"]).where("access_restricted", "=", true).where("deleted_at", "is", null).where(
|
|
23
|
+
(eb) => eb.or([
|
|
24
|
+
eb("tenant_id", "=", tenantId),
|
|
25
|
+
eb("tenant_id", "is", null)
|
|
26
|
+
])
|
|
27
|
+
).execute();
|
|
28
|
+
return rows.map((row) => ({
|
|
29
|
+
entityId: String(row.entity_id ?? ""),
|
|
30
|
+
label: typeof row.label === "string" && row.label.length ? row.label : String(row.entity_id ?? "")
|
|
31
|
+
})).filter((row) => row.entityId.length > 0);
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function synthesizeRestrictedEntityFeatures(em, tenantId) {
|
|
37
|
+
if (!tenantId) return [];
|
|
38
|
+
const declared = declaredEntities();
|
|
39
|
+
const byEntityId = /* @__PURE__ */ new Map();
|
|
40
|
+
for (const entity of declared.restricted) byEntityId.set(entity.entityId, entity);
|
|
41
|
+
for (const entity of await registeredRestrictedEntities(em, tenantId)) {
|
|
42
|
+
if (declared.ids.has(entity.entityId)) continue;
|
|
43
|
+
byEntityId.set(entity.entityId, entity);
|
|
44
|
+
}
|
|
45
|
+
const items = [];
|
|
46
|
+
for (const { entityId, label } of byEntityId.values()) {
|
|
47
|
+
for (const feature of synthesizedRecordFeatures(entityId)) {
|
|
48
|
+
const verb = feature.action === "manage" ? "Manage records" : "View records";
|
|
49
|
+
items.push({
|
|
50
|
+
id: feature.id,
|
|
51
|
+
title: `${verb}: ${label}`,
|
|
52
|
+
module: "entities",
|
|
53
|
+
dependsOn: feature.dependsOn
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return items;
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
synthesizeRestrictedEntityFeatures
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=restrictedEntityFeatures.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/entities/lib/restrictedEntityFeatures.ts"],
|
|
4
|
+
"sourcesContent": ["import { getModules } from '@open-mercato/shared/lib/i18n/server'\nimport { synthesizedRecordFeatures } from './recordFeatures'\n\nexport type SynthesizedFeatureItem = {\n id: string\n title: string\n module: string\n dependsOn?: string[]\n}\n\ntype RestrictedEntity = { entityId: string; label: string }\n\ntype DeclaredEntities = { restricted: RestrictedEntity[]; ids: Set<string> }\n\n// Reads module-declared (ce.ts) custom entities. `restricted` holds those flagged\n// accessRestricted; `ids` holds ALL declared ids so the DB path can defer to the\n// declaration \u2014 the declared registry is authoritative for its own ids, matching\n// the records-route enforcement precedence (declared wins over the DB row).\nfunction declaredEntities(): DeclaredEntities {\n const restricted: RestrictedEntity[] = []\n const ids = new Set<string>()\n try {\n const mods = getModules() as Array<{\n customEntities?: Array<{ id?: string; label?: string; accessRestricted?: boolean }>\n }>\n for (const mod of mods || []) {\n for (const spec of mod?.customEntities ?? []) {\n if (!spec?.id) continue\n ids.add(spec.id)\n if (spec.accessRestricted === true) restricted.push({ entityId: spec.id, label: spec.label || spec.id })\n }\n }\n } catch {}\n return { restricted, ids }\n}\n\n// Reads user-registered custom entities flagged access_restricted for the given\n// tenant scope. Uses the raw table (not the ORM class) to keep this helper cheap\n// and avoid pulling entity metadata into unrelated callers.\nasync function registeredRestrictedEntities(em: any, tenantId: string): Promise<RestrictedEntity[]> {\n try {\n const db = em.getKysely()\n const rows = await db\n .selectFrom('custom_entities' as any)\n .select(['entity_id' as any, 'label' as any])\n .where('access_restricted' as any, '=', true)\n .where('deleted_at' as any, 'is', null as any)\n .where((eb: any) =>\n eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null as any),\n ]),\n )\n .execute()\n return (rows as Array<{ entity_id?: unknown; label?: unknown }>).map((row) => ({\n entityId: String(row.entity_id ?? ''),\n label: typeof row.label === 'string' && row.label.length ? row.label : String(row.entity_id ?? ''),\n })).filter((row) => row.entityId.length > 0)\n } catch {\n return []\n }\n}\n\n/**\n * Feature-catalog contribution for restricted custom entities: two synthesized\n * per-entity features (view/manage) per restricted entity in the tenant scope,\n * so admins can grant them in the Role/User ACL editor. Fails safe (returns what\n * it can) \u2014 never throws \u2014 so the static feature catalog is unaffected on error.\n */\nexport async function synthesizeRestrictedEntityFeatures(\n em: any,\n tenantId: string | null | undefined,\n): Promise<SynthesizedFeatureItem[]> {\n if (!tenantId) return []\n const declared = declaredEntities()\n const byEntityId = new Map<string, RestrictedEntity>()\n for (const entity of declared.restricted) byEntityId.set(entity.entityId, entity)\n // DB rows are authoritative only for ids the declared registry does not own, so\n // the catalog and the records-route enforcement agree on the same source.\n for (const entity of await registeredRestrictedEntities(em, tenantId)) {\n if (declared.ids.has(entity.entityId)) continue\n byEntityId.set(entity.entityId, entity)\n }\n\n const items: SynthesizedFeatureItem[] = []\n for (const { entityId, label } of byEntityId.values()) {\n for (const feature of synthesizedRecordFeatures(entityId)) {\n const verb = feature.action === 'manage' ? 'Manage records' : 'View records'\n items.push({\n id: feature.id,\n title: `${verb}: ${label}`,\n module: 'entities',\n dependsOn: feature.dependsOn,\n })\n }\n }\n return items\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,kBAAkB;AAC3B,SAAS,iCAAiC;AAiB1C,SAAS,mBAAqC;AAC5C,QAAM,aAAiC,CAAC;AACxC,QAAM,MAAM,oBAAI,IAAY;AAC5B,MAAI;AACF,UAAM,OAAO,WAAW;AAGxB,eAAW,OAAO,QAAQ,CAAC,GAAG;AAC5B,iBAAW,QAAQ,KAAK,kBAAkB,CAAC,GAAG;AAC5C,YAAI,CAAC,MAAM,GAAI;AACf,YAAI,IAAI,KAAK,EAAE;AACf,YAAI,KAAK,qBAAqB,KAAM,YAAW,KAAK,EAAE,UAAU,KAAK,IAAI,OAAO,KAAK,SAAS,KAAK,GAAG,CAAC;AAAA,MACzG;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAAC;AACT,SAAO,EAAE,YAAY,IAAI;AAC3B;AAKA,eAAe,6BAA6B,IAAS,UAA+C;AAClG,MAAI;AACF,UAAM,KAAK,GAAG,UAAU;AACxB,UAAM,OAAO,MAAM,GAChB,WAAW,iBAAwB,EACnC,OAAO,CAAC,aAAoB,OAAc,CAAC,EAC3C,MAAM,qBAA4B,KAAK,IAAI,EAC3C,MAAM,cAAqB,MAAM,IAAW,EAC5C;AAAA,MAAM,CAAC,OACN,GAAG,GAAG;AAAA,QACJ,GAAG,aAAoB,KAAK,QAAQ;AAAA,QACpC,GAAG,aAAoB,MAAM,IAAW;AAAA,MAC1C,CAAC;AAAA,IACH,EACC,QAAQ;AACX,WAAQ,KAAyD,IAAI,CAAC,SAAS;AAAA,MAC7E,UAAU,OAAO,IAAI,aAAa,EAAE;AAAA,MACpC,OAAO,OAAO,IAAI,UAAU,YAAY,IAAI,MAAM,SAAS,IAAI,QAAQ,OAAO,IAAI,aAAa,EAAE;AAAA,IACnG,EAAE,EAAE,OAAO,CAAC,QAAQ,IAAI,SAAS,SAAS,CAAC;AAAA,EAC7C,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAQA,eAAsB,mCACpB,IACA,UACmC;AACnC,MAAI,CAAC,SAAU,QAAO,CAAC;AACvB,QAAM,WAAW,iBAAiB;AAClC,QAAM,aAAa,oBAAI,IAA8B;AACrD,aAAW,UAAU,SAAS,WAAY,YAAW,IAAI,OAAO,UAAU,MAAM;AAGhF,aAAW,UAAU,MAAM,6BAA6B,IAAI,QAAQ,GAAG;AACrE,QAAI,SAAS,IAAI,IAAI,OAAO,QAAQ,EAAG;AACvC,eAAW,IAAI,OAAO,UAAU,MAAM;AAAA,EACxC;AAEA,QAAM,QAAkC,CAAC;AACzC,aAAW,EAAE,UAAU,MAAM,KAAK,WAAW,OAAO,GAAG;AACrD,eAAW,WAAW,0BAA0B,QAAQ,GAAG;AACzD,YAAM,OAAO,QAAQ,WAAW,WAAW,mBAAmB;AAC9D,YAAM,KAAK;AAAA,QACT,IAAI,QAAQ;AAAA,QACZ,OAAO,GAAG,IAAI,KAAK,KAAK;AAAA,QACxB,QAAQ;AAAA,QACR,WAAW,QAAQ;AAAA,MACrB,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Migration } from "@mikro-orm/migrations";
|
|
2
|
+
class Migration20260716120000 extends Migration {
|
|
3
|
+
async up() {
|
|
4
|
+
this.addSql(`alter table "custom_entities" add column "access_restricted" boolean not null default false;`);
|
|
5
|
+
}
|
|
6
|
+
async down() {
|
|
7
|
+
this.addSql(`alter table "custom_entities" drop column "access_restricted";`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
Migration20260716120000
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=Migration20260716120000.js.map
|