@open-mercato/core 0.6.7-develop.6593.1.0ecf630f1e → 0.6.7-develop.6595.1.e93ecd3cfa

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.
Files changed (53) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/custom_entity/index.js +2 -0
  3. package/dist/generated/entities/custom_entity/index.js.map +2 -2
  4. package/dist/generated/entity-fields-registry.js +1 -0
  5. package/dist/generated/entity-fields-registry.js.map +2 -2
  6. package/dist/modules/auth/api/features.js +14 -0
  7. package/dist/modules/auth/api/features.js.map +2 -2
  8. package/dist/modules/entities/api/entities.js +20 -0
  9. package/dist/modules/entities/api/entities.js.map +2 -2
  10. package/dist/modules/entities/api/entity-settings.js +89 -0
  11. package/dist/modules/entities/api/entity-settings.js.map +7 -0
  12. package/dist/modules/entities/api/records.js +46 -25
  13. package/dist/modules/entities/api/records.js.map +2 -2
  14. package/dist/modules/entities/backend/entities/user/[entityId]/page.js +13 -4
  15. package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
  16. package/dist/modules/entities/backend/entities/user/create/page.js +27 -2
  17. package/dist/modules/entities/backend/entities/user/create/page.js.map +2 -2
  18. package/dist/modules/entities/data/entities.js +4 -0
  19. package/dist/modules/entities/data/entities.js.map +2 -2
  20. package/dist/modules/entities/lib/entityAcl.js +17 -4
  21. package/dist/modules/entities/lib/entityAcl.js.map +3 -3
  22. package/dist/modules/entities/lib/install-from-ce.js +2 -0
  23. package/dist/modules/entities/lib/install-from-ce.js.map +2 -2
  24. package/dist/modules/entities/lib/recordFeatures.js +24 -0
  25. package/dist/modules/entities/lib/recordFeatures.js.map +7 -0
  26. package/dist/modules/entities/lib/register.js +3 -0
  27. package/dist/modules/entities/lib/register.js.map +2 -2
  28. package/dist/modules/entities/lib/restrictedEntityFeatures.js +62 -0
  29. package/dist/modules/entities/lib/restrictedEntityFeatures.js.map +7 -0
  30. package/dist/modules/entities/migrations/Migration20260716120000.js +13 -0
  31. package/dist/modules/entities/migrations/Migration20260716120000.js.map +7 -0
  32. package/generated/entities/custom_entity/index.ts +1 -0
  33. package/generated/entity-fields-registry.ts +1 -0
  34. package/package.json +7 -7
  35. package/src/modules/auth/api/features.ts +17 -0
  36. package/src/modules/customers/i18n/pl.json +484 -484
  37. package/src/modules/entities/api/entities.ts +28 -0
  38. package/src/modules/entities/api/entity-settings.ts +103 -0
  39. package/src/modules/entities/api/records.ts +70 -29
  40. package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +12 -3
  41. package/src/modules/entities/backend/entities/user/create/page.tsx +33 -1
  42. package/src/modules/entities/data/entities.ts +6 -0
  43. package/src/modules/entities/i18n/de.json +3 -0
  44. package/src/modules/entities/i18n/en.json +3 -0
  45. package/src/modules/entities/i18n/es.json +3 -0
  46. package/src/modules/entities/i18n/pl.json +3 -0
  47. package/src/modules/entities/lib/entityAcl.ts +27 -5
  48. package/src/modules/entities/lib/install-from-ce.ts +2 -0
  49. package/src/modules/entities/lib/recordFeatures.ts +48 -0
  50. package/src/modules/entities/lib/register.ts +4 -0
  51. package/src/modules/entities/lib/restrictedEntityFeatures.ts +98 -0
  52. package/src/modules/entities/migrations/.snapshot-open-mercato.json +10 -0
  53. package/src/modules/entities/migrations/Migration20260716120000.ts +13 -0
