@open-mercato/shared 0.4.2-canary-c02407ff85

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 (324) hide show
  1. package/build.mjs +101 -0
  2. package/dist/index.js +1 -0
  3. package/dist/index.js.map +7 -0
  4. package/dist/lib/api/crud.js +47 -0
  5. package/dist/lib/api/crud.js.map +7 -0
  6. package/dist/lib/api/scoped.js +140 -0
  7. package/dist/lib/api/scoped.js.map +7 -0
  8. package/dist/lib/auth/jwt.js +34 -0
  9. package/dist/lib/auth/jwt.js.map +7 -0
  10. package/dist/lib/auth/server.js +157 -0
  11. package/dist/lib/auth/server.js.map +7 -0
  12. package/dist/lib/boolean.js +22 -0
  13. package/dist/lib/boolean.js.map +7 -0
  14. package/dist/lib/bootstrap/appResolver.js +43 -0
  15. package/dist/lib/bootstrap/appResolver.js.map +7 -0
  16. package/dist/lib/bootstrap/dynamicLoader.js +108 -0
  17. package/dist/lib/bootstrap/dynamicLoader.js.map +7 -0
  18. package/dist/lib/bootstrap/factory.js +59 -0
  19. package/dist/lib/bootstrap/factory.js.map +7 -0
  20. package/dist/lib/bootstrap/index.js +11 -0
  21. package/dist/lib/bootstrap/index.js.map +7 -0
  22. package/dist/lib/bootstrap/types.js +1 -0
  23. package/dist/lib/bootstrap/types.js.map +7 -0
  24. package/dist/lib/cache/segments.js +36 -0
  25. package/dist/lib/cache/segments.js.map +7 -0
  26. package/dist/lib/cli/progress.js +46 -0
  27. package/dist/lib/cli/progress.js.map +7 -0
  28. package/dist/lib/commands/command-bus.js +285 -0
  29. package/dist/lib/commands/command-bus.js.map +7 -0
  30. package/dist/lib/commands/customFieldSnapshots.js +66 -0
  31. package/dist/lib/commands/customFieldSnapshots.js.map +7 -0
  32. package/dist/lib/commands/helpers.js +98 -0
  33. package/dist/lib/commands/helpers.js.map +7 -0
  34. package/dist/lib/commands/index.js +8 -0
  35. package/dist/lib/commands/index.js.map +7 -0
  36. package/dist/lib/commands/operationMetadata.js +32 -0
  37. package/dist/lib/commands/operationMetadata.js.map +7 -0
  38. package/dist/lib/commands/registry.js +43 -0
  39. package/dist/lib/commands/registry.js.map +7 -0
  40. package/dist/lib/commands/scope.js +44 -0
  41. package/dist/lib/commands/scope.js.map +7 -0
  42. package/dist/lib/commands/types.js +8 -0
  43. package/dist/lib/commands/types.js.map +7 -0
  44. package/dist/lib/crud/cache-stats.js +98 -0
  45. package/dist/lib/crud/cache-stats.js.map +7 -0
  46. package/dist/lib/crud/cache.js +175 -0
  47. package/dist/lib/crud/cache.js.map +7 -0
  48. package/dist/lib/crud/custom-fields-client.js +52 -0
  49. package/dist/lib/crud/custom-fields-client.js.map +7 -0
  50. package/dist/lib/crud/custom-fields.js +467 -0
  51. package/dist/lib/crud/custom-fields.js.map +7 -0
  52. package/dist/lib/crud/errors.js +24 -0
  53. package/dist/lib/crud/errors.js.map +7 -0
  54. package/dist/lib/crud/exporters.js +154 -0
  55. package/dist/lib/crud/exporters.js.map +7 -0
  56. package/dist/lib/crud/factory.js +1311 -0
  57. package/dist/lib/crud/factory.js.map +7 -0
  58. package/dist/lib/crud/types.js +1 -0
  59. package/dist/lib/crud/types.js.map +7 -0
  60. package/dist/lib/custom-fields/normalize.js +36 -0
  61. package/dist/lib/custom-fields/normalize.js.map +7 -0
  62. package/dist/lib/data/engine.js +396 -0
  63. package/dist/lib/data/engine.js.map +7 -0
  64. package/dist/lib/db/escapeLikePattern.js +5 -0
  65. package/dist/lib/db/escapeLikePattern.js.map +7 -0
  66. package/dist/lib/db/mikro.js +82 -0
  67. package/dist/lib/db/mikro.js.map +7 -0
  68. package/dist/lib/di/container.js +94 -0
  69. package/dist/lib/di/container.js.map +7 -0
  70. package/dist/lib/email/send.js +12 -0
  71. package/dist/lib/email/send.js.map +7 -0
  72. package/dist/lib/encryption/aes.js +58 -0
  73. package/dist/lib/encryption/aes.js.map +7 -0
  74. package/dist/lib/encryption/customFieldValues.js +49 -0
  75. package/dist/lib/encryption/customFieldValues.js.map +7 -0
  76. package/dist/lib/encryption/entityFields.js +26 -0
  77. package/dist/lib/encryption/entityFields.js.map +7 -0
  78. package/dist/lib/encryption/entityIds.js +80 -0
  79. package/dist/lib/encryption/entityIds.js.map +7 -0
  80. package/dist/lib/encryption/find.js +45 -0
  81. package/dist/lib/encryption/find.js.map +7 -0
  82. package/dist/lib/encryption/indexDoc.js +69 -0
  83. package/dist/lib/encryption/indexDoc.js.map +7 -0
  84. package/dist/lib/encryption/kms.js +282 -0
  85. package/dist/lib/encryption/kms.js.map +7 -0
  86. package/dist/lib/encryption/subscriber.js +330 -0
  87. package/dist/lib/encryption/subscriber.js.map +7 -0
  88. package/dist/lib/encryption/tenantDataEncryptionService.js +252 -0
  89. package/dist/lib/encryption/tenantDataEncryptionService.js.map +7 -0
  90. package/dist/lib/encryption/toggles.js +18 -0
  91. package/dist/lib/encryption/toggles.js.map +7 -0
  92. package/dist/lib/entities/naming.js +9 -0
  93. package/dist/lib/entities/naming.js.map +7 -0
  94. package/dist/lib/entities/system-entities.js +43 -0
  95. package/dist/lib/entities/system-entities.js.map +7 -0
  96. package/dist/lib/frontend/organizationEvents.js +41 -0
  97. package/dist/lib/frontend/organizationEvents.js.map +7 -0
  98. package/dist/lib/frontend/useOrganizationScope.js +32 -0
  99. package/dist/lib/frontend/useOrganizationScope.js.map +7 -0
  100. package/dist/lib/hotkeys/index.js +128 -0
  101. package/dist/lib/hotkeys/index.js.map +7 -0
  102. package/dist/lib/i18n/app-dictionaries.js +17 -0
  103. package/dist/lib/i18n/app-dictionaries.js.map +7 -0
  104. package/dist/lib/i18n/config.js +7 -0
  105. package/dist/lib/i18n/config.js.map +7 -0
  106. package/dist/lib/i18n/context.js +50 -0
  107. package/dist/lib/i18n/context.js.map +7 -0
  108. package/dist/lib/i18n/server.js +68 -0
  109. package/dist/lib/i18n/server.js.map +7 -0
  110. package/dist/lib/i18n/translate.js +45 -0
  111. package/dist/lib/i18n/translate.js.map +7 -0
  112. package/dist/lib/indexers/error-log.js +82 -0
  113. package/dist/lib/indexers/error-log.js.map +7 -0
  114. package/dist/lib/indexers/status-log.js +80 -0
  115. package/dist/lib/indexers/status-log.js.map +7 -0
  116. package/dist/lib/lib/auth/jwt.js +34 -0
  117. package/dist/lib/lib/auth/jwt.js.map +7 -0
  118. package/dist/lib/lib/auth/server.js +77 -0
  119. package/dist/lib/lib/auth/server.js.map +7 -0
  120. package/dist/lib/lib/email/send.js +12 -0
  121. package/dist/lib/lib/email/send.js.map +7 -0
  122. package/dist/lib/lib/i18n/config.js +7 -0
  123. package/dist/lib/lib/i18n/config.js.map +7 -0
  124. package/dist/lib/lib/i18n/context.js +31 -0
  125. package/dist/lib/lib/i18n/context.js.map +7 -0
  126. package/dist/lib/lib/utils.js +9 -0
  127. package/dist/lib/lib/utils.js.map +7 -0
  128. package/dist/lib/location/countries.js +68 -0
  129. package/dist/lib/location/countries.js.map +7 -0
  130. package/dist/lib/modules/index.js +6 -0
  131. package/dist/lib/modules/index.js.map +7 -0
  132. package/dist/lib/modules/registry.js +18 -0
  133. package/dist/lib/modules/registry.js.map +7 -0
  134. package/dist/lib/openapi/crud.js +137 -0
  135. package/dist/lib/openapi/crud.js.map +7 -0
  136. package/dist/lib/openapi/generator.js +1131 -0
  137. package/dist/lib/openapi/generator.js.map +7 -0
  138. package/dist/lib/openapi/index.js +10 -0
  139. package/dist/lib/openapi/index.js.map +7 -0
  140. package/dist/lib/openapi/sanitize.js +110 -0
  141. package/dist/lib/openapi/sanitize.js.map +7 -0
  142. package/dist/lib/openapi/types.js +1 -0
  143. package/dist/lib/openapi/types.js.map +7 -0
  144. package/dist/lib/profiler/index.js +258 -0
  145. package/dist/lib/profiler/index.js.map +7 -0
  146. package/dist/lib/query/engine.js +729 -0
  147. package/dist/lib/query/engine.js.map +7 -0
  148. package/dist/lib/query/join-utils.js +195 -0
  149. package/dist/lib/query/join-utils.js.map +7 -0
  150. package/dist/lib/query/types.js +9 -0
  151. package/dist/lib/query/types.js.map +7 -0
  152. package/dist/lib/search/config.js +32 -0
  153. package/dist/lib/search/config.js.map +7 -0
  154. package/dist/lib/search/tokenize.js +34 -0
  155. package/dist/lib/search/tokenize.js.map +7 -0
  156. package/dist/lib/slugify.js +24 -0
  157. package/dist/lib/slugify.js.map +7 -0
  158. package/dist/lib/testing/bootstrap.js +51 -0
  159. package/dist/lib/testing/bootstrap.js.map +7 -0
  160. package/dist/lib/testing/index.js +17 -0
  161. package/dist/lib/testing/index.js.map +7 -0
  162. package/dist/lib/testing/renderWithProviders.js +15 -0
  163. package/dist/lib/testing/renderWithProviders.js.map +7 -0
  164. package/dist/lib/url.js +12 -0
  165. package/dist/lib/url.js.map +7 -0
  166. package/dist/lib/utils.js +13 -0
  167. package/dist/lib/utils.js.map +7 -0
  168. package/dist/lib/version.js +7 -0
  169. package/dist/lib/version.js.map +7 -0
  170. package/dist/modules/dashboard/widgets.js +1 -0
  171. package/dist/modules/dashboard/widgets.js.map +7 -0
  172. package/dist/modules/dsl.js +30 -0
  173. package/dist/modules/dsl.js.map +7 -0
  174. package/dist/modules/entities/kinds.js +22 -0
  175. package/dist/modules/entities/kinds.js.map +7 -0
  176. package/dist/modules/entities/options.js +26 -0
  177. package/dist/modules/entities/options.js.map +7 -0
  178. package/dist/modules/entities/validation.js +102 -0
  179. package/dist/modules/entities/validation.js.map +7 -0
  180. package/dist/modules/entities/validators.js +88 -0
  181. package/dist/modules/entities/validators.js.map +7 -0
  182. package/dist/modules/entities.js +1 -0
  183. package/dist/modules/entities.js.map +7 -0
  184. package/dist/modules/navigation/sidebarPreferences.js +50 -0
  185. package/dist/modules/navigation/sidebarPreferences.js.map +7 -0
  186. package/dist/modules/perspectives/types.js +1 -0
  187. package/dist/modules/perspectives/types.js.map +7 -0
  188. package/dist/modules/registry.js +96 -0
  189. package/dist/modules/registry.js.map +7 -0
  190. package/dist/modules/search.js +15 -0
  191. package/dist/modules/search.js.map +7 -0
  192. package/dist/modules/vector.js +1 -0
  193. package/dist/modules/vector.js.map +7 -0
  194. package/dist/modules/widgets/injection-loader.js +180 -0
  195. package/dist/modules/widgets/injection-loader.js.map +7 -0
  196. package/dist/modules/widgets/injection.js +1 -0
  197. package/dist/modules/widgets/injection.js.map +7 -0
  198. package/dist/security/features.js +23 -0
  199. package/dist/security/features.js.map +7 -0
  200. package/dist/types/pg.d.js +1 -0
  201. package/dist/types/pg.d.js.map +7 -0
  202. package/dist/types/react-email.d.js +1 -0
  203. package/dist/types/react-email.d.js.map +7 -0
  204. package/dist/types/resend.d.js +1 -0
  205. package/dist/types/resend.d.js.map +7 -0
  206. package/jest.config.cjs +22 -0
  207. package/package.json +88 -0
  208. package/src/index.ts +0 -0
  209. package/src/lib/api/__tests__/scoped.test.ts +38 -0
  210. package/src/lib/api/crud.ts +59 -0
  211. package/src/lib/api/scoped.ts +239 -0
  212. package/src/lib/auth/jwt.ts +39 -0
  213. package/src/lib/auth/server.ts +199 -0
  214. package/src/lib/boolean.ts +17 -0
  215. package/src/lib/bootstrap/appResolver.ts +85 -0
  216. package/src/lib/bootstrap/dynamicLoader.ts +177 -0
  217. package/src/lib/bootstrap/factory.ts +108 -0
  218. package/src/lib/bootstrap/index.ts +23 -0
  219. package/src/lib/bootstrap/types.ts +31 -0
  220. package/src/lib/cache/segments.ts +56 -0
  221. package/src/lib/cli/progress.ts +55 -0
  222. package/src/lib/commands/__tests__/command-bus.test.ts +84 -0
  223. package/src/lib/commands/__tests__/helpers.test.ts +42 -0
  224. package/src/lib/commands/command-bus.ts +349 -0
  225. package/src/lib/commands/customFieldSnapshots.ts +86 -0
  226. package/src/lib/commands/helpers.ts +143 -0
  227. package/src/lib/commands/index.ts +4 -0
  228. package/src/lib/commands/operationMetadata.ts +40 -0
  229. package/src/lib/commands/registry.ts +46 -0
  230. package/src/lib/commands/scope.ts +59 -0
  231. package/src/lib/commands/types.ts +63 -0
  232. package/src/lib/crud/__tests__/crud-factory.test.ts +333 -0
  233. package/src/lib/crud/__tests__/custom-fields.test.ts +150 -0
  234. package/src/lib/crud/cache-stats.ts +127 -0
  235. package/src/lib/crud/cache.ts +205 -0
  236. package/src/lib/crud/custom-fields-client.ts +54 -0
  237. package/src/lib/crud/custom-fields.ts +607 -0
  238. package/src/lib/crud/errors.ts +23 -0
  239. package/src/lib/crud/exporters.ts +188 -0
  240. package/src/lib/crud/factory.ts +1622 -0
  241. package/src/lib/crud/types.ts +29 -0
  242. package/src/lib/custom-fields/normalize.ts +45 -0
  243. package/src/lib/data/engine.ts +562 -0
  244. package/src/lib/db/escapeLikePattern.ts +2 -0
  245. package/src/lib/db/mikro.ts +100 -0
  246. package/src/lib/di/container.ts +105 -0
  247. package/src/lib/email/send.ts +18 -0
  248. package/src/lib/encryption/__tests__/customFieldValues.test.ts +63 -0
  249. package/src/lib/encryption/__tests__/indexDoc.test.ts +115 -0
  250. package/src/lib/encryption/aes.ts +64 -0
  251. package/src/lib/encryption/customFieldValues.ts +67 -0
  252. package/src/lib/encryption/entityFields.ts +39 -0
  253. package/src/lib/encryption/entityIds.ts +107 -0
  254. package/src/lib/encryption/find.ts +81 -0
  255. package/src/lib/encryption/indexDoc.ts +104 -0
  256. package/src/lib/encryption/kms.ts +337 -0
  257. package/src/lib/encryption/subscriber.ts +416 -0
  258. package/src/lib/encryption/tenantDataEncryptionService.ts +313 -0
  259. package/src/lib/encryption/toggles.ts +15 -0
  260. package/src/lib/entities/naming.ts +6 -0
  261. package/src/lib/entities/system-entities.ts +43 -0
  262. package/src/lib/frontend/organizationEvents.ts +55 -0
  263. package/src/lib/frontend/useOrganizationScope.ts +30 -0
  264. package/src/lib/hotkeys/index.ts +168 -0
  265. package/src/lib/i18n/app-dictionaries.ts +18 -0
  266. package/src/lib/i18n/config.ts +4 -0
  267. package/src/lib/i18n/context.tsx +66 -0
  268. package/src/lib/i18n/server.ts +74 -0
  269. package/src/lib/i18n/translate.ts +54 -0
  270. package/src/lib/indexers/error-log.ts +106 -0
  271. package/src/lib/indexers/status-log.ts +119 -0
  272. package/src/lib/lib/auth/jwt.ts +39 -0
  273. package/src/lib/lib/auth/server.ts +94 -0
  274. package/src/lib/lib/email/send.ts +18 -0
  275. package/src/lib/lib/i18n/config.ts +4 -0
  276. package/src/lib/lib/i18n/context.tsx +38 -0
  277. package/src/lib/lib/utils.ts +6 -0
  278. package/src/lib/location/countries.ts +97 -0
  279. package/src/lib/modules/index.ts +1 -0
  280. package/src/lib/modules/registry.ts +18 -0
  281. package/src/lib/openapi/crud.ts +218 -0
  282. package/src/lib/openapi/generator.ts +1311 -0
  283. package/src/lib/openapi/index.ts +4 -0
  284. package/src/lib/openapi/sanitize.ts +137 -0
  285. package/src/lib/openapi/types.ts +79 -0
  286. package/src/lib/profiler/index.ts +371 -0
  287. package/src/lib/query/__tests__/engine.test.ts +274 -0
  288. package/src/lib/query/engine.ts +837 -0
  289. package/src/lib/query/join-utils.ts +238 -0
  290. package/src/lib/query/types.ts +121 -0
  291. package/src/lib/search/config.ts +49 -0
  292. package/src/lib/search/tokenize.ts +45 -0
  293. package/src/lib/slugify.ts +28 -0
  294. package/src/lib/testing/bootstrap.ts +124 -0
  295. package/src/lib/testing/index.ts +15 -0
  296. package/src/lib/testing/renderWithProviders.tsx +31 -0
  297. package/src/lib/url.ts +12 -0
  298. package/src/lib/utils.ts +17 -0
  299. package/src/lib/version.ts +5 -0
  300. package/src/modules/__tests__/dsl.test.ts +35 -0
  301. package/src/modules/__tests__/registry.test.ts +300 -0
  302. package/src/modules/dashboard/widgets.ts +57 -0
  303. package/src/modules/dsl.ts +32 -0
  304. package/src/modules/entities/__tests__/validation.test.ts +52 -0
  305. package/src/modules/entities/kinds.ts +20 -0
  306. package/src/modules/entities/options.ts +36 -0
  307. package/src/modules/entities/validation.ts +118 -0
  308. package/src/modules/entities/validators.ts +93 -0
  309. package/src/modules/entities.ts +102 -0
  310. package/src/modules/navigation/sidebarPreferences.ts +62 -0
  311. package/src/modules/perspectives/types.ts +40 -0
  312. package/src/modules/registry.ts +249 -0
  313. package/src/modules/search.ts +325 -0
  314. package/src/modules/vector.ts +122 -0
  315. package/src/modules/widgets/__tests__/injection.test.ts +48 -0
  316. package/src/modules/widgets/injection-loader.ts +235 -0
  317. package/src/modules/widgets/injection.ts +120 -0
  318. package/src/security/features.ts +22 -0
  319. package/src/types/pg.d.ts +2 -0
  320. package/src/types/react-email.d.ts +2 -0
  321. package/src/types/resend.d.ts +2 -0
  322. package/tsconfig.build.json +11 -0
  323. package/tsconfig.json +9 -0
  324. package/watch.mjs +6 -0
