create-nextblock 0.16.4 → 0.17.1
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/CLAUDE.md +22 -0
- package/docker-template/.env.docker.example +69 -69
- package/docker-template/docker/db/init/99-jwt.sql +6 -6
- package/docker-template/docker/db/init/99-roles.sql +25 -25
- package/docker-template/docker/kong/kong.yml +112 -112
- package/docker-template/scripts/docker-setup.mjs +310 -310
- package/package.json +1 -1
- package/templates/nextblock-template/.browserslistrc +11 -11
- package/templates/nextblock-template/.swcrc +30 -30
- package/templates/nextblock-template/CLAUDE.md +26 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
- package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
- package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
- package/templates/nextblock-template/app/actions/consent.ts +57 -57
- package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
- package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
- package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
- package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
- package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
- package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
- package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
- package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +272 -205
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +10473 -8467
- package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
- package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
- package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
- package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
- package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
- package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
- package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
- package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +442 -441
- package/templates/nextblock-template/app/article/[slug]/page.utils.ts +4 -1
- package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
- package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
- package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
- package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
- package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
- package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +3 -3
- package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
- package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -136
- package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
- package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
- package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
- package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
- package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
- package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
- package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
- package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
- package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
- package/templates/nextblock-template/app/cms/pages/page.tsx +273 -273
- package/templates/nextblock-template/app/cms/posts/page.tsx +251 -251
- package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
- package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
- package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
- package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
- package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
- package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
- package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
- package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
- package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
- package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
- package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
- package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
- package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
- package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
- package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
- package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
- package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
- package/templates/nextblock-template/app/setup/layout.tsx +13 -13
- package/templates/nextblock-template/app/setup/page.tsx +111 -111
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
- package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
- package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
- package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
- package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
- package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
- package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
- package/templates/nextblock-template/components/Header.tsx +38 -38
- package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
- package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
- package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
- package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
- package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +11 -0
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
- package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/media/YouTubeFacade.tsx +105 -105
- package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -32
- package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
- package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -471
- package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
- package/templates/nextblock-template/context/language-rest-client.ts +32 -32
- package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
- package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
- package/templates/nextblock-template/docker/kong/kong.yml +112 -112
- package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +1 -1
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +402 -282
- package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +15 -8
- package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +23 -4
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
- package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +4 -1
- package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +8 -0
- package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +2 -2
- package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +95 -104
- package/templates/nextblock-template/lib/app-secrets.ts +39 -39
- package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
- package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
- package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
- package/templates/nextblock-template/lib/blocks/README.md +13 -13
- package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
- package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -135
- package/templates/nextblock-template/lib/cms/payments-reminder.ts +105 -105
- package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
- package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
- package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
- package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
- package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
- package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
- package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
- package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
- package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
- package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
- package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
- package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
- package/templates/nextblock-template/lib/email/branding.ts +76 -76
- package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
- package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
- package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
- package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +56 -54
- package/templates/nextblock-template/lib/media/youtube.ts +99 -99
- package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
- package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
- package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
- package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
- package/templates/nextblock-template/lib/privacy/types.ts +69 -69
- package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
- package/templates/nextblock-template/lib/promotions/server.ts +741 -741
- package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
- package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
- package/templates/nextblock-template/lib/seo/page-document.ts +4 -4
- package/templates/nextblock-template/lib/seo/redirect-store.ts +3 -2
- package/templates/nextblock-template/lib/setup/actions.ts +460 -460
- package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
- package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +15 -175
- package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
- package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
- package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
- package/templates/nextblock-template/lib/setup/types.ts +18 -18
- package/templates/nextblock-template/lib/storage/provider.ts +66 -66
- package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
- package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
- package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
- package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
- package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
- package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
- package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
- package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
- package/templates/nextblock-template/lib/zod-config.ts +5 -5
- package/templates/nextblock-template/next-env.d.ts +1 -0
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/public/images/cortex_post.webp +0 -0
- package/templates/nextblock-template/public/images/update_nextblock.webp +0 -0
- package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
- package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
- package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
- package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
- package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
- package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
- package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
- package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
- package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
- package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
- package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
- package/templates/nextblock-template/types/jsdom.d.ts +6 -6
|
@@ -1,255 +1,255 @@
|
|
|
1
|
-
import 'server-only';
|
|
2
|
-
|
|
3
|
-
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
4
|
-
import { getSsgSupabaseClient } from '@nextblock-cms/db/server';
|
|
5
|
-
import type { Database } from '@nextblock-cms/db';
|
|
6
|
-
import type { CustomBlockField } from '@nextblock-cms/utils';
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
getCustomBlockRelationTarget,
|
|
10
|
-
normalizeCustomBlockRelationValue,
|
|
11
|
-
} from './custom-block-relation-registry';
|
|
12
|
-
import { searchCustomBlockRelationRows, type CustomBlockRelationRow } from './custom-block-relations';
|
|
13
|
-
|
|
14
|
-
type CustomBlockRelationField = Extract<CustomBlockField, { type: 'db_relation' }>;
|
|
15
|
-
type RelationRecord = Record<string, unknown> | null;
|
|
16
|
-
|
|
17
|
-
export type ResolvedCustomBlockRelation =
|
|
18
|
-
| {
|
|
19
|
-
error?: string;
|
|
20
|
-
record: RelationRecord;
|
|
21
|
-
table: string;
|
|
22
|
-
value: string;
|
|
23
|
-
}
|
|
24
|
-
| Array<{
|
|
25
|
-
error?: string;
|
|
26
|
-
record: RelationRecord;
|
|
27
|
-
table: string;
|
|
28
|
-
value: string;
|
|
29
|
-
}>;
|
|
30
|
-
|
|
31
|
-
export type CustomBlockInstanceWithRelations = {
|
|
32
|
-
data?: Record<string, unknown> | null;
|
|
33
|
-
definition?: { fields?: CustomBlockField[] | null } | null;
|
|
34
|
-
fields?: CustomBlockField[] | null;
|
|
35
|
-
resolved_relations?: Record<string, ResolvedCustomBlockRelation>;
|
|
36
|
-
values?: Record<string, unknown> | null;
|
|
37
|
-
[key: string]: unknown;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export type ResolveBlockRelationsOptions = {
|
|
41
|
-
fields?: CustomBlockField[];
|
|
42
|
-
supabase?: SupabaseClient<Database>;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
type RelationLookup = {
|
|
46
|
-
field: CustomBlockRelationField;
|
|
47
|
-
instanceIndex: number;
|
|
48
|
-
rawValues: unknown[];
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
type RelationGroup = {
|
|
52
|
-
displayColumn: string;
|
|
53
|
-
field: CustomBlockRelationField;
|
|
54
|
-
lookups: RelationLookup[];
|
|
55
|
-
table: string;
|
|
56
|
-
valueColumn: string;
|
|
57
|
-
values: Set<string>;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
function getInstanceFields(
|
|
61
|
-
instance: CustomBlockInstanceWithRelations,
|
|
62
|
-
fallbackFields?: CustomBlockField[]
|
|
63
|
-
) {
|
|
64
|
-
return instance.definition?.fields ?? instance.fields ?? fallbackFields ?? [];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function getInstanceData(instance: CustomBlockInstanceWithRelations) {
|
|
68
|
-
return instance.data ?? instance.values ?? {};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function toRelationField(field: CustomBlockField): CustomBlockRelationField | null {
|
|
72
|
-
return field.type === 'db_relation' ? field : null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function toRawRelationValues(value: unknown, multiple: boolean) {
|
|
76
|
-
if (multiple) {
|
|
77
|
-
return Array.isArray(value) ? value.filter((entry) => entry !== null && entry !== '') : [];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return value === null || value === undefined || value === '' ? [] : [value];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function buildMissingRelation(
|
|
84
|
-
table: string,
|
|
85
|
-
value: unknown,
|
|
86
|
-
error?: string
|
|
87
|
-
): Exclude<ResolvedCustomBlockRelation, unknown[]> {
|
|
88
|
-
return {
|
|
89
|
-
...(error ? { error } : {}),
|
|
90
|
-
record: null,
|
|
91
|
-
table,
|
|
92
|
-
value: String(value),
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function groupKey(table: string, valueColumn: string, displayColumn: string) {
|
|
97
|
-
return `${table}:${valueColumn}:${displayColumn}`;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function fetchRelationGroup(
|
|
101
|
-
supabase: SupabaseClient<Database>,
|
|
102
|
-
group: RelationGroup
|
|
103
|
-
) {
|
|
104
|
-
const result = await searchCustomBlockRelationRows(supabase, {
|
|
105
|
-
displayColumn: group.displayColumn,
|
|
106
|
-
limit: group.values.size,
|
|
107
|
-
table: group.table,
|
|
108
|
-
valueColumn: group.valueColumn,
|
|
109
|
-
values: Array.from(group.values),
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
if ('error' in result) {
|
|
113
|
-
return { error: result.error, rows: new Map<string, CustomBlockRelationRow>() };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return {
|
|
117
|
-
rows: new Map(result.items.map((item) => [item.value, item])),
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function resolveLookupFromGroup(
|
|
122
|
-
lookup: RelationLookup,
|
|
123
|
-
rowsByValue: Map<string, CustomBlockRelationRow>,
|
|
124
|
-
groupError?: string
|
|
125
|
-
) {
|
|
126
|
-
const target = getCustomBlockRelationTarget(lookup.field.table);
|
|
127
|
-
if (!target) {
|
|
128
|
-
const missing = lookup.rawValues.map((value) =>
|
|
129
|
-
buildMissingRelation(lookup.field.table, value, 'Relation table is not available.')
|
|
130
|
-
);
|
|
131
|
-
return lookup.field.multiple ? missing : missing[0];
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const resolved = lookup.rawValues.map((rawValue) => {
|
|
135
|
-
const normalizedValue = normalizeCustomBlockRelationValue(target, rawValue);
|
|
136
|
-
const stringValue = normalizedValue === null ? String(rawValue) : String(normalizedValue);
|
|
137
|
-
const match = rowsByValue.get(stringValue);
|
|
138
|
-
|
|
139
|
-
if (!match) {
|
|
140
|
-
return buildMissingRelation(
|
|
141
|
-
lookup.field.table,
|
|
142
|
-
stringValue,
|
|
143
|
-
groupError ?? 'Relation record was not found.'
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return {
|
|
148
|
-
record: match.record,
|
|
149
|
-
table: lookup.field.table,
|
|
150
|
-
value: match.value,
|
|
151
|
-
};
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
return lookup.field.multiple ? resolved : resolved[0];
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export async function resolveBlockRelations<T extends CustomBlockInstanceWithRelations>(
|
|
158
|
-
instances: T[],
|
|
159
|
-
options?: ResolveBlockRelationsOptions
|
|
160
|
-
): Promise<T[]>;
|
|
161
|
-
export async function resolveBlockRelations<T extends CustomBlockInstanceWithRelations>(
|
|
162
|
-
instance: T,
|
|
163
|
-
options?: ResolveBlockRelationsOptions
|
|
164
|
-
): Promise<T>;
|
|
165
|
-
export async function resolveBlockRelations<T extends CustomBlockInstanceWithRelations>(
|
|
166
|
-
input: T | T[],
|
|
167
|
-
options: ResolveBlockRelationsOptions = {}
|
|
168
|
-
): Promise<T | T[]> {
|
|
169
|
-
const isArrayInput = Array.isArray(input);
|
|
170
|
-
const instances = (isArrayInput ? input : [input]) as T[];
|
|
171
|
-
const supabase = options.supabase ?? getSsgSupabaseClient();
|
|
172
|
-
const groups = new Map<string, RelationGroup>();
|
|
173
|
-
const resolvedByInstance = instances.map(
|
|
174
|
-
(instance) => ({ ...(instance.resolved_relations ?? {}) }) as Record<
|
|
175
|
-
string,
|
|
176
|
-
ResolvedCustomBlockRelation
|
|
177
|
-
>
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
instances.forEach((instance, instanceIndex) => {
|
|
181
|
-
const fields = getInstanceFields(instance, options.fields);
|
|
182
|
-
const data = getInstanceData(instance);
|
|
183
|
-
|
|
184
|
-
fields.forEach((field) => {
|
|
185
|
-
const relationField = toRelationField(field);
|
|
186
|
-
if (!relationField) {
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const rawValues = toRawRelationValues(data[relationField.key], relationField.multiple);
|
|
191
|
-
if (rawValues.length === 0) {
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const target = getCustomBlockRelationTarget(relationField.table);
|
|
196
|
-
if (!target) {
|
|
197
|
-
const missing = rawValues.map((value) =>
|
|
198
|
-
buildMissingRelation(relationField.table, value, 'Relation table is not available.')
|
|
199
|
-
);
|
|
200
|
-
resolvedByInstance[instanceIndex][relationField.key] = relationField.multiple
|
|
201
|
-
? missing
|
|
202
|
-
: missing[0];
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const valueColumn = target.selectColumns.includes(relationField.value_column)
|
|
207
|
-
? relationField.value_column
|
|
208
|
-
: target.valueColumn;
|
|
209
|
-
const displayColumn = target.selectColumns.includes(relationField.display_column)
|
|
210
|
-
? relationField.display_column
|
|
211
|
-
: target.displayColumn;
|
|
212
|
-
const key = groupKey(target.table, valueColumn, displayColumn);
|
|
213
|
-
const group =
|
|
214
|
-
groups.get(key) ??
|
|
215
|
-
({
|
|
216
|
-
displayColumn,
|
|
217
|
-
field: relationField,
|
|
218
|
-
lookups: [],
|
|
219
|
-
table: target.table,
|
|
220
|
-
valueColumn,
|
|
221
|
-
values: new Set<string>(),
|
|
222
|
-
} satisfies RelationGroup);
|
|
223
|
-
|
|
224
|
-
group.lookups.push({ field: relationField, instanceIndex, rawValues });
|
|
225
|
-
|
|
226
|
-
rawValues.forEach((rawValue) => {
|
|
227
|
-
const normalized = normalizeCustomBlockRelationValue(target, rawValue);
|
|
228
|
-
if (normalized !== null) {
|
|
229
|
-
group.values.add(String(normalized));
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
groups.set(key, group);
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
for (const group of groups.values()) {
|
|
238
|
-
const { error, rows } = await fetchRelationGroup(supabase, group);
|
|
239
|
-
|
|
240
|
-
for (const lookup of group.lookups) {
|
|
241
|
-
resolvedByInstance[lookup.instanceIndex][lookup.field.key] = resolveLookupFromGroup(
|
|
242
|
-
lookup,
|
|
243
|
-
rows,
|
|
244
|
-
error
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const hydrated = instances.map((instance, index) => ({
|
|
250
|
-
...instance,
|
|
251
|
-
resolved_relations: resolvedByInstance[index],
|
|
252
|
-
}));
|
|
253
|
-
|
|
254
|
-
return isArrayInput ? hydrated : hydrated[0];
|
|
255
|
-
}
|
|
1
|
+
import 'server-only';
|
|
2
|
+
|
|
3
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
4
|
+
import { getSsgSupabaseClient } from '@nextblock-cms/db/server';
|
|
5
|
+
import type { Database } from '@nextblock-cms/db';
|
|
6
|
+
import type { CustomBlockField } from '@nextblock-cms/utils';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
getCustomBlockRelationTarget,
|
|
10
|
+
normalizeCustomBlockRelationValue,
|
|
11
|
+
} from './custom-block-relation-registry';
|
|
12
|
+
import { searchCustomBlockRelationRows, type CustomBlockRelationRow } from './custom-block-relations';
|
|
13
|
+
|
|
14
|
+
type CustomBlockRelationField = Extract<CustomBlockField, { type: 'db_relation' }>;
|
|
15
|
+
type RelationRecord = Record<string, unknown> | null;
|
|
16
|
+
|
|
17
|
+
export type ResolvedCustomBlockRelation =
|
|
18
|
+
| {
|
|
19
|
+
error?: string;
|
|
20
|
+
record: RelationRecord;
|
|
21
|
+
table: string;
|
|
22
|
+
value: string;
|
|
23
|
+
}
|
|
24
|
+
| Array<{
|
|
25
|
+
error?: string;
|
|
26
|
+
record: RelationRecord;
|
|
27
|
+
table: string;
|
|
28
|
+
value: string;
|
|
29
|
+
}>;
|
|
30
|
+
|
|
31
|
+
export type CustomBlockInstanceWithRelations = {
|
|
32
|
+
data?: Record<string, unknown> | null;
|
|
33
|
+
definition?: { fields?: CustomBlockField[] | null } | null;
|
|
34
|
+
fields?: CustomBlockField[] | null;
|
|
35
|
+
resolved_relations?: Record<string, ResolvedCustomBlockRelation>;
|
|
36
|
+
values?: Record<string, unknown> | null;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type ResolveBlockRelationsOptions = {
|
|
41
|
+
fields?: CustomBlockField[];
|
|
42
|
+
supabase?: SupabaseClient<Database>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type RelationLookup = {
|
|
46
|
+
field: CustomBlockRelationField;
|
|
47
|
+
instanceIndex: number;
|
|
48
|
+
rawValues: unknown[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type RelationGroup = {
|
|
52
|
+
displayColumn: string;
|
|
53
|
+
field: CustomBlockRelationField;
|
|
54
|
+
lookups: RelationLookup[];
|
|
55
|
+
table: string;
|
|
56
|
+
valueColumn: string;
|
|
57
|
+
values: Set<string>;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function getInstanceFields(
|
|
61
|
+
instance: CustomBlockInstanceWithRelations,
|
|
62
|
+
fallbackFields?: CustomBlockField[]
|
|
63
|
+
) {
|
|
64
|
+
return instance.definition?.fields ?? instance.fields ?? fallbackFields ?? [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getInstanceData(instance: CustomBlockInstanceWithRelations) {
|
|
68
|
+
return instance.data ?? instance.values ?? {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function toRelationField(field: CustomBlockField): CustomBlockRelationField | null {
|
|
72
|
+
return field.type === 'db_relation' ? field : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toRawRelationValues(value: unknown, multiple: boolean) {
|
|
76
|
+
if (multiple) {
|
|
77
|
+
return Array.isArray(value) ? value.filter((entry) => entry !== null && entry !== '') : [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return value === null || value === undefined || value === '' ? [] : [value];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildMissingRelation(
|
|
84
|
+
table: string,
|
|
85
|
+
value: unknown,
|
|
86
|
+
error?: string
|
|
87
|
+
): Exclude<ResolvedCustomBlockRelation, unknown[]> {
|
|
88
|
+
return {
|
|
89
|
+
...(error ? { error } : {}),
|
|
90
|
+
record: null,
|
|
91
|
+
table,
|
|
92
|
+
value: String(value),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function groupKey(table: string, valueColumn: string, displayColumn: string) {
|
|
97
|
+
return `${table}:${valueColumn}:${displayColumn}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function fetchRelationGroup(
|
|
101
|
+
supabase: SupabaseClient<Database>,
|
|
102
|
+
group: RelationGroup
|
|
103
|
+
) {
|
|
104
|
+
const result = await searchCustomBlockRelationRows(supabase, {
|
|
105
|
+
displayColumn: group.displayColumn,
|
|
106
|
+
limit: group.values.size,
|
|
107
|
+
table: group.table,
|
|
108
|
+
valueColumn: group.valueColumn,
|
|
109
|
+
values: Array.from(group.values),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if ('error' in result) {
|
|
113
|
+
return { error: result.error, rows: new Map<string, CustomBlockRelationRow>() };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
rows: new Map(result.items.map((item) => [item.value, item])),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function resolveLookupFromGroup(
|
|
122
|
+
lookup: RelationLookup,
|
|
123
|
+
rowsByValue: Map<string, CustomBlockRelationRow>,
|
|
124
|
+
groupError?: string
|
|
125
|
+
) {
|
|
126
|
+
const target = getCustomBlockRelationTarget(lookup.field.table);
|
|
127
|
+
if (!target) {
|
|
128
|
+
const missing = lookup.rawValues.map((value) =>
|
|
129
|
+
buildMissingRelation(lookup.field.table, value, 'Relation table is not available.')
|
|
130
|
+
);
|
|
131
|
+
return lookup.field.multiple ? missing : missing[0];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const resolved = lookup.rawValues.map((rawValue) => {
|
|
135
|
+
const normalizedValue = normalizeCustomBlockRelationValue(target, rawValue);
|
|
136
|
+
const stringValue = normalizedValue === null ? String(rawValue) : String(normalizedValue);
|
|
137
|
+
const match = rowsByValue.get(stringValue);
|
|
138
|
+
|
|
139
|
+
if (!match) {
|
|
140
|
+
return buildMissingRelation(
|
|
141
|
+
lookup.field.table,
|
|
142
|
+
stringValue,
|
|
143
|
+
groupError ?? 'Relation record was not found.'
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
record: match.record,
|
|
149
|
+
table: lookup.field.table,
|
|
150
|
+
value: match.value,
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return lookup.field.multiple ? resolved : resolved[0];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function resolveBlockRelations<T extends CustomBlockInstanceWithRelations>(
|
|
158
|
+
instances: T[],
|
|
159
|
+
options?: ResolveBlockRelationsOptions
|
|
160
|
+
): Promise<T[]>;
|
|
161
|
+
export async function resolveBlockRelations<T extends CustomBlockInstanceWithRelations>(
|
|
162
|
+
instance: T,
|
|
163
|
+
options?: ResolveBlockRelationsOptions
|
|
164
|
+
): Promise<T>;
|
|
165
|
+
export async function resolveBlockRelations<T extends CustomBlockInstanceWithRelations>(
|
|
166
|
+
input: T | T[],
|
|
167
|
+
options: ResolveBlockRelationsOptions = {}
|
|
168
|
+
): Promise<T | T[]> {
|
|
169
|
+
const isArrayInput = Array.isArray(input);
|
|
170
|
+
const instances = (isArrayInput ? input : [input]) as T[];
|
|
171
|
+
const supabase = options.supabase ?? getSsgSupabaseClient();
|
|
172
|
+
const groups = new Map<string, RelationGroup>();
|
|
173
|
+
const resolvedByInstance = instances.map(
|
|
174
|
+
(instance) => ({ ...(instance.resolved_relations ?? {}) }) as Record<
|
|
175
|
+
string,
|
|
176
|
+
ResolvedCustomBlockRelation
|
|
177
|
+
>
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
instances.forEach((instance, instanceIndex) => {
|
|
181
|
+
const fields = getInstanceFields(instance, options.fields);
|
|
182
|
+
const data = getInstanceData(instance);
|
|
183
|
+
|
|
184
|
+
fields.forEach((field) => {
|
|
185
|
+
const relationField = toRelationField(field);
|
|
186
|
+
if (!relationField) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const rawValues = toRawRelationValues(data[relationField.key], relationField.multiple);
|
|
191
|
+
if (rawValues.length === 0) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const target = getCustomBlockRelationTarget(relationField.table);
|
|
196
|
+
if (!target) {
|
|
197
|
+
const missing = rawValues.map((value) =>
|
|
198
|
+
buildMissingRelation(relationField.table, value, 'Relation table is not available.')
|
|
199
|
+
);
|
|
200
|
+
resolvedByInstance[instanceIndex][relationField.key] = relationField.multiple
|
|
201
|
+
? missing
|
|
202
|
+
: missing[0];
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const valueColumn = target.selectColumns.includes(relationField.value_column)
|
|
207
|
+
? relationField.value_column
|
|
208
|
+
: target.valueColumn;
|
|
209
|
+
const displayColumn = target.selectColumns.includes(relationField.display_column)
|
|
210
|
+
? relationField.display_column
|
|
211
|
+
: target.displayColumn;
|
|
212
|
+
const key = groupKey(target.table, valueColumn, displayColumn);
|
|
213
|
+
const group =
|
|
214
|
+
groups.get(key) ??
|
|
215
|
+
({
|
|
216
|
+
displayColumn,
|
|
217
|
+
field: relationField,
|
|
218
|
+
lookups: [],
|
|
219
|
+
table: target.table,
|
|
220
|
+
valueColumn,
|
|
221
|
+
values: new Set<string>(),
|
|
222
|
+
} satisfies RelationGroup);
|
|
223
|
+
|
|
224
|
+
group.lookups.push({ field: relationField, instanceIndex, rawValues });
|
|
225
|
+
|
|
226
|
+
rawValues.forEach((rawValue) => {
|
|
227
|
+
const normalized = normalizeCustomBlockRelationValue(target, rawValue);
|
|
228
|
+
if (normalized !== null) {
|
|
229
|
+
group.values.add(String(normalized));
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
groups.set(key, group);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
for (const group of groups.values()) {
|
|
238
|
+
const { error, rows } = await fetchRelationGroup(supabase, group);
|
|
239
|
+
|
|
240
|
+
for (const lookup of group.lookups) {
|
|
241
|
+
resolvedByInstance[lookup.instanceIndex][lookup.field.key] = resolveLookupFromGroup(
|
|
242
|
+
lookup,
|
|
243
|
+
rows,
|
|
244
|
+
error
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const hydrated = instances.map((instance, index) => ({
|
|
250
|
+
...instance,
|
|
251
|
+
resolved_relations: resolvedByInstance[index],
|
|
252
|
+
}));
|
|
253
|
+
|
|
254
|
+
return isArrayInput ? hydrated : hydrated[0];
|
|
255
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export {
|
|
2
|
-
buildPageSeoDocument,
|
|
3
|
-
type BuildPageSeoDocumentOptions,
|
|
4
|
-
} from '@nextblock-cms/utils/seo';
|
|
1
|
+
export {
|
|
2
|
+
buildPageSeoDocument,
|
|
3
|
+
type BuildPageSeoDocumentOptions,
|
|
4
|
+
} from '@nextblock-cms/utils/seo';
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
*
|
|
14
14
|
* WHY THE ROW TYPE IS HAND-WRITTEN. `libs/db`'s generated `Database` type is
|
|
15
15
|
* produced by `npm run db:types` against a live Supabase project. The migration
|
|
16
|
-
* that
|
|
17
|
-
*
|
|
16
|
+
* that created `cms_redirects` (originally 00000000000030_seo_redirects_and_robots.sql,
|
|
17
|
+
* now part of 02001_baseline_schema.sql) was
|
|
18
|
+
* committed before it was applied, so the generated type did not contain
|
|
18
19
|
* the table and `Database['public']['Tables']['cms_redirects']` would not compile.
|
|
19
20
|
* Writing the row shape by hand from the migration's DDL keeps this file building
|
|
20
21
|
* today; once the migration is applied and the types are regenerated, this
|