@@ -0,0 +1,48 @@
1
+ // Pure, dependency-free helpers describing the ACL features that gate
2
+ // custom-entity records. Kept import-free so both the enforcement path
3
+ // (`entityAcl.ts`) and the feature catalog (`auth/api/features.ts`) can derive
4
+ // the SAME feature id without coupling to each other or to the ORM.
5
+ //
6
+ // Coarse (route-level) features stay entity-agnostic:
7
+ // entities.records.view — read any non-restricted custom entity's records
8
+ // entities.records.manage — write any non-restricted custom entity's records
9
+ //
10
+ // A custom entity flagged `access_restricted` ALSO requires a synthesized
11
+ // per-entity feature keyed on its immutable entityId:
12
+ // entities.records.<entityId>.view
13
+ // entities.records.<entityId>.manage
14
+ //
15
+ // entityId always matches `^[a-z0-9_]+:[a-z0-9_]+$` (it contains a `:` and no
16
+ // `.`), so it can never collide with the coarse `view`/`manage` segments and
17
+ // stays a single dotted segment for the wildcard matcher.
18
+
19
+ export type RecordsAction = 'view' | 'manage'
20
+
21
+ export const RECORDS_VIEW_FEATURE = 'entities.records.view'
22
+ export const RECORDS_MANAGE_FEATURE = 'entities.records.manage'
23
+
24
+ export function coarseRecordFeature(action: RecordsAction): string {
25
+ return action === 'manage' ? RECORDS_MANAGE_FEATURE : RECORDS_VIEW_FEATURE
26
+ }
27
+
28
+ export function deriveCustomEntityRecordFeature(entityId: string, action: RecordsAction): string {
29
+ return `entities.records.${entityId}.${action}`
30
+ }
31
+
32
+ export type SynthesizedRecordFeature = {
33
+ id: string
34
+ action: RecordsAction
35
+ dependsOn: string[]
36
+ }
37
+
38
+ // The per-entity features an admin can grant for a restricted custom entity.
39
+ // `dependsOn` mirrors the coarse prerequisite composition: the per-entity grant
40
+ // is meaningful only alongside the coarse route-level feature.
41
+ export function synthesizedRecordFeatures(entityId: string): SynthesizedRecordFeature[] {
42
+ const view = deriveCustomEntityRecordFeature(entityId, 'view')
43
+ const manage = deriveCustomEntityRecordFeature(entityId, 'manage')
44
+ return [
45
+ { id: view, action: 'view', dependsOn: [RECORDS_VIEW_FEATURE] },
46
+ { id: manage, action: 'manage', dependsOn: [view, RECORDS_MANAGE_FEATURE] },
47
+ ]
48
+ }
@@ -7,6 +7,7 @@ export type UpsertEntityOptions = {
7
7
  organizationId?: string | null
8
8
  tenantId?: string | null
9
9
  showInSidebar?: boolean
10
+ accessRestricted?: boolean
10
11
  labelField?: string | null
11
12
  defaultEditor?: string | null
12
13
  isActive?: boolean
@@ -29,6 +30,7 @@ export async function upsertCustomEntity(em: EntityManager, entityId: string, op
29
30
  description: opts.description ?? null,
30
31
  isActive: opts.isActive ?? true,
31
32
  showInSidebar: !!opts.showInSidebar,
33
+ accessRestricted: !!opts.accessRestricted,
32
34
  labelField: opts.labelField ?? null,
33
35
  defaultEditor: opts.defaultEditor ?? null,
34
36
  }
@@ -40,6 +42,7 @@ export async function upsertCustomEntity(em: EntityManager, entityId: string, op
40
42
  ent.description = desired.description
41
43
  ent.isActive = desired.isActive
42
44
  ent.showInSidebar = desired.showInSidebar
45
+ ent.accessRestricted = desired.accessRestricted
43
46
  ent.labelField = desired.labelField
44
47
  ent.defaultEditor = desired.defaultEditor
45
48
  ent.updatedAt = new Date()
@@ -51,6 +54,7 @@ export async function upsertCustomEntity(em: EntityManager, entityId: string, op
51
54
  if ((ent.description ?? null) !== desired.description) return true
52
55
  if ((ent.isActive ?? true) !== desired.isActive) return true
53
56
  if ((ent.showInSidebar ?? false) !== desired.showInSidebar) return true
57
+ if ((ent.accessRestricted ?? false) !== desired.accessRestricted) return true
54
58
  if ((ent.labelField ?? null) !== desired.labelField) return true
55
59
  if ((ent.defaultEditor ?? null) !== desired.defaultEditor) return true
56
60
  if (ent.deletedAt != null) return true
@@ -0,0 +1,98 @@
1
+ import { getModules } from '@open-mercato/shared/lib/i18n/server'
2
+ import { synthesizedRecordFeatures } from './recordFeatures'
3
+
4
+ export type SynthesizedFeatureItem = {
5
+ id: string
6
+ title: string
7
+ module: string
8
+ dependsOn?: string[]
9
+ }
10
+
11
+ type RestrictedEntity = { entityId: string; label: string }
12
+
13
+ type DeclaredEntities = { restricted: RestrictedEntity[]; ids: Set<string> }
14
+
15
+ // Reads module-declared (ce.ts) custom entities. `restricted` holds those flagged
16
+ // accessRestricted; `ids` holds ALL declared ids so the DB path can defer to the
17
+ // declaration — the declared registry is authoritative for its own ids, matching
18
+ // the records-route enforcement precedence (declared wins over the DB row).
19
+ function declaredEntities(): DeclaredEntities {
20
+ const restricted: RestrictedEntity[] = []
21
+ const ids = new Set<string>()
22
+ try {
23
+ const mods = getModules() as Array<{
24
+ customEntities?: Array<{ id?: string; label?: string; accessRestricted?: boolean }>
25
+ }>
26
+ for (const mod of mods || []) {
27
+ for (const spec of mod?.customEntities ?? []) {
28
+ if (!spec?.id) continue
29
+ ids.add(spec.id)
30
+ if (spec.accessRestricted === true) restricted.push({ entityId: spec.id, label: spec.label || spec.id })
31
+ }
32
+ }
33
+ } catch {}
34
+ return { restricted, ids }
35
+ }
36
+
37
+ // Reads user-registered custom entities flagged access_restricted for the given
38
+ // tenant scope. Uses the raw table (not the ORM class) to keep this helper cheap
39
+ // and avoid pulling entity metadata into unrelated callers.
40
+ async function registeredRestrictedEntities(em: any, tenantId: string): Promise<RestrictedEntity[]> {
41
+ try {
42
+ const db = em.getKysely()
43
+ const rows = await db
44
+ .selectFrom('custom_entities' as any)
45
+ .select(['entity_id' as any, 'label' as any])
46
+ .where('access_restricted' as any, '=', true)
47
+ .where('deleted_at' as any, 'is', null as any)
48
+ .where((eb: any) =>
49
+ eb.or([
50
+ eb('tenant_id' as any, '=', tenantId),
51
+ eb('tenant_id' as any, 'is', null as any),
52
+ ]),
53
+ )
54
+ .execute()
55
+ return (rows as Array<{ entity_id?: unknown; label?: unknown }>).map((row) => ({
56
+ entityId: String(row.entity_id ?? ''),
57
+ label: typeof row.label === 'string' && row.label.length ? row.label : String(row.entity_id ?? ''),
58
+ })).filter((row) => row.entityId.length > 0)
59
+ } catch {
60
+ return []
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Feature-catalog contribution for restricted custom entities: two synthesized
66
+ * per-entity features (view/manage) per restricted entity in the tenant scope,
67
+ * so admins can grant them in the Role/User ACL editor. Fails safe (returns what
68
+ * it can) — never throws — so the static feature catalog is unaffected on error.
69
+ */
70
+ export async function synthesizeRestrictedEntityFeatures(
71
+ em: any,
72
+ tenantId: string | null | undefined,
73
+ ): Promise<SynthesizedFeatureItem[]> {
74
+ if (!tenantId) return []
75
+ const declared = declaredEntities()
76
+ const byEntityId = new Map<string, RestrictedEntity>()
77
+ for (const entity of declared.restricted) byEntityId.set(entity.entityId, entity)
78
+ // DB rows are authoritative only for ids the declared registry does not own, so
79
+ // the catalog and the records-route enforcement agree on the same source.
80
+ for (const entity of await registeredRestrictedEntities(em, tenantId)) {
81
+ if (declared.ids.has(entity.entityId)) continue
82
+ byEntityId.set(entity.entityId, entity)
83
+ }
84
+
85
+ const items: SynthesizedFeatureItem[] = []
86
+ for (const { entityId, label } of byEntityId.values()) {
87
+ for (const feature of synthesizedRecordFeatures(entityId)) {
88
+ const verb = feature.action === 'manage' ? 'Manage records' : 'View records'
89
+ items.push({
90
+ id: feature.id,
91
+ title: `${verb}: ${label}`,
92
+ module: 'entities',
93
+ dependsOn: feature.dependsOn,
94
+ })
95
+ }
96
+ }
97
+ return items
98
+ }
@@ -71,6 +71,16 @@
71
71
  "default": "false",
72
72
  "mappedType": "boolean"
73
73
  },
74
+ "access_restricted": {
75
+ "name": "access_restricted",
76
+ "type": "boolean",
77
+ "unsigned": false,
78
+ "autoincrement": false,
79
+ "primary": false,
80
+ "nullable": false,
81
+ "default": "false",
82
+ "mappedType": "boolean"
83
+ },
74
84
  "organization_id": {
75
85
  "name": "organization_id",
76
86
  "type": "uuid",
@@ -0,0 +1,13 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260716120000 extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ this.addSql(`alter table "custom_entities" add column "access_restricted" boolean not null default false;`);
7
+ }
8
+
9
+ override async down(): Promise<void> {
10
+ this.addSql(`alter table "custom_entities" drop column "access_restricted";`);
11
+ }
12
+
13
+ }