@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.
- package/build.mjs +101 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +7 -0
- package/dist/lib/api/crud.js +47 -0
- package/dist/lib/api/crud.js.map +7 -0
- package/dist/lib/api/scoped.js +140 -0
- package/dist/lib/api/scoped.js.map +7 -0
- package/dist/lib/auth/jwt.js +34 -0
- package/dist/lib/auth/jwt.js.map +7 -0
- package/dist/lib/auth/server.js +157 -0
- package/dist/lib/auth/server.js.map +7 -0
- package/dist/lib/boolean.js +22 -0
- package/dist/lib/boolean.js.map +7 -0
- package/dist/lib/bootstrap/appResolver.js +43 -0
- package/dist/lib/bootstrap/appResolver.js.map +7 -0
- package/dist/lib/bootstrap/dynamicLoader.js +108 -0
- package/dist/lib/bootstrap/dynamicLoader.js.map +7 -0
- package/dist/lib/bootstrap/factory.js +59 -0
- package/dist/lib/bootstrap/factory.js.map +7 -0
- package/dist/lib/bootstrap/index.js +11 -0
- package/dist/lib/bootstrap/index.js.map +7 -0
- package/dist/lib/bootstrap/types.js +1 -0
- package/dist/lib/bootstrap/types.js.map +7 -0
- package/dist/lib/cache/segments.js +36 -0
- package/dist/lib/cache/segments.js.map +7 -0
- package/dist/lib/cli/progress.js +46 -0
- package/dist/lib/cli/progress.js.map +7 -0
- package/dist/lib/commands/command-bus.js +285 -0
- package/dist/lib/commands/command-bus.js.map +7 -0
- package/dist/lib/commands/customFieldSnapshots.js +66 -0
- package/dist/lib/commands/customFieldSnapshots.js.map +7 -0
- package/dist/lib/commands/helpers.js +98 -0
- package/dist/lib/commands/helpers.js.map +7 -0
- package/dist/lib/commands/index.js +8 -0
- package/dist/lib/commands/index.js.map +7 -0
- package/dist/lib/commands/operationMetadata.js +32 -0
- package/dist/lib/commands/operationMetadata.js.map +7 -0
- package/dist/lib/commands/registry.js +43 -0
- package/dist/lib/commands/registry.js.map +7 -0
- package/dist/lib/commands/scope.js +44 -0
- package/dist/lib/commands/scope.js.map +7 -0
- package/dist/lib/commands/types.js +8 -0
- package/dist/lib/commands/types.js.map +7 -0
- package/dist/lib/crud/cache-stats.js +98 -0
- package/dist/lib/crud/cache-stats.js.map +7 -0
- package/dist/lib/crud/cache.js +175 -0
- package/dist/lib/crud/cache.js.map +7 -0
- package/dist/lib/crud/custom-fields-client.js +52 -0
- package/dist/lib/crud/custom-fields-client.js.map +7 -0
- package/dist/lib/crud/custom-fields.js +467 -0
- package/dist/lib/crud/custom-fields.js.map +7 -0
- package/dist/lib/crud/errors.js +24 -0
- package/dist/lib/crud/errors.js.map +7 -0
- package/dist/lib/crud/exporters.js +154 -0
- package/dist/lib/crud/exporters.js.map +7 -0
- package/dist/lib/crud/factory.js +1311 -0
- package/dist/lib/crud/factory.js.map +7 -0
- package/dist/lib/crud/types.js +1 -0
- package/dist/lib/crud/types.js.map +7 -0
- package/dist/lib/custom-fields/normalize.js +36 -0
- package/dist/lib/custom-fields/normalize.js.map +7 -0
- package/dist/lib/data/engine.js +396 -0
- package/dist/lib/data/engine.js.map +7 -0
- package/dist/lib/db/escapeLikePattern.js +5 -0
- package/dist/lib/db/escapeLikePattern.js.map +7 -0
- package/dist/lib/db/mikro.js +82 -0
- package/dist/lib/db/mikro.js.map +7 -0
- package/dist/lib/di/container.js +94 -0
- package/dist/lib/di/container.js.map +7 -0
- package/dist/lib/email/send.js +12 -0
- package/dist/lib/email/send.js.map +7 -0
- package/dist/lib/encryption/aes.js +58 -0
- package/dist/lib/encryption/aes.js.map +7 -0
- package/dist/lib/encryption/customFieldValues.js +49 -0
- package/dist/lib/encryption/customFieldValues.js.map +7 -0
- package/dist/lib/encryption/entityFields.js +26 -0
- package/dist/lib/encryption/entityFields.js.map +7 -0
- package/dist/lib/encryption/entityIds.js +80 -0
- package/dist/lib/encryption/entityIds.js.map +7 -0
- package/dist/lib/encryption/find.js +45 -0
- package/dist/lib/encryption/find.js.map +7 -0
- package/dist/lib/encryption/indexDoc.js +69 -0
- package/dist/lib/encryption/indexDoc.js.map +7 -0
- package/dist/lib/encryption/kms.js +282 -0
- package/dist/lib/encryption/kms.js.map +7 -0
- package/dist/lib/encryption/subscriber.js +330 -0
- package/dist/lib/encryption/subscriber.js.map +7 -0
- package/dist/lib/encryption/tenantDataEncryptionService.js +252 -0
- package/dist/lib/encryption/tenantDataEncryptionService.js.map +7 -0
- package/dist/lib/encryption/toggles.js +18 -0
- package/dist/lib/encryption/toggles.js.map +7 -0
- package/dist/lib/entities/naming.js +9 -0
- package/dist/lib/entities/naming.js.map +7 -0
- package/dist/lib/entities/system-entities.js +43 -0
- package/dist/lib/entities/system-entities.js.map +7 -0
- package/dist/lib/frontend/organizationEvents.js +41 -0
- package/dist/lib/frontend/organizationEvents.js.map +7 -0
- package/dist/lib/frontend/useOrganizationScope.js +32 -0
- package/dist/lib/frontend/useOrganizationScope.js.map +7 -0
- package/dist/lib/hotkeys/index.js +128 -0
- package/dist/lib/hotkeys/index.js.map +7 -0
- package/dist/lib/i18n/app-dictionaries.js +17 -0
- package/dist/lib/i18n/app-dictionaries.js.map +7 -0
- package/dist/lib/i18n/config.js +7 -0
- package/dist/lib/i18n/config.js.map +7 -0
- package/dist/lib/i18n/context.js +50 -0
- package/dist/lib/i18n/context.js.map +7 -0
- package/dist/lib/i18n/server.js +68 -0
- package/dist/lib/i18n/server.js.map +7 -0
- package/dist/lib/i18n/translate.js +45 -0
- package/dist/lib/i18n/translate.js.map +7 -0
- package/dist/lib/indexers/error-log.js +82 -0
- package/dist/lib/indexers/error-log.js.map +7 -0
- package/dist/lib/indexers/status-log.js +80 -0
- package/dist/lib/indexers/status-log.js.map +7 -0
- package/dist/lib/lib/auth/jwt.js +34 -0
- package/dist/lib/lib/auth/jwt.js.map +7 -0
- package/dist/lib/lib/auth/server.js +77 -0
- package/dist/lib/lib/auth/server.js.map +7 -0
- package/dist/lib/lib/email/send.js +12 -0
- package/dist/lib/lib/email/send.js.map +7 -0
- package/dist/lib/lib/i18n/config.js +7 -0
- package/dist/lib/lib/i18n/config.js.map +7 -0
- package/dist/lib/lib/i18n/context.js +31 -0
- package/dist/lib/lib/i18n/context.js.map +7 -0
- package/dist/lib/lib/utils.js +9 -0
- package/dist/lib/lib/utils.js.map +7 -0
- package/dist/lib/location/countries.js +68 -0
- package/dist/lib/location/countries.js.map +7 -0
- package/dist/lib/modules/index.js +6 -0
- package/dist/lib/modules/index.js.map +7 -0
- package/dist/lib/modules/registry.js +18 -0
- package/dist/lib/modules/registry.js.map +7 -0
- package/dist/lib/openapi/crud.js +137 -0
- package/dist/lib/openapi/crud.js.map +7 -0
- package/dist/lib/openapi/generator.js +1131 -0
- package/dist/lib/openapi/generator.js.map +7 -0
- package/dist/lib/openapi/index.js +10 -0
- package/dist/lib/openapi/index.js.map +7 -0
- package/dist/lib/openapi/sanitize.js +110 -0
- package/dist/lib/openapi/sanitize.js.map +7 -0
- package/dist/lib/openapi/types.js +1 -0
- package/dist/lib/openapi/types.js.map +7 -0
- package/dist/lib/profiler/index.js +258 -0
- package/dist/lib/profiler/index.js.map +7 -0
- package/dist/lib/query/engine.js +729 -0
- package/dist/lib/query/engine.js.map +7 -0
- package/dist/lib/query/join-utils.js +195 -0
- package/dist/lib/query/join-utils.js.map +7 -0
- package/dist/lib/query/types.js +9 -0
- package/dist/lib/query/types.js.map +7 -0
- package/dist/lib/search/config.js +32 -0
- package/dist/lib/search/config.js.map +7 -0
- package/dist/lib/search/tokenize.js +34 -0
- package/dist/lib/search/tokenize.js.map +7 -0
- package/dist/lib/slugify.js +24 -0
- package/dist/lib/slugify.js.map +7 -0
- package/dist/lib/testing/bootstrap.js +51 -0
- package/dist/lib/testing/bootstrap.js.map +7 -0
- package/dist/lib/testing/index.js +17 -0
- package/dist/lib/testing/index.js.map +7 -0
- package/dist/lib/testing/renderWithProviders.js +15 -0
- package/dist/lib/testing/renderWithProviders.js.map +7 -0
- package/dist/lib/url.js +12 -0
- package/dist/lib/url.js.map +7 -0
- package/dist/lib/utils.js +13 -0
- package/dist/lib/utils.js.map +7 -0
- package/dist/lib/version.js +7 -0
- package/dist/lib/version.js.map +7 -0
- package/dist/modules/dashboard/widgets.js +1 -0
- package/dist/modules/dashboard/widgets.js.map +7 -0
- package/dist/modules/dsl.js +30 -0
- package/dist/modules/dsl.js.map +7 -0
- package/dist/modules/entities/kinds.js +22 -0
- package/dist/modules/entities/kinds.js.map +7 -0
- package/dist/modules/entities/options.js +26 -0
- package/dist/modules/entities/options.js.map +7 -0
- package/dist/modules/entities/validation.js +102 -0
- package/dist/modules/entities/validation.js.map +7 -0
- package/dist/modules/entities/validators.js +88 -0
- package/dist/modules/entities/validators.js.map +7 -0
- package/dist/modules/entities.js +1 -0
- package/dist/modules/entities.js.map +7 -0
- package/dist/modules/navigation/sidebarPreferences.js +50 -0
- package/dist/modules/navigation/sidebarPreferences.js.map +7 -0
- package/dist/modules/perspectives/types.js +1 -0
- package/dist/modules/perspectives/types.js.map +7 -0
- package/dist/modules/registry.js +96 -0
- package/dist/modules/registry.js.map +7 -0
- package/dist/modules/search.js +15 -0
- package/dist/modules/search.js.map +7 -0
- package/dist/modules/vector.js +1 -0
- package/dist/modules/vector.js.map +7 -0
- package/dist/modules/widgets/injection-loader.js +180 -0
- package/dist/modules/widgets/injection-loader.js.map +7 -0
- package/dist/modules/widgets/injection.js +1 -0
- package/dist/modules/widgets/injection.js.map +7 -0
- package/dist/security/features.js +23 -0
- package/dist/security/features.js.map +7 -0
- package/dist/types/pg.d.js +1 -0
- package/dist/types/pg.d.js.map +7 -0
- package/dist/types/react-email.d.js +1 -0
- package/dist/types/react-email.d.js.map +7 -0
- package/dist/types/resend.d.js +1 -0
- package/dist/types/resend.d.js.map +7 -0
- package/jest.config.cjs +22 -0
- package/package.json +88 -0
- package/src/index.ts +0 -0
- package/src/lib/api/__tests__/scoped.test.ts +38 -0
- package/src/lib/api/crud.ts +59 -0
- package/src/lib/api/scoped.ts +239 -0
- package/src/lib/auth/jwt.ts +39 -0
- package/src/lib/auth/server.ts +199 -0
- package/src/lib/boolean.ts +17 -0
- package/src/lib/bootstrap/appResolver.ts +85 -0
- package/src/lib/bootstrap/dynamicLoader.ts +177 -0
- package/src/lib/bootstrap/factory.ts +108 -0
- package/src/lib/bootstrap/index.ts +23 -0
- package/src/lib/bootstrap/types.ts +31 -0
- package/src/lib/cache/segments.ts +56 -0
- package/src/lib/cli/progress.ts +55 -0
- package/src/lib/commands/__tests__/command-bus.test.ts +84 -0
- package/src/lib/commands/__tests__/helpers.test.ts +42 -0
- package/src/lib/commands/command-bus.ts +349 -0
- package/src/lib/commands/customFieldSnapshots.ts +86 -0
- package/src/lib/commands/helpers.ts +143 -0
- package/src/lib/commands/index.ts +4 -0
- package/src/lib/commands/operationMetadata.ts +40 -0
- package/src/lib/commands/registry.ts +46 -0
- package/src/lib/commands/scope.ts +59 -0
- package/src/lib/commands/types.ts +63 -0
- package/src/lib/crud/__tests__/crud-factory.test.ts +333 -0
- package/src/lib/crud/__tests__/custom-fields.test.ts +150 -0
- package/src/lib/crud/cache-stats.ts +127 -0
- package/src/lib/crud/cache.ts +205 -0
- package/src/lib/crud/custom-fields-client.ts +54 -0
- package/src/lib/crud/custom-fields.ts +607 -0
- package/src/lib/crud/errors.ts +23 -0
- package/src/lib/crud/exporters.ts +188 -0
- package/src/lib/crud/factory.ts +1622 -0
- package/src/lib/crud/types.ts +29 -0
- package/src/lib/custom-fields/normalize.ts +45 -0
- package/src/lib/data/engine.ts +562 -0
- package/src/lib/db/escapeLikePattern.ts +2 -0
- package/src/lib/db/mikro.ts +100 -0
- package/src/lib/di/container.ts +105 -0
- package/src/lib/email/send.ts +18 -0
- package/src/lib/encryption/__tests__/customFieldValues.test.ts +63 -0
- package/src/lib/encryption/__tests__/indexDoc.test.ts +115 -0
- package/src/lib/encryption/aes.ts +64 -0
- package/src/lib/encryption/customFieldValues.ts +67 -0
- package/src/lib/encryption/entityFields.ts +39 -0
- package/src/lib/encryption/entityIds.ts +107 -0
- package/src/lib/encryption/find.ts +81 -0
- package/src/lib/encryption/indexDoc.ts +104 -0
- package/src/lib/encryption/kms.ts +337 -0
- package/src/lib/encryption/subscriber.ts +416 -0
- package/src/lib/encryption/tenantDataEncryptionService.ts +313 -0
- package/src/lib/encryption/toggles.ts +15 -0
- package/src/lib/entities/naming.ts +6 -0
- package/src/lib/entities/system-entities.ts +43 -0
- package/src/lib/frontend/organizationEvents.ts +55 -0
- package/src/lib/frontend/useOrganizationScope.ts +30 -0
- package/src/lib/hotkeys/index.ts +168 -0
- package/src/lib/i18n/app-dictionaries.ts +18 -0
- package/src/lib/i18n/config.ts +4 -0
- package/src/lib/i18n/context.tsx +66 -0
- package/src/lib/i18n/server.ts +74 -0
- package/src/lib/i18n/translate.ts +54 -0
- package/src/lib/indexers/error-log.ts +106 -0
- package/src/lib/indexers/status-log.ts +119 -0
- package/src/lib/lib/auth/jwt.ts +39 -0
- package/src/lib/lib/auth/server.ts +94 -0
- package/src/lib/lib/email/send.ts +18 -0
- package/src/lib/lib/i18n/config.ts +4 -0
- package/src/lib/lib/i18n/context.tsx +38 -0
- package/src/lib/lib/utils.ts +6 -0
- package/src/lib/location/countries.ts +97 -0
- package/src/lib/modules/index.ts +1 -0
- package/src/lib/modules/registry.ts +18 -0
- package/src/lib/openapi/crud.ts +218 -0
- package/src/lib/openapi/generator.ts +1311 -0
- package/src/lib/openapi/index.ts +4 -0
- package/src/lib/openapi/sanitize.ts +137 -0
- package/src/lib/openapi/types.ts +79 -0
- package/src/lib/profiler/index.ts +371 -0
- package/src/lib/query/__tests__/engine.test.ts +274 -0
- package/src/lib/query/engine.ts +837 -0
- package/src/lib/query/join-utils.ts +238 -0
- package/src/lib/query/types.ts +121 -0
- package/src/lib/search/config.ts +49 -0
- package/src/lib/search/tokenize.ts +45 -0
- package/src/lib/slugify.ts +28 -0
- package/src/lib/testing/bootstrap.ts +124 -0
- package/src/lib/testing/index.ts +15 -0
- package/src/lib/testing/renderWithProviders.tsx +31 -0
- package/src/lib/url.ts +12 -0
- package/src/lib/utils.ts +17 -0
- package/src/lib/version.ts +5 -0
- package/src/modules/__tests__/dsl.test.ts +35 -0
- package/src/modules/__tests__/registry.test.ts +300 -0
- package/src/modules/dashboard/widgets.ts +57 -0
- package/src/modules/dsl.ts +32 -0
- package/src/modules/entities/__tests__/validation.test.ts +52 -0
- package/src/modules/entities/kinds.ts +20 -0
- package/src/modules/entities/options.ts +36 -0
- package/src/modules/entities/validation.ts +118 -0
- package/src/modules/entities/validators.ts +93 -0
- package/src/modules/entities.ts +102 -0
- package/src/modules/navigation/sidebarPreferences.ts +62 -0
- package/src/modules/perspectives/types.ts +40 -0
- package/src/modules/registry.ts +249 -0
- package/src/modules/search.ts +325 -0
- package/src/modules/vector.ts +122 -0
- package/src/modules/widgets/__tests__/injection.test.ts +48 -0
- package/src/modules/widgets/injection-loader.ts +235 -0
- package/src/modules/widgets/injection.ts +120 -0
- package/src/security/features.ts +22 -0
- package/src/types/pg.d.ts +2 -0
- package/src/types/react-email.d.ts +2 -0
- package/src/types/resend.d.ts +2 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +6 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'
|
|
2
|
+
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
3
|
+
import { splitCustomFieldPayload } from '@open-mercato/shared/lib/crud/custom-fields'
|
|
4
|
+
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
5
|
+
import type { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
export type ScopedContext = (CommandRuntimeContext | CrudCtx) & {
|
|
8
|
+
auth: { tenantId?: string | null; orgId?: string | null } | null
|
|
9
|
+
selectedOrganizationId?: string | null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type TranslateFn = (key: string, fallback?: string) => string
|
|
13
|
+
|
|
14
|
+
export type ScopedMessage = {
|
|
15
|
+
key: string
|
|
16
|
+
fallback: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ScopedPayloadMessages = {
|
|
20
|
+
tenantRequired?: ScopedMessage
|
|
21
|
+
organizationRequired?: ScopedMessage
|
|
22
|
+
idRequired?: ScopedMessage
|
|
23
|
+
tenantForbidden?: ScopedMessage
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ScopedPayloadOptions = {
|
|
27
|
+
requireOrganization?: boolean
|
|
28
|
+
messages?: ScopedPayloadMessages
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DEFAULT_MESSAGES: Required<ScopedPayloadMessages> = {
|
|
32
|
+
tenantRequired: { key: 'errors.tenant_required', fallback: 'Tenant context is required.' },
|
|
33
|
+
organizationRequired: { key: 'errors.organization_required', fallback: 'Organization context is required.' },
|
|
34
|
+
idRequired: { key: 'errors.id_required', fallback: 'Record identifier is required.' },
|
|
35
|
+
tenantForbidden: { key: 'errors.tenant_forbidden', fallback: 'You are not allowed to target this tenant.' },
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveMessage(messages: ScopedPayloadMessages | undefined, key: keyof ScopedPayloadMessages): ScopedMessage {
|
|
39
|
+
const override = messages?.[key]
|
|
40
|
+
if (override && typeof override.key === 'string' && override.key.length > 0) {
|
|
41
|
+
return {
|
|
42
|
+
key: override.key,
|
|
43
|
+
fallback: override.fallback ?? DEFAULT_MESSAGES[key]!.fallback,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return DEFAULT_MESSAGES[key]!
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function withScopedPayload<T extends Record<string, unknown>>(
|
|
50
|
+
payload: T | null | undefined,
|
|
51
|
+
ctx: ScopedContext,
|
|
52
|
+
translate: TranslateFn,
|
|
53
|
+
options: ScopedPayloadOptions = {}
|
|
54
|
+
): T & { tenantId: string; organizationId?: string } {
|
|
55
|
+
const requireOrganization = options.requireOrganization !== false
|
|
56
|
+
const hasGlobalOrgAccess = ctx.organizationScope?.allowedIds === null
|
|
57
|
+
const source = payload ? { ...payload } : {}
|
|
58
|
+
const tenantId = (source as { tenantId?: string })?.tenantId ?? ctx.auth?.tenantId ?? null
|
|
59
|
+
if (!tenantId) {
|
|
60
|
+
const msg = resolveMessage(options.messages, 'tenantRequired')
|
|
61
|
+
throw new CrudHttpError(400, { error: translate(msg.key, msg.fallback) })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const resolvedOrg =
|
|
65
|
+
(source as { organizationId?: string })?.organizationId ??
|
|
66
|
+
ctx.selectedOrganizationId ??
|
|
67
|
+
ctx.auth?.orgId ??
|
|
68
|
+
null
|
|
69
|
+
|
|
70
|
+
if (requireOrganization && !hasGlobalOrgAccess && !resolvedOrg) {
|
|
71
|
+
const msg = resolveMessage(options.messages, 'organizationRequired')
|
|
72
|
+
throw new CrudHttpError(400, { error: translate(msg.key, msg.fallback) })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const scoped = {
|
|
76
|
+
...source,
|
|
77
|
+
tenantId,
|
|
78
|
+
} as T & { tenantId: string; organizationId?: string }
|
|
79
|
+
|
|
80
|
+
if (resolvedOrg) scoped.organizationId = resolvedOrg
|
|
81
|
+
|
|
82
|
+
return scoped
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function parseScopedCommandInput<TSchema extends z.ZodTypeAny>(
|
|
86
|
+
schema: TSchema,
|
|
87
|
+
payload: unknown,
|
|
88
|
+
ctx: ScopedContext,
|
|
89
|
+
translate: TranslateFn,
|
|
90
|
+
options: ScopedPayloadOptions = {}
|
|
91
|
+
): z.infer<TSchema> & { customFields?: Record<string, unknown> } {
|
|
92
|
+
const scoped = withScopedPayload(
|
|
93
|
+
(payload && typeof payload === 'object' ? payload : {}) as Record<string, unknown>,
|
|
94
|
+
ctx,
|
|
95
|
+
translate,
|
|
96
|
+
options
|
|
97
|
+
)
|
|
98
|
+
const actorTenantId = normalizeTenant(ctx.auth?.tenantId)
|
|
99
|
+
const requestedTenantId = normalizeTenant(scoped.tenantId)
|
|
100
|
+
const isSuperAdmin = authIsSuperAdmin(ctx.auth)
|
|
101
|
+
if (!isSuperAdmin) {
|
|
102
|
+
if (actorTenantId) {
|
|
103
|
+
if (!requestedTenantId || requestedTenantId !== actorTenantId) {
|
|
104
|
+
const msg = resolveMessage(options.messages, 'tenantForbidden')
|
|
105
|
+
throw new CrudHttpError(403, { error: translate(msg.key, msg.fallback) })
|
|
106
|
+
}
|
|
107
|
+
} else if (requestedTenantId) {
|
|
108
|
+
const msg = resolveMessage(options.messages, 'tenantForbidden')
|
|
109
|
+
throw new CrudHttpError(403, { error: translate(msg.key, msg.fallback) })
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const { base, custom } = splitCustomFieldPayload(scoped)
|
|
113
|
+
const hasCustomFields = custom && Object.keys(custom).length > 0
|
|
114
|
+
const candidates: Array<Record<string, unknown>> = hasCustomFields
|
|
115
|
+
? [base, { ...base, customFields: custom }]
|
|
116
|
+
: [base]
|
|
117
|
+
|
|
118
|
+
let parsed: z.infer<TSchema> | undefined
|
|
119
|
+
let lastError: unknown
|
|
120
|
+
for (const candidate of candidates) {
|
|
121
|
+
try {
|
|
122
|
+
parsed = schema.parse(candidate) as z.infer<TSchema>
|
|
123
|
+
break
|
|
124
|
+
} catch (err) {
|
|
125
|
+
lastError = err
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (!parsed) {
|
|
129
|
+
if (lastError instanceof Error) throw lastError
|
|
130
|
+
throw new CrudHttpError(400, { error: translate('errors.invalid_input', 'Invalid input') })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const parsedWithCustom = hasCustomFields
|
|
134
|
+
? Object.assign({}, parsed, { customFields: custom })
|
|
135
|
+
: parsed
|
|
136
|
+
|
|
137
|
+
return parsedWithCustom as z.infer<TSchema> & { customFields?: Record<string, unknown> }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function normalizeTenant(candidate: unknown): string | null {
|
|
141
|
+
if (typeof candidate === 'string' && candidate.trim().length > 0) return candidate.trim()
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function authIsSuperAdmin(auth: ScopedContext['auth']): boolean {
|
|
146
|
+
if (!auth) return false
|
|
147
|
+
return (auth as Record<string, unknown>).isSuperAdmin === true
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function requireRecordId(
|
|
151
|
+
candidate: unknown,
|
|
152
|
+
ctx: ScopedContext,
|
|
153
|
+
translate: TranslateFn,
|
|
154
|
+
options: ScopedPayloadOptions = {}
|
|
155
|
+
): string {
|
|
156
|
+
const fieldName = 'id'
|
|
157
|
+
const id =
|
|
158
|
+
typeof candidate === 'string'
|
|
159
|
+
? candidate.trim()
|
|
160
|
+
: candidate && typeof candidate === 'object'
|
|
161
|
+
? typeof (candidate as Record<string, unknown>)[fieldName] === 'string'
|
|
162
|
+
? String((candidate as Record<string, unknown>)[fieldName])
|
|
163
|
+
: null
|
|
164
|
+
: null
|
|
165
|
+
if (id && id.length > 0) return id
|
|
166
|
+
const msg = resolveMessage(options.messages, 'idRequired')
|
|
167
|
+
throw new CrudHttpError(400, { error: translate(msg.key, msg.fallback) })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function resolveCrudRecordId(
|
|
171
|
+
parsed: unknown,
|
|
172
|
+
ctx: ScopedContext,
|
|
173
|
+
translate: TranslateFn,
|
|
174
|
+
options: ScopedPayloadOptions & { fieldName?: string; queryParam?: string } = {}
|
|
175
|
+
): string {
|
|
176
|
+
const fieldName = options.fieldName ?? 'id'
|
|
177
|
+
const queryParam = options.queryParam ?? fieldName
|
|
178
|
+
|
|
179
|
+
const tryRequire = (value: unknown): string | null => {
|
|
180
|
+
try {
|
|
181
|
+
return requireRecordId(value, ctx, translate, options)
|
|
182
|
+
} catch {
|
|
183
|
+
return null
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (parsed && typeof parsed === 'object') {
|
|
188
|
+
const body = (parsed as Record<string, unknown>).body
|
|
189
|
+
const fromBody = body && typeof body === 'object' ? tryRequire(body) : null
|
|
190
|
+
if (fromBody) return fromBody
|
|
191
|
+
|
|
192
|
+
const fallback = tryRequire(parsed)
|
|
193
|
+
if (fallback) return fallback
|
|
194
|
+
|
|
195
|
+
const query = (parsed as Record<string, unknown>).query
|
|
196
|
+
if (query && typeof query === 'object') {
|
|
197
|
+
const candidate = (query as Record<string, unknown>)[queryParam]
|
|
198
|
+
if (typeof candidate === 'string' && candidate.trim().length > 0) return candidate.trim()
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (ctx.request instanceof Request) {
|
|
203
|
+
const value = new URL(ctx.request.url).searchParams.get(queryParam)
|
|
204
|
+
if (value && value.trim().length > 0) return value.trim()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const msg = resolveMessage(options.messages, 'idRequired')
|
|
208
|
+
throw new CrudHttpError(400, { error: translate(msg.key, msg.fallback) })
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function createScopedApiHelpers(baseOptions?: ScopedPayloadOptions) {
|
|
212
|
+
return {
|
|
213
|
+
withScopedPayload: <T extends Record<string, unknown>>(
|
|
214
|
+
payload: T | null | undefined,
|
|
215
|
+
ctx: ScopedContext,
|
|
216
|
+
translate: TranslateFn,
|
|
217
|
+
options: ScopedPayloadOptions = {}
|
|
218
|
+
) => withScopedPayload(payload, ctx, translate, { ...baseOptions, ...options }),
|
|
219
|
+
parseScopedCommandInput: <TSchema extends z.ZodTypeAny>(
|
|
220
|
+
schema: TSchema,
|
|
221
|
+
payload: unknown,
|
|
222
|
+
ctx: ScopedContext,
|
|
223
|
+
translate: TranslateFn,
|
|
224
|
+
options: ScopedPayloadOptions = {}
|
|
225
|
+
) => parseScopedCommandInput(schema, payload, ctx, translate, { ...baseOptions, ...options }),
|
|
226
|
+
requireRecordId: (
|
|
227
|
+
candidate: unknown,
|
|
228
|
+
ctx: ScopedContext,
|
|
229
|
+
translate: TranslateFn,
|
|
230
|
+
options: ScopedPayloadOptions = {}
|
|
231
|
+
) => requireRecordId(candidate, ctx, translate, { ...baseOptions, ...options }),
|
|
232
|
+
resolveCrudRecordId: (
|
|
233
|
+
parsed: unknown,
|
|
234
|
+
ctx: ScopedContext,
|
|
235
|
+
translate: TranslateFn,
|
|
236
|
+
options: ScopedPayloadOptions & { fieldName?: string; queryParam?: string } = {}
|
|
237
|
+
) => resolveCrudRecordId(parsed, ctx, translate, { ...baseOptions, ...options }),
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
function base64url(input: Buffer | string) {
|
|
4
|
+
return (typeof input === 'string' ? Buffer.from(input) : input)
|
|
5
|
+
.toString('base64')
|
|
6
|
+
.replace(/=/g, '')
|
|
7
|
+
.replace(/\+/g, '-')
|
|
8
|
+
.replace(/\//g, '_')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type JwtPayload = Record<string, any>
|
|
12
|
+
|
|
13
|
+
export function signJwt(payload: JwtPayload, secret = process.env.JWT_SECRET!, expiresInSec = 60 * 60 * 8) {
|
|
14
|
+
if (!secret) throw new Error('JWT_SECRET is not set')
|
|
15
|
+
const header = { alg: 'HS256', typ: 'JWT' }
|
|
16
|
+
const now = Math.floor(Date.now() / 1000)
|
|
17
|
+
const body = { iat: now, exp: now + expiresInSec, ...payload }
|
|
18
|
+
const encHeader = base64url(JSON.stringify(header))
|
|
19
|
+
const encBody = base64url(JSON.stringify(body))
|
|
20
|
+
const data = `${encHeader}.${encBody}`
|
|
21
|
+
const sig = crypto.createHmac('sha256', secret).update(data).digest()
|
|
22
|
+
const encSig = base64url(sig)
|
|
23
|
+
return `${data}.${encSig}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function verifyJwt(token: string, secret = process.env.JWT_SECRET!) {
|
|
27
|
+
if (!secret) throw new Error('JWT_SECRET is not set')
|
|
28
|
+
const parts = token.split('.')
|
|
29
|
+
if (parts.length !== 3) return null
|
|
30
|
+
const [h, p, s] = parts
|
|
31
|
+
const data = `${h}.${p}`
|
|
32
|
+
const expected = base64url(crypto.createHmac('sha256', secret).update(data).digest())
|
|
33
|
+
if (!crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected))) return null
|
|
34
|
+
const payload = JSON.parse(Buffer.from(p, 'base64').toString('utf8'))
|
|
35
|
+
const now = Math.floor(Date.now() / 1000)
|
|
36
|
+
if (payload.exp && now > payload.exp) return null
|
|
37
|
+
return payload
|
|
38
|
+
}
|
|
39
|
+
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { cookies } from 'next/headers'
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
|
+
import { verifyJwt } from './jwt'
|
|
4
|
+
|
|
5
|
+
const TENANT_COOKIE_NAME = 'om_selected_tenant'
|
|
6
|
+
const ORGANIZATION_COOKIE_NAME = 'om_selected_org'
|
|
7
|
+
const ALL_ORGANIZATIONS_COOKIE_VALUE = '__all__'
|
|
8
|
+
const SUPERADMIN_ROLE = 'superadmin'
|
|
9
|
+
|
|
10
|
+
export type AuthContext = {
|
|
11
|
+
sub: string
|
|
12
|
+
tenantId: string | null
|
|
13
|
+
orgId: string | null
|
|
14
|
+
email?: string
|
|
15
|
+
roles?: string[]
|
|
16
|
+
isApiKey?: boolean
|
|
17
|
+
keyId?: string
|
|
18
|
+
keyName?: string
|
|
19
|
+
[k: string]: unknown
|
|
20
|
+
} | null
|
|
21
|
+
|
|
22
|
+
type CookieOverride = { applied: boolean; value: string | null }
|
|
23
|
+
|
|
24
|
+
function decodeCookieValue(raw: string | undefined): string | null {
|
|
25
|
+
if (raw === undefined) return null
|
|
26
|
+
try {
|
|
27
|
+
const decoded = decodeURIComponent(raw)
|
|
28
|
+
return decoded ?? null
|
|
29
|
+
} catch {
|
|
30
|
+
return raw ?? null
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readCookieFromHeader(header: string | null | undefined, name: string): string | undefined {
|
|
35
|
+
if (!header) return undefined
|
|
36
|
+
const parts = header.split(';')
|
|
37
|
+
for (const part of parts) {
|
|
38
|
+
const trimmed = part.trim()
|
|
39
|
+
if (trimmed.startsWith(`${name}=`)) {
|
|
40
|
+
return trimmed.slice(name.length + 1)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveTenantOverride(raw: string | undefined): CookieOverride {
|
|
47
|
+
if (raw === undefined) return { applied: false, value: null }
|
|
48
|
+
const decoded = decodeCookieValue(raw)
|
|
49
|
+
if (!decoded) return { applied: true, value: null }
|
|
50
|
+
const trimmed = decoded.trim()
|
|
51
|
+
if (!trimmed) return { applied: true, value: null }
|
|
52
|
+
return { applied: true, value: trimmed }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveOrganizationOverride(raw: string | undefined): CookieOverride {
|
|
56
|
+
if (raw === undefined) return { applied: false, value: null }
|
|
57
|
+
const decoded = decodeCookieValue(raw)
|
|
58
|
+
if (!decoded || decoded === ALL_ORGANIZATIONS_COOKIE_VALUE) {
|
|
59
|
+
return { applied: true, value: null }
|
|
60
|
+
}
|
|
61
|
+
const trimmed = decoded.trim()
|
|
62
|
+
if (!trimmed || trimmed === ALL_ORGANIZATIONS_COOKIE_VALUE) {
|
|
63
|
+
return { applied: true, value: null }
|
|
64
|
+
}
|
|
65
|
+
return { applied: true, value: trimmed }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isSuperAdminAuth(auth: AuthContext | null | undefined): boolean {
|
|
69
|
+
if (!auth) return false
|
|
70
|
+
if ((auth as Record<string, unknown>).isSuperAdmin === true) return true
|
|
71
|
+
const roles = Array.isArray(auth?.roles) ? auth.roles : []
|
|
72
|
+
return roles.some((role) => typeof role === 'string' && role.trim().toLowerCase() === SUPERADMIN_ROLE)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function applySuperAdminScope(
|
|
76
|
+
auth: AuthContext,
|
|
77
|
+
tenantCookie: string | undefined,
|
|
78
|
+
orgCookie: string | undefined
|
|
79
|
+
): AuthContext {
|
|
80
|
+
if (!auth || !isSuperAdminAuth(auth)) return auth
|
|
81
|
+
|
|
82
|
+
const tenantOverride = resolveTenantOverride(tenantCookie)
|
|
83
|
+
const orgOverride = resolveOrganizationOverride(orgCookie)
|
|
84
|
+
if (!tenantOverride.applied && !orgOverride.applied) return auth
|
|
85
|
+
|
|
86
|
+
type MutableAuthContext = Exclude<AuthContext, null> & {
|
|
87
|
+
actorTenantId?: string | null
|
|
88
|
+
actorOrgId?: string | null
|
|
89
|
+
}
|
|
90
|
+
const baseAuth = auth as Exclude<AuthContext, null>
|
|
91
|
+
const next: MutableAuthContext = { ...baseAuth }
|
|
92
|
+
if (tenantOverride.applied) {
|
|
93
|
+
if (!('actorTenantId' in next)) next.actorTenantId = auth?.tenantId ?? null
|
|
94
|
+
next.tenantId = tenantOverride.value
|
|
95
|
+
}
|
|
96
|
+
if (orgOverride.applied) {
|
|
97
|
+
if (!('actorOrgId' in next)) next.actorOrgId = auth?.orgId ?? null
|
|
98
|
+
next.orgId = orgOverride.value
|
|
99
|
+
}
|
|
100
|
+
next.isSuperAdmin = true
|
|
101
|
+
const existingRoles = Array.isArray(next.roles) ? next.roles : []
|
|
102
|
+
if (!existingRoles.some((role) => typeof role === 'string' && role.trim().toLowerCase() === SUPERADMIN_ROLE)) {
|
|
103
|
+
next.roles = [...existingRoles, 'superadmin']
|
|
104
|
+
}
|
|
105
|
+
return next
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function resolveApiKeyAuth(secret: string): Promise<AuthContext> {
|
|
109
|
+
if (!secret) return null
|
|
110
|
+
try {
|
|
111
|
+
const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')
|
|
112
|
+
const container = await createRequestContainer()
|
|
113
|
+
const em = (container.resolve('em') as EntityManager)
|
|
114
|
+
const { findApiKeyBySecret } = await import('@open-mercato/core/modules/api_keys/services/apiKeyService')
|
|
115
|
+
const { Role } = await import('@open-mercato/core/modules/auth/data/entities')
|
|
116
|
+
|
|
117
|
+
const record = await findApiKeyBySecret(em, secret)
|
|
118
|
+
if (!record) return null
|
|
119
|
+
|
|
120
|
+
const roleIds = Array.isArray(record.rolesJson)
|
|
121
|
+
? record.rolesJson.filter((value): value is string => typeof value === 'string' && value.length > 0)
|
|
122
|
+
: []
|
|
123
|
+
const roles = roleIds.length
|
|
124
|
+
? await em.find(Role, { id: { $in: roleIds } })
|
|
125
|
+
: []
|
|
126
|
+
const roleNames = roles.map((role) => role.name).filter((name): name is string => typeof name === 'string' && name.length > 0)
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
record.lastUsedAt = new Date()
|
|
130
|
+
await em.persistAndFlush(record)
|
|
131
|
+
} catch {
|
|
132
|
+
// best-effort update; ignore write failures
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
sub: `api_key:${record.id}`,
|
|
137
|
+
tenantId: record.tenantId ?? null,
|
|
138
|
+
orgId: record.organizationId ?? null,
|
|
139
|
+
roles: roleNames,
|
|
140
|
+
isApiKey: true,
|
|
141
|
+
keyId: record.id,
|
|
142
|
+
keyName: record.name,
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function extractApiKey(req: Request): string | null {
|
|
150
|
+
const header = (req.headers.get('x-api-key') || '').trim()
|
|
151
|
+
if (header) return header
|
|
152
|
+
const authHeader = (req.headers.get('authorization') || '').trim()
|
|
153
|
+
if (authHeader.toLowerCase().startsWith('apikey ')) {
|
|
154
|
+
return authHeader.slice(7).trim()
|
|
155
|
+
}
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function getAuthFromCookies(): Promise<AuthContext> {
|
|
160
|
+
const cookieStore = await cookies()
|
|
161
|
+
const token = cookieStore.get('auth_token')?.value
|
|
162
|
+
if (!token) return null
|
|
163
|
+
try {
|
|
164
|
+
const payload = verifyJwt(token) as AuthContext
|
|
165
|
+
if (!payload) return null
|
|
166
|
+
const tenantCookie = cookieStore.get(TENANT_COOKIE_NAME)?.value
|
|
167
|
+
const orgCookie = cookieStore.get(ORGANIZATION_COOKIE_NAME)?.value
|
|
168
|
+
return applySuperAdminScope(payload, tenantCookie, orgCookie)
|
|
169
|
+
} catch {
|
|
170
|
+
return null
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function getAuthFromRequest(req: Request): Promise<AuthContext> {
|
|
175
|
+
const cookieHeader = req.headers.get('cookie') || ''
|
|
176
|
+
const tenantCookie = readCookieFromHeader(cookieHeader, TENANT_COOKIE_NAME)
|
|
177
|
+
const orgCookie = readCookieFromHeader(cookieHeader, ORGANIZATION_COOKIE_NAME)
|
|
178
|
+
const authHeader = (req.headers.get('authorization') || '').trim()
|
|
179
|
+
let token: string | undefined
|
|
180
|
+
if (authHeader.toLowerCase().startsWith('bearer ')) token = authHeader.slice(7).trim()
|
|
181
|
+
if (!token) {
|
|
182
|
+
const match = cookieHeader.match(/(?:^|;\s*)auth_token=([^;]+)/)
|
|
183
|
+
if (match) token = decodeURIComponent(match[1])
|
|
184
|
+
}
|
|
185
|
+
if (token) {
|
|
186
|
+
try {
|
|
187
|
+
const payload = verifyJwt(token) as AuthContext
|
|
188
|
+
if (payload) return applySuperAdminScope(payload, tenantCookie, orgCookie)
|
|
189
|
+
} catch {
|
|
190
|
+
// fall back to API key detection
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const apiKey = extractApiKey(req)
|
|
195
|
+
if (!apiKey) return null
|
|
196
|
+
const apiAuth = await resolveApiKeyAuth(apiKey)
|
|
197
|
+
if (!apiAuth) return null
|
|
198
|
+
return applySuperAdminScope(apiAuth, tenantCookie, orgCookie)
|
|
199
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const TRUE_VALUES = new Set(['1', 'true', 'yes', 'y', 'on', 'enable', 'enabled'])
|
|
2
|
+
export const FALSE_VALUES = new Set(['0', 'false', 'no', 'n', 'off', 'disable', 'disabled'])
|
|
3
|
+
|
|
4
|
+
export function parseBooleanToken(raw: string | null | undefined): boolean | null {
|
|
5
|
+
if (typeof raw !== 'string') return null
|
|
6
|
+
const trimmed = raw.trim()
|
|
7
|
+
if (!trimmed) return null
|
|
8
|
+
const normalized = trimmed.toLowerCase()
|
|
9
|
+
if (TRUE_VALUES.has(normalized)) return true
|
|
10
|
+
if (FALSE_VALUES.has(normalized)) return false
|
|
11
|
+
return null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function parseBooleanWithDefault(raw: string | null | undefined, fallback: boolean): boolean {
|
|
15
|
+
const parsed = parseBooleanToken(raw)
|
|
16
|
+
return parsed === null ? fallback : parsed
|
|
17
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
|
|
4
|
+
export interface AppRoot {
|
|
5
|
+
appDir: string
|
|
6
|
+
mercatoDir: string
|
|
7
|
+
generatedDir: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Find the Next.js app root by searching for next.config.ts/js/mjs.
|
|
12
|
+
*
|
|
13
|
+
* Starts from the given directory (defaults to cwd) and walks up the
|
|
14
|
+
* directory tree until it finds a Next.js config file with a .mercato/generated
|
|
15
|
+
* directory.
|
|
16
|
+
*
|
|
17
|
+
* @param startDir - Directory to start searching from (defaults to process.cwd())
|
|
18
|
+
* @returns The resolved app root paths, or null if not found
|
|
19
|
+
*/
|
|
20
|
+
export function findAppRoot(startDir: string = process.cwd()): AppRoot | null {
|
|
21
|
+
let current = startDir
|
|
22
|
+
|
|
23
|
+
while (current !== path.dirname(current)) {
|
|
24
|
+
const configTs = path.join(current, 'next.config.ts')
|
|
25
|
+
const configJs = path.join(current, 'next.config.js')
|
|
26
|
+
const configMjs = path.join(current, 'next.config.mjs')
|
|
27
|
+
|
|
28
|
+
if (fs.existsSync(configTs) || fs.existsSync(configJs) || fs.existsSync(configMjs)) {
|
|
29
|
+
const mercatoDir = path.join(current, '.mercato')
|
|
30
|
+
const generatedDir = path.join(mercatoDir, 'generated')
|
|
31
|
+
|
|
32
|
+
// Only return if .mercato/generated exists
|
|
33
|
+
if (fs.existsSync(generatedDir)) {
|
|
34
|
+
return { appDir: current, mercatoDir, generatedDir }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Found Next.js config but no .mercato/generated - return anyway for generate command
|
|
38
|
+
// The caller can decide whether to create the directory
|
|
39
|
+
return { appDir: current, mercatoDir, generatedDir }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
current = path.dirname(current)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Find all apps with .mercato directories in a monorepo.
|
|
50
|
+
*
|
|
51
|
+
* Scans the apps/ directory for Next.js apps with .mercato/generated directories.
|
|
52
|
+
*
|
|
53
|
+
* @param rootDir - The monorepo root directory
|
|
54
|
+
* @returns Array of app root paths
|
|
55
|
+
*/
|
|
56
|
+
export function findAllApps(rootDir: string): AppRoot[] {
|
|
57
|
+
const appsDir = path.join(rootDir, 'apps')
|
|
58
|
+
if (!fs.existsSync(appsDir)) return []
|
|
59
|
+
|
|
60
|
+
const apps: AppRoot[] = []
|
|
61
|
+
const entries = fs.readdirSync(appsDir, { withFileTypes: true })
|
|
62
|
+
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
if (!entry.isDirectory()) continue
|
|
65
|
+
|
|
66
|
+
const appDir = path.join(appsDir, entry.name)
|
|
67
|
+
|
|
68
|
+
// Check for Next.js config
|
|
69
|
+
const hasNextConfig =
|
|
70
|
+
fs.existsSync(path.join(appDir, 'next.config.ts')) ||
|
|
71
|
+
fs.existsSync(path.join(appDir, 'next.config.js')) ||
|
|
72
|
+
fs.existsSync(path.join(appDir, 'next.config.mjs'))
|
|
73
|
+
|
|
74
|
+
if (!hasNextConfig) continue
|
|
75
|
+
|
|
76
|
+
const mercatoDir = path.join(appDir, '.mercato')
|
|
77
|
+
const generatedDir = path.join(mercatoDir, 'generated')
|
|
78
|
+
|
|
79
|
+
if (fs.existsSync(generatedDir)) {
|
|
80
|
+
apps.push({ appDir, mercatoDir, generatedDir })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return apps
|
|
85
|
+
}
|