@@ -0,0 +1,35 @@
1
+ import { defineLink, entityId, linkable, defineFields, cf } from '../../modules/dsl'
2
+
3
+ describe('DSL helpers', () => {
4
+ test('defineLink + entityId', () => {
5
+ const link = defineLink(entityId('auth','user'), entityId('my_module','user_profile'), {
6
+ join: { baseKey: 'id', extensionKey: 'user_id' },
7
+ cardinality: 'one-to-one',
8
+ required: false,
9
+ description: 'Profile for user',
10
+ })
11
+ expect(link.base).toBe('auth:user')
12
+ expect(link.extension).toBe('my_module:user_profile')
13
+ expect(link.join).toEqual({ baseKey: 'id', extensionKey: 'user_id' })
14
+ })
15
+
16
+ test('linkable helper', () => {
17
+ const Auth = { linkable: linkable('auth', ['user','role']) }
18
+ expect(Auth.linkable.user).toBe('auth:user')
19
+ expect(Auth.linkable.role).toBe('auth:role')
20
+ })
21
+
22
+ test('defineFields + cf helpers', () => {
23
+ const set = defineFields(entityId('directory','organization'), [
24
+ cf.select('industry', ['SaaS','Retail'], { filterable: true }),
25
+ cf.boolean('vip', { required: false }),
26
+ cf.integer('priority'),
27
+ ], 'my_module')
28
+ expect(set.entity).toBe('directory:organization')
29
+ expect(set.fields[0]).toMatchObject({ key: 'industry', kind: 'select', options: ['SaaS','Retail'], filterable: true })
30
+ expect(set.fields[1]).toMatchObject({ key: 'vip', kind: 'boolean' })
31
+ expect(set.fields[2]).toMatchObject({ key: 'priority', kind: 'integer' })
32
+ expect(set.source).toBe('my_module')
33
+ })
34
+ })
35
+
@@ -0,0 +1,300 @@
1
+ import type { Module } from '../registry'
2
+ import {
3
+ registerCliModules,
4
+ getCliModules,
5
+ hasCliModules,
6
+ findFrontendMatch,
7
+ findBackendMatch,
8
+ findApi,
9
+ } from '../registry'
10
+
11
+ describe('CLI Modules Registry', () => {
12
+ // Clear the registry before each test
13
+ beforeEach(() => {
14
+ registerCliModules([])
15
+ })
16
+
17
+ afterEach(() => {
18
+ registerCliModules([])
19
+ })
20
+
21
+ describe('registerCliModules', () => {
22
+ it('should register modules', () => {
23
+ const modules: Module[] = [
24
+ { id: 'module-a' },
25
+ { id: 'module-b' },
26
+ ]
27
+
28
+ registerCliModules(modules)
29
+
30
+ expect(getCliModules()).toEqual(modules)
31
+ })
32
+
33
+ it('should overwrite previously registered modules', () => {
34
+ const modules1: Module[] = [{ id: 'module-a' }]
35
+ const modules2: Module[] = [{ id: 'module-b' }, { id: 'module-c' }]
36
+
37
+ registerCliModules(modules1)
38
+ expect(getCliModules().length).toBe(1)
39
+
40
+ registerCliModules(modules2)
41
+ expect(getCliModules().length).toBe(2)
42
+ expect(getCliModules().map(m => m.id)).toEqual(['module-b', 'module-c'])
43
+ })
44
+ })
45
+
46
+ describe('getCliModules', () => {
47
+ it('should return empty array when no modules registered', () => {
48
+ expect(getCliModules()).toEqual([])
49
+ })
50
+
51
+ it('should return registered modules', () => {
52
+ const modules: Module[] = [
53
+ { id: 'test-module', info: { name: 'Test Module' } },
54
+ ]
55
+
56
+ registerCliModules(modules)
57
+
58
+ const result = getCliModules()
59
+ expect(result.length).toBe(1)
60
+ expect(result[0].id).toBe('test-module')
61
+ expect(result[0].info?.name).toBe('Test Module')
62
+ })
63
+ })
64
+
65
+ describe('hasCliModules', () => {
66
+ it('should return false when no modules registered', () => {
67
+ expect(hasCliModules()).toBe(false)
68
+ })
69
+
70
+ it('should return false for empty array', () => {
71
+ registerCliModules([])
72
+ expect(hasCliModules()).toBe(false)
73
+ })
74
+
75
+ it('should return true when modules are registered', () => {
76
+ registerCliModules([{ id: 'some-module' }])
77
+ expect(hasCliModules()).toBe(true)
78
+ })
79
+ })
80
+
81
+ describe('findFrontendMatch', () => {
82
+ it('should match simple routes', () => {
83
+ const modules: Module[] = [
84
+ {
85
+ id: 'test',
86
+ frontendRoutes: [
87
+ {
88
+ pattern: '/dashboard',
89
+ Component: () => null,
90
+ },
91
+ ],
92
+ },
93
+ ]
94
+
95
+ const result = findFrontendMatch(modules, '/dashboard')
96
+ expect(result).toBeDefined()
97
+ expect(result?.params).toEqual({})
98
+ })
99
+
100
+ it('should match dynamic routes', () => {
101
+ const modules: Module[] = [
102
+ {
103
+ id: 'test',
104
+ frontendRoutes: [
105
+ {
106
+ pattern: '/users/[id]',
107
+ Component: () => null,
108
+ },
109
+ ],
110
+ },
111
+ ]
112
+
113
+ const result = findFrontendMatch(modules, '/users/123')
114
+ expect(result).toBeDefined()
115
+ expect(result?.params).toEqual({ id: '123' })
116
+ })
117
+
118
+ it('should match catch-all routes', () => {
119
+ const modules: Module[] = [
120
+ {
121
+ id: 'test',
122
+ frontendRoutes: [
123
+ {
124
+ pattern: '/docs/[...slug]',
125
+ Component: () => null,
126
+ },
127
+ ],
128
+ },
129
+ ]
130
+
131
+ const result = findFrontendMatch(modules, '/docs/api/getting-started')
132
+ expect(result).toBeDefined()
133
+ expect(result?.params).toEqual({ slug: ['api', 'getting-started'] })
134
+ })
135
+
136
+ it('should return undefined for non-matching routes', () => {
137
+ const modules: Module[] = [
138
+ {
139
+ id: 'test',
140
+ frontendRoutes: [
141
+ {
142
+ pattern: '/dashboard',
143
+ Component: () => null,
144
+ },
145
+ ],
146
+ },
147
+ ]
148
+
149
+ const result = findFrontendMatch(modules, '/settings')
150
+ expect(result).toBeUndefined()
151
+ })
152
+ })
153
+
154
+ describe('findBackendMatch', () => {
155
+ it('should match backend routes', () => {
156
+ const modules: Module[] = [
157
+ {
158
+ id: 'test',
159
+ backendRoutes: [
160
+ {
161
+ pattern: '/backend/settings',
162
+ Component: () => null,
163
+ },
164
+ ],
165
+ },
166
+ ]
167
+
168
+ const result = findBackendMatch(modules, '/backend/settings')
169
+ expect(result).toBeDefined()
170
+ })
171
+
172
+ it('should match dynamic backend routes', () => {
173
+ const modules: Module[] = [
174
+ {
175
+ id: 'test',
176
+ backendRoutes: [
177
+ {
178
+ pattern: '/backend/users/[id]/edit',
179
+ Component: () => null,
180
+ },
181
+ ],
182
+ },
183
+ ]
184
+
185
+ const result = findBackendMatch(modules, '/backend/users/456/edit')
186
+ expect(result).toBeDefined()
187
+ expect(result?.params).toEqual({ id: '456' })
188
+ })
189
+ })
190
+
191
+ describe('findApi', () => {
192
+ it('should find API handlers by method and path', () => {
193
+ const mockHandler = async () => new Response('OK')
194
+ const modules: Module[] = [
195
+ {
196
+ id: 'test',
197
+ apis: [
198
+ {
199
+ path: '/users',
200
+ handlers: {
201
+ GET: mockHandler,
202
+ POST: mockHandler,
203
+ },
204
+ },
205
+ ],
206
+ },
207
+ ]
208
+
209
+ const getResult = findApi(modules, 'GET', '/users')
210
+ expect(getResult).toBeDefined()
211
+ expect(getResult?.handler).toBe(mockHandler)
212
+
213
+ const postResult = findApi(modules, 'POST', '/users')
214
+ expect(postResult).toBeDefined()
215
+
216
+ const deleteResult = findApi(modules, 'DELETE', '/users')
217
+ expect(deleteResult).toBeUndefined()
218
+ })
219
+
220
+ it('should match dynamic API paths', () => {
221
+ const mockHandler = async () => new Response('OK')
222
+ const modules: Module[] = [
223
+ {
224
+ id: 'test',
225
+ apis: [
226
+ {
227
+ path: '/users/[id]',
228
+ handlers: {
229
+ GET: mockHandler,
230
+ },
231
+ },
232
+ ],
233
+ },
234
+ ]
235
+
236
+ const result = findApi(modules, 'GET', '/users/123')
237
+ expect(result).toBeDefined()
238
+ expect(result?.params).toEqual({ id: '123' })
239
+ })
240
+
241
+ it('should include requireAuth and requireRoles', () => {
242
+ const mockHandler = async () => new Response('OK')
243
+ const modules: Module[] = [
244
+ {
245
+ id: 'test',
246
+ apis: [
247
+ {
248
+ path: '/admin',
249
+ handlers: { GET: mockHandler },
250
+ requireAuth: true,
251
+ requireRoles: ['admin'],
252
+ },
253
+ ],
254
+ },
255
+ ]
256
+
257
+ const result = findApi(modules, 'GET', '/admin')
258
+ expect(result).toBeDefined()
259
+ expect(result?.requireAuth).toBe(true)
260
+ expect(result?.requireRoles).toEqual(['admin'])
261
+ })
262
+ })
263
+ })
264
+
265
+ describe('Module type with workers', () => {
266
+ it('should allow workers property on Module', () => {
267
+ const module: Module = {
268
+ id: 'test-module',
269
+ workers: [
270
+ {
271
+ id: 'test:worker',
272
+ queue: 'test-queue',
273
+ concurrency: 2,
274
+ handler: async () => {},
275
+ },
276
+ ],
277
+ }
278
+
279
+ expect(module.workers).toBeDefined()
280
+ expect(module.workers?.length).toBe(1)
281
+ expect(module.workers?.[0].queue).toBe('test-queue')
282
+ })
283
+
284
+ it('should allow subscribers property on Module', () => {
285
+ const module: Module = {
286
+ id: 'test-module',
287
+ subscribers: [
288
+ {
289
+ id: 'test:subscriber',
290
+ event: 'user.created',
291
+ handler: async () => {},
292
+ },
293
+ ],
294
+ }
295
+
296
+ expect(module.subscribers).toBeDefined()
297
+ expect(module.subscribers?.length).toBe(1)
298
+ expect(module.subscribers?.[0].event).toBe('user.created')
299
+ })
300
+ })
@@ -0,0 +1,57 @@
1
+ import type { ComponentType } from 'react'
2
+
3
+ export type DashboardWidgetSize = 'sm' | 'md' | 'lg'
4
+
5
+ export type DashboardWidgetMetadata = {
6
+ id: string
7
+ title: string
8
+ description?: string
9
+ subtitle?: string
10
+ features?: string[]
11
+ defaultSize?: DashboardWidgetSize
12
+ defaultSettings?: unknown
13
+ defaultEnabled?: boolean
14
+ tags?: string[]
15
+ category?: string
16
+ icon?: string
17
+ supportsRefresh?: boolean
18
+ }
19
+
20
+ export type DashboardLayoutItem = {
21
+ id: string
22
+ widgetId: string
23
+ order: number
24
+ priority?: number
25
+ size?: DashboardWidgetSize
26
+ settings?: unknown
27
+ }
28
+
29
+ export type DashboardWidgetRenderMode = 'view' | 'settings'
30
+
31
+ export type DashboardWidgetRenderContext = {
32
+ userId: string
33
+ tenantId?: string | null
34
+ organizationId?: string | null
35
+ userName?: string | null
36
+ userEmail?: string | null
37
+ userLabel?: string | null
38
+ }
39
+
40
+ export type DashboardWidgetComponentProps<TSettings = unknown> = {
41
+ mode: DashboardWidgetRenderMode
42
+ layout: DashboardLayoutItem
43
+ settings: TSettings
44
+ context: DashboardWidgetRenderContext
45
+ onSettingsChange: (next: TSettings) => void
46
+ refreshToken: number
47
+ onRefreshStateChange?: (refreshing: boolean) => void
48
+ }
49
+
50
+ export type DashboardWidgetModule<TSettings = unknown> = {
51
+ metadata: DashboardWidgetMetadata
52
+ Widget: ComponentType<DashboardWidgetComponentProps<TSettings>>
53
+ hydrateSettings?: (raw: unknown) => TSettings
54
+ dehydrateSettings?: (settings: TSettings) => unknown
55
+ }
56
+
57
+ export type DashboardWidgetRenderProps<TSettings = unknown> = DashboardWidgetComponentProps<TSettings>
@@ -0,0 +1,32 @@
1
+ import type { CustomFieldDefinition, CustomFieldSet, EntityExtension, EntityId } from '@open-mercato/shared/modules/entities'
2
+
3
+ export function entityId(moduleId: string, entity: string): EntityId {
4
+ return `${moduleId}:${entity}`
5
+ }
6
+
7
+ export function linkable(moduleId: string, entities: string[]): Record<string, EntityId> {
8
+ return Object.fromEntries(entities.map((e) => [e, entityId(moduleId, e)]))
9
+ }
10
+
11
+ export function defineLink(
12
+ base: EntityId,
13
+ extension: EntityId,
14
+ opts: Pick<EntityExtension, 'join' | 'cardinality' | 'required' | 'description'>
15
+ ): EntityExtension {
16
+ return { base, extension, ...opts }
17
+ }
18
+
19
+ export const cf = {
20
+ text: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'text', ...opts }),
21
+ multiline: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'multiline', ...opts }),
22
+ integer: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'integer', ...opts }),
23
+ float: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'float', ...opts }),
24
+ boolean: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'boolean', ...opts }),
25
+ select: (key: string, options: string[], opts: Omit<CustomFieldDefinition, 'key' | 'kind' | 'options'> = {}): CustomFieldDefinition => ({ key, kind: 'select', options, ...opts }),
26
+ currency: (key: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind'> = {}): CustomFieldDefinition => ({ key, kind: 'currency', ...opts }),
27
+ dictionary: (key: string, dictionaryId: string, opts: Omit<CustomFieldDefinition, 'key' | 'kind' | 'dictionaryId'> = {}): CustomFieldDefinition => ({ key, kind: 'dictionary', dictionaryId, ...opts }),
28
+ }
29
+
30
+ export function defineFields(entity: EntityId, fields: CustomFieldDefinition[], source?: string): CustomFieldSet {
31
+ return { entity, fields, source }
32
+ }
@@ -0,0 +1,52 @@
1
+ /** @jest-environment node */
2
+ import { validateValuesAgainstDefs } from '../validation'
3
+
4
+ describe('validateValuesAgainstDefs', () => {
5
+ it('validates required and integer/float comparisons', () => {
6
+ const defs = [
7
+ { key: 'a', kind: 'integer', configJson: { validation: [ { rule: 'required', message: 'a required' }, { rule: 'integer', message: 'a int' }, { rule: 'gte', param: 1, message: 'a >= 1' } ] } },
8
+ { key: 'b', kind: 'float', configJson: { validation: [ { rule: 'float', message: 'b float' }, { rule: 'lt', param: 10, message: 'b < 10' } ] } },
9
+ ]
10
+
11
+ // Empty a should fail required
12
+ let r = validateValuesAgainstDefs({}, defs as any)
13
+ expect(r.ok).toBe(false)
14
+ expect(r.fieldErrors['cf_a']).toBe('a required')
15
+
16
+ // Non-integer a should fail integer
17
+ r = validateValuesAgainstDefs({ a: 1.2 }, defs as any)
18
+ expect(r.ok).toBe(false)
19
+ expect(r.fieldErrors['cf_a']).toBe('a int')
20
+
21
+ // Too-small a should fail gte
22
+ r = validateValuesAgainstDefs({ a: 0 }, defs as any)
23
+ expect(r.ok).toBe(false)
24
+ expect(r.fieldErrors['cf_a']).toBe('a >= 1')
25
+
26
+ // b too large should fail lt
27
+ r = validateValuesAgainstDefs({ a: 2, b: 11 }, defs as any)
28
+ expect(r.ok).toBe(false)
29
+ expect(r.fieldErrors['cf_b']).toBe('b < 10')
30
+
31
+ // Both valid
32
+ r = validateValuesAgainstDefs({ a: 2, b: 9.5 }, defs as any)
33
+ expect(r.ok).toBe(true)
34
+ expect(Object.keys(r.fieldErrors).length).toBe(0)
35
+ })
36
+
37
+ it('validates regex and eq/ne', () => {
38
+ const defs = [
39
+ { key: 'code', kind: 'text', configJson: { validation: [ { rule: 'regex', param: '^[a-z0-9_-]+$', message: 'bad code' } ] } },
40
+ { key: 'status', kind: 'select', configJson: { validation: [ { rule: 'eq', param: 'open', message: 'must be open' } ] } },
41
+ ]
42
+ let r = validateValuesAgainstDefs({ code: 'Bad!' }, defs as any)
43
+ expect(r.ok).toBe(false)
44
+ expect(r.fieldErrors['cf_code']).toBe('bad code')
45
+ r = validateValuesAgainstDefs({ code: 'ok', status: 'closed' }, defs as any)
46
+ expect(r.ok).toBe(false)
47
+ expect(r.fieldErrors['cf_status']).toBe('must be open')
48
+ r = validateValuesAgainstDefs({ code: 'ok', status: 'open' }, defs as any)
49
+ expect(r.ok).toBe(true)
50
+ })
51
+ })
52
+
@@ -0,0 +1,20 @@
1
+ export const CUSTOM_FIELD_KINDS = [
2
+ 'text',
3
+ 'multiline',
4
+ 'integer',
5
+ 'float',
6
+ 'boolean',
7
+ 'select',
8
+ 'currency',
9
+ 'relation',
10
+ 'attachment',
11
+ 'dictionary',
12
+ ] as const
13
+
14
+ export type CustomFieldKind = typeof CUSTOM_FIELD_KINDS[number]
15
+
16
+ export function isCustomFieldKind(x: string): x is CustomFieldKind {
17
+ return (CUSTOM_FIELD_KINDS as readonly string[]).includes(x)
18
+ }
19
+
20
+ export const CURRENCY_OPTIONS_URL = '/api/currencies/currencies/options'
@@ -0,0 +1,36 @@
1
+ export type CustomFieldOptionInput =
2
+ | string
3
+ | {
4
+ value?: string | null
5
+ label?: string | null
6
+ }
7
+ | null
8
+ | undefined
9
+
10
+ export type CustomFieldOptionDto = {
11
+ value: string
12
+ label: string
13
+ }
14
+
15
+ export function normalizeCustomFieldOption(option: CustomFieldOptionInput): CustomFieldOptionDto | null {
16
+ if (typeof option === 'string') {
17
+ const value = option.trim()
18
+ if (!value) return null
19
+ return { value, label: value }
20
+ }
21
+ if (!option || typeof option !== 'object') return null
22
+ const rawValue = typeof option.value === 'string' ? option.value.trim() : ''
23
+ if (!rawValue) return null
24
+ const rawLabel = typeof option.label === 'string' ? option.label.trim() : ''
25
+ return { value: rawValue, label: rawLabel || rawValue }
26
+ }
27
+
28
+ export function normalizeCustomFieldOptions(options: unknown): CustomFieldOptionDto[] {
29
+ if (!Array.isArray(options)) return []
30
+ const normalized: CustomFieldOptionDto[] = []
31
+ for (const entry of options) {
32
+ const item = normalizeCustomFieldOption(entry as CustomFieldOptionInput)
33
+ if (item) normalized.push(item)
34
+ }
35
+ return normalized
36
+ }
@@ -0,0 +1,118 @@
1
+ import { z } from 'zod'
2
+
3
+ // Supported rule types for custom fields validation
4
+ export const VALIDATION_RULES = [
5
+ 'required',
6
+ 'date',
7
+ 'integer',
8
+ 'float',
9
+ 'lt',
10
+ 'lte',
11
+ 'gt',
12
+ 'gte',
13
+ 'eq',
14
+ 'ne',
15
+ 'regex',
16
+ ] as const
17
+
18
+ export type ValidationRuleKind = typeof VALIDATION_RULES[number]
19
+
20
+ export const validationRuleSchema = z.discriminatedUnion('rule', [
21
+ z.object({ rule: z.literal('required'), message: z.string().min(1) }),
22
+ z.object({ rule: z.literal('date'), message: z.string().min(1) }),
23
+ z.object({ rule: z.literal('integer'), message: z.string().min(1) }),
24
+ z.object({ rule: z.literal('float'), message: z.string().min(1) }),
25
+ z.object({ rule: z.literal('lt'), param: z.number(), message: z.string().min(1) }),
26
+ z.object({ rule: z.literal('lte'), param: z.number(), message: z.string().min(1) }),
27
+ z.object({ rule: z.literal('gt'), param: z.number(), message: z.string().min(1) }),
28
+ z.object({ rule: z.literal('gte'), param: z.number(), message: z.string().min(1) }),
29
+ z.object({ rule: z.literal('eq'), param: z.any(), message: z.string().min(1) }),
30
+ z.object({ rule: z.literal('ne'), param: z.any(), message: z.string().min(1) }),
31
+ z.object({ rule: z.literal('regex'), param: z.string().min(1), message: z.string().min(1) }),
32
+ ])
33
+
34
+ export type ValidationRule = z.infer<typeof validationRuleSchema>
35
+
36
+ export const validationRulesArraySchema = z.array(validationRuleSchema).max(32)
37
+
38
+ export type CustomFieldDefLike = {
39
+ key: string
40
+ kind: string
41
+ configJson?: any
42
+ }
43
+
44
+ // Evaluate a single rule against a value
45
+ function evalRule(rule: ValidationRule, value: any, kind: string): string | null {
46
+ const isEmpty = (v: any) => v == null || (typeof v === 'string' && v.trim() === '') || (Array.isArray(v) && v.length === 0)
47
+
48
+ switch (rule.rule) {
49
+ case 'required':
50
+ return isEmpty(value) ? rule.message : null
51
+ case 'date': {
52
+ if (isEmpty(value)) return null
53
+ const d = new Date(String(value))
54
+ return isNaN(d.getTime()) ? rule.message : null
55
+ }
56
+ case 'integer': {
57
+ if (isEmpty(value)) return null
58
+ const n = Number(value)
59
+ return Number.isInteger(n) ? null : rule.message
60
+ }
61
+ case 'float': {
62
+ if (isEmpty(value)) return null
63
+ const n = Number(value)
64
+ return Number.isFinite(n) ? null : rule.message
65
+ }
66
+ case 'lt':
67
+ if (isEmpty(value)) return null
68
+ return Number(value) < (rule as any).param ? null : rule.message
69
+ case 'lte':
70
+ if (isEmpty(value)) return null
71
+ return Number(value) <= (rule as any).param ? null : rule.message
72
+ case 'gt':
73
+ if (isEmpty(value)) return null
74
+ return Number(value) > (rule as any).param ? null : rule.message
75
+ case 'gte':
76
+ if (isEmpty(value)) return null
77
+ return Number(value) >= (rule as any).param ? null : rule.message
78
+ case 'eq':
79
+ if (isEmpty(value)) return null
80
+ return value === (rule as any).param ? null : rule.message
81
+ case 'ne':
82
+ if (isEmpty(value)) return null
83
+ return value !== (rule as any).param ? null : rule.message
84
+ case 'regex':
85
+ if (isEmpty(value)) return null
86
+ try {
87
+ const re = new RegExp((rule as any).param)
88
+ return re.test(String(value)) ? null : rule.message
89
+ } catch {
90
+ // Invalid regex in definition: consider it failed safe and return message
91
+ return rule.message
92
+ }
93
+ default:
94
+ return null
95
+ }
96
+ }
97
+
98
+ export function validateValuesAgainstDefs(
99
+ values: Record<string, any>,
100
+ defs: CustomFieldDefLike[],
101
+ ): { ok: boolean; fieldErrors: Record<string, string> } {
102
+ const errors: Record<string, string> = {}
103
+ for (const def of defs) {
104
+ const cfg = def?.configJson || {}
105
+ const rules: ValidationRule[] = Array.isArray(cfg.validation) ? cfg.validation : []
106
+ if (rules.length === 0) continue
107
+ const value = values[def.key]
108
+ for (const r of rules) {
109
+ const msg = evalRule(r as any, value, def.kind)
110
+ if (msg) {
111
+ errors[`cf_${def.key}`] = msg
112
+ break
113
+ }
114
+ }
115
+ }
116
+ return { ok: Object.keys(errors).length === 0, fieldErrors: errors }
117
+ }
118
+