@eide/uniformgen 0.1.2 → 0.1.4

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.
@@ -425,3 +425,940 @@ export const ROUTABLE_MODELS = [${modelKeys.map(k => `'${k}'`).join(', ')}] as c
425
425
  export type RoutableModelKey = typeof ROUTABLE_MODELS[number];
426
426
  `;
427
427
  }
428
+ /**
429
+ * Generate React hooks for entity routes lookup
430
+ */
431
+ export function generateEntityRoutesReact(routableModels) {
432
+ const modelKeys = routableModels.map(m => m.key);
433
+ return `/**
434
+ * Entity Routes Lookup Hooks
435
+ *
436
+ * React hooks for fetching CMS routes for entities by natural key.
437
+ * Use for product cards, collection pages, etc. where you need URLs.
438
+ *
439
+ * Handles models: ${modelKeys.join(', ')}
440
+ *
441
+ * @generated by UniformGen - DO NOT EDIT MANUALLY
442
+ */
443
+
444
+ import { useMemo } from 'react';
445
+ import { useQuery, type QueryHookOptions } from '@apollo/client';
446
+ import { gql } from '@apollo/client';
447
+
448
+ /**
449
+ * Route with context dimensions
450
+ */
451
+ export interface EntityRouteWithContext {
452
+ /** The full URL path */
453
+ path: string;
454
+ /** Whether this is the canonical route for this context */
455
+ isCanonical: boolean;
456
+ /** Alias handling strategy if not canonical */
457
+ aliasStrategy: string | null;
458
+ /** Canonical path (if this is an alias) */
459
+ canonicalPath: string | null;
460
+ /** Context dimensions (market, locale, brand, etc.) */
461
+ contexts: Record<string, string | boolean | undefined>;
462
+ }
463
+
464
+ /**
465
+ * Route information for a single entity
466
+ */
467
+ export interface EntityRouteInfo {
468
+ /** The natural key that was requested */
469
+ naturalKey: string;
470
+ /** Whether the entity was found */
471
+ found: boolean;
472
+ /** Entity record ID */
473
+ entityId: string | null;
474
+ /** Entity record metadata */
475
+ metadata: Record<string, unknown> | null;
476
+ /** All routes for this entity */
477
+ routes: EntityRouteWithContext[];
478
+ /** Child entity routes (only when includeChildren: true) */
479
+ children?: EntityRouteInfo[];
480
+ }
481
+
482
+ /**
483
+ * Result of batch route lookup
484
+ */
485
+ export interface GetEntitiesRoutesResult {
486
+ /** Results in same order as input naturalKeys */
487
+ results: EntityRouteInfo[];
488
+ /** Natural keys that were not found */
489
+ notFound: string[];
490
+ }
491
+
492
+ /**
493
+ * GraphQL query for single entity routes
494
+ */
495
+ const GET_ENTITY_ROUTES_QUERY = gql\\\`
496
+ query GetEntityRoutes($input: GetEntityRoutesInput!) {
497
+ getEntityRoutes(input: $input) {
498
+ naturalKey
499
+ found
500
+ entityId
501
+ metadata
502
+ routes {
503
+ path
504
+ isCanonical
505
+ aliasStrategy
506
+ canonicalPath
507
+ contexts
508
+ }
509
+ children {
510
+ naturalKey
511
+ found
512
+ entityId
513
+ metadata
514
+ routes {
515
+ path
516
+ isCanonical
517
+ aliasStrategy
518
+ canonicalPath
519
+ contexts
520
+ }
521
+ }
522
+ }
523
+ }
524
+ \\\`;
525
+
526
+ /**
527
+ * GraphQL query for batch entity routes
528
+ */
529
+ const GET_ENTITIES_ROUTES_QUERY = gql\\\`
530
+ query GetEntitiesRoutes($input: GetEntitiesRoutesInput!) {
531
+ getEntitiesRoutes(input: $input) {
532
+ results {
533
+ naturalKey
534
+ found
535
+ entityId
536
+ metadata
537
+ routes {
538
+ path
539
+ isCanonical
540
+ aliasStrategy
541
+ canonicalPath
542
+ contexts
543
+ }
544
+ }
545
+ notFound
546
+ }
547
+ }
548
+ \\\`;
549
+
550
+ /**
551
+ * Get routes for a single entity by natural key
552
+ *
553
+ * @param modelKey - Entity model key (e.g., 'shopify-product')
554
+ * @param naturalKey - Entity natural key (e.g., 'winter-jacket')
555
+ * @param options - Include children and query options
556
+ *
557
+ * @example
558
+ * const { data, loading } = useGetEntityRoutes('shopify-collection', 'winter-coats', {
559
+ * includeChildren: true,
560
+ * childModelKey: 'shopify-product',
561
+ * });
562
+ *
563
+ * // data.routes contains all routes for the collection
564
+ * // data.children contains routes for products in this collection
565
+ */
566
+ export function useGetEntityRoutes(
567
+ modelKey: string,
568
+ naturalKey: string,
569
+ options?: {
570
+ includeChildren?: boolean;
571
+ childModelKey?: string;
572
+ childContexts?: Record<string, string>;
573
+ queryOptions?: Omit<QueryHookOptions, 'variables'>;
574
+ }
575
+ ) {
576
+ const { queryOptions, ...inputOptions } = options ?? {};
577
+
578
+ const result = useQuery(GET_ENTITY_ROUTES_QUERY, {
579
+ ...queryOptions,
580
+ variables: {
581
+ input: {
582
+ modelKey,
583
+ naturalKey,
584
+ includeChildren: inputOptions.includeChildren,
585
+ childModelKey: inputOptions.childModelKey,
586
+ childContexts: inputOptions.childContexts,
587
+ },
588
+ },
589
+ skip: !modelKey || !naturalKey,
590
+ });
591
+
592
+ return {
593
+ ...result,
594
+ data: result.data?.getEntityRoutes as EntityRouteInfo | undefined,
595
+ };
596
+ }
597
+
598
+ /**
599
+ * Get routes for multiple entities by natural key (batch)
600
+ *
601
+ * Efficient batch lookup optimized for product cards, search results, etc.
602
+ * Returns results in the same order as input naturalKeys.
603
+ *
604
+ * @param modelKey - Entity model key (e.g., 'shopify-product')
605
+ * @param naturalKeys - Array of entity natural keys
606
+ * @param options - Query options
607
+ *
608
+ * @example
609
+ * const { data, loading } = useGetEntitiesRoutes('shopify-product', [
610
+ * 'winter-jacket',
611
+ * 'summer-dress',
612
+ * 'casual-shirt',
613
+ * ]);
614
+ *
615
+ * // data.results is in same order as input naturalKeys
616
+ * // data.notFound contains any keys that weren't found
617
+ */
618
+ export function useGetEntitiesRoutes(
619
+ modelKey: string,
620
+ naturalKeys: string[],
621
+ options?: {
622
+ queryOptions?: Omit<QueryHookOptions, 'variables'>;
623
+ }
624
+ ) {
625
+ const { queryOptions } = options ?? {};
626
+
627
+ // Memoize naturalKeys to prevent unnecessary re-renders
628
+ const stableNaturalKeys = useMemo(() => naturalKeys, [JSON.stringify(naturalKeys)]);
629
+
630
+ const result = useQuery(GET_ENTITIES_ROUTES_QUERY, {
631
+ ...queryOptions,
632
+ variables: {
633
+ input: {
634
+ modelKey,
635
+ naturalKeys: stableNaturalKeys,
636
+ },
637
+ },
638
+ skip: !modelKey || stableNaturalKeys.length === 0,
639
+ });
640
+
641
+ return {
642
+ ...result,
643
+ data: result.data?.getEntitiesRoutes as GetEntitiesRoutesResult | undefined,
644
+ };
645
+ }
646
+
647
+ /**
648
+ * List of model keys that support route resolution
649
+ */
650
+ export const ROUTABLE_MODELS = [\${modelKeys.map(k => \\\`'\${k}'\\\`).join(', ')}] as const;
651
+
652
+ /**
653
+ * Type for routable model keys
654
+ */
655
+ export type RoutableModelKey = typeof ROUTABLE_MODELS[number];
656
+ `;
657
+ }
658
+ /**
659
+ * Generate Remix/Hydrogen functions for entity routes lookup
660
+ */
661
+ export function generateEntityRoutesRemix(routableModels) {
662
+ const modelKeys = routableModels.map(m => m.key);
663
+ return `/**
664
+ * Entity Routes Lookup Functions
665
+ *
666
+ * Server-side functions for fetching CMS routes for entities by natural key.
667
+ * Use in Remix loaders or Hydrogen server functions.
668
+ *
669
+ * Handles models: ${modelKeys.join(', ')}
670
+ *
671
+ * @generated by UniformGen - DO NOT EDIT MANUALLY
672
+ */
673
+
674
+ /**
675
+ * Route with context dimensions
676
+ */
677
+ export interface EntityRouteWithContext {
678
+ /** The full URL path */
679
+ path: string;
680
+ /** Whether this is the canonical route for this context */
681
+ isCanonical: boolean;
682
+ /** Alias handling strategy if not canonical */
683
+ aliasStrategy: string | null;
684
+ /** Canonical path (if this is an alias) */
685
+ canonicalPath: string | null;
686
+ /** Context dimensions (market, locale, brand, etc.) */
687
+ contexts: Record<string, string | boolean | undefined>;
688
+ }
689
+
690
+ /**
691
+ * Route information for a single entity
692
+ */
693
+ export interface EntityRouteInfo {
694
+ /** The natural key that was requested */
695
+ naturalKey: string;
696
+ /** Whether the entity was found */
697
+ found: boolean;
698
+ /** Entity record ID */
699
+ entityId: string | null;
700
+ /** Entity record metadata */
701
+ metadata: Record<string, unknown> | null;
702
+ /** All routes for this entity */
703
+ routes: EntityRouteWithContext[];
704
+ /** Child entity routes (only when includeChildren: true) */
705
+ children?: EntityRouteInfo[];
706
+ }
707
+
708
+ /**
709
+ * Result of batch route lookup
710
+ */
711
+ export interface GetEntitiesRoutesResult {
712
+ /** Results in same order as input naturalKeys */
713
+ results: EntityRouteInfo[];
714
+ /** Natural keys that were not found */
715
+ notFound: string[];
716
+ }
717
+
718
+ /** GraphQL client interface */
719
+ export interface GraphQLClient {
720
+ request<T>(query: string, variables?: Record<string, unknown>): Promise<T>;
721
+ }
722
+
723
+ /**
724
+ * GraphQL query for single entity routes
725
+ */
726
+ const GET_ENTITY_ROUTES_QUERY = \\\`
727
+ query GetEntityRoutes($input: GetEntityRoutesInput!) {
728
+ getEntityRoutes(input: $input) {
729
+ naturalKey
730
+ found
731
+ entityId
732
+ metadata
733
+ routes {
734
+ path
735
+ isCanonical
736
+ aliasStrategy
737
+ canonicalPath
738
+ contexts
739
+ }
740
+ children {
741
+ naturalKey
742
+ found
743
+ entityId
744
+ metadata
745
+ routes {
746
+ path
747
+ isCanonical
748
+ aliasStrategy
749
+ canonicalPath
750
+ contexts
751
+ }
752
+ }
753
+ }
754
+ }
755
+ \\\`;
756
+
757
+ /**
758
+ * GraphQL query for batch entity routes
759
+ */
760
+ const GET_ENTITIES_ROUTES_QUERY = \\\`
761
+ query GetEntitiesRoutes($input: GetEntitiesRoutesInput!) {
762
+ getEntitiesRoutes(input: $input) {
763
+ results {
764
+ naturalKey
765
+ found
766
+ entityId
767
+ metadata
768
+ routes {
769
+ path
770
+ isCanonical
771
+ aliasStrategy
772
+ canonicalPath
773
+ contexts
774
+ }
775
+ }
776
+ notFound
777
+ }
778
+ }
779
+ \\\`;
780
+
781
+ /**
782
+ * Get routes for a single entity by natural key
783
+ *
784
+ * @param client - GraphQL client
785
+ * @param modelKey - Entity model key (e.g., 'shopify-product')
786
+ * @param naturalKey - Entity natural key (e.g., 'winter-jacket')
787
+ * @param options - Include children options
788
+ *
789
+ * @example
790
+ * // In a Remix loader
791
+ * export async function loader({ context }: LoaderFunctionArgs) {
792
+ * const routes = await getEntityRoutes(
793
+ * context.graphql,
794
+ * 'shopify-collection',
795
+ * 'winter-coats',
796
+ * { includeChildren: true, childModelKey: 'shopify-product' }
797
+ * );
798
+ * return json({ routes });
799
+ * }
800
+ */
801
+ export async function getEntityRoutes(
802
+ client: GraphQLClient,
803
+ modelKey: string,
804
+ naturalKey: string,
805
+ options?: {
806
+ includeChildren?: boolean;
807
+ childModelKey?: string;
808
+ childContexts?: Record<string, string>;
809
+ }
810
+ ): Promise<EntityRouteInfo | null> {
811
+ const result = await client.request<{ getEntityRoutes: EntityRouteInfo | null }>(
812
+ GET_ENTITY_ROUTES_QUERY,
813
+ {
814
+ input: {
815
+ modelKey,
816
+ naturalKey,
817
+ includeChildren: options?.includeChildren,
818
+ childModelKey: options?.childModelKey,
819
+ childContexts: options?.childContexts,
820
+ },
821
+ }
822
+ );
823
+ return result.getEntityRoutes;
824
+ }
825
+
826
+ /**
827
+ * Get routes for multiple entities by natural key (batch)
828
+ *
829
+ * Efficient batch lookup optimized for product cards, search results, etc.
830
+ * Returns results in the same order as input naturalKeys.
831
+ *
832
+ * @param client - GraphQL client
833
+ * @param modelKey - Entity model key (e.g., 'shopify-product')
834
+ * @param naturalKeys - Array of entity natural keys
835
+ *
836
+ * @example
837
+ * // In a Remix loader - get URLs for products from Shopify
838
+ * export async function loader({ context }: LoaderFunctionArgs) {
839
+ * // Get products from Shopify
840
+ * const { products } = await context.storefront.query(PRODUCTS_QUERY);
841
+ *
842
+ * // Get CMS routes for those products
843
+ * const routes = await getEntitiesRoutes(
844
+ * context.graphql,
845
+ * 'shopify-product',
846
+ * products.nodes.map(p => p.handle)
847
+ * );
848
+ *
849
+ * return json({ products, routes });
850
+ * }
851
+ */
852
+ export async function getEntitiesRoutes(
853
+ client: GraphQLClient,
854
+ modelKey: string,
855
+ naturalKeys: string[]
856
+ ): Promise<GetEntitiesRoutesResult> {
857
+ if (naturalKeys.length === 0) {
858
+ return { results: [], notFound: [] };
859
+ }
860
+
861
+ const result = await client.request<{ getEntitiesRoutes: GetEntitiesRoutesResult }>(
862
+ GET_ENTITIES_ROUTES_QUERY,
863
+ {
864
+ input: {
865
+ modelKey,
866
+ naturalKeys,
867
+ },
868
+ }
869
+ );
870
+ return result.getEntitiesRoutes;
871
+ }
872
+
873
+ /**
874
+ * List of model keys that support route resolution
875
+ */
876
+ export const ROUTABLE_MODELS = [\${modelKeys.map(k => \\\`'\${k}'\\\`).join(', ')}] as const;
877
+
878
+ /**
879
+ * Type for routable model keys
880
+ */
881
+ export type RoutableModelKey = typeof ROUTABLE_MODELS[number];
882
+ `;
883
+ }
884
+ /**
885
+ * Generate React hook for batch entity content resolution
886
+ */
887
+ export function generateResolveEntitiesReact(routableModels) {
888
+ const modelKeys = routableModels.map(m => m.key);
889
+ return `/**
890
+ * Batch Entity Content Resolution Hook
891
+ *
892
+ * React hook for resolving full content for multiple entities by natural key.
893
+ * Use when you need full content with templates, zones, layouts.
894
+ *
895
+ * Handles models: ${modelKeys.join(', ')}
896
+ *
897
+ * @generated by UniformGen - DO NOT EDIT MANUALLY
898
+ */
899
+
900
+ import { useMemo } from 'react';
901
+ import { useQuery, type QueryHookOptions } from '@apollo/client';
902
+ import { gql } from '@apollo/client';
903
+ import type {
904
+ ResolvedContent,
905
+ ResolvedField,
906
+ ResolvedTemplateRef,
907
+ FieldLayout,
908
+ GridConfig,
909
+ } from './types.js';
910
+
911
+ /**
912
+ * Resolved entity content result
913
+ */
914
+ export interface ResolvedEntityContent {
915
+ /** Entity record info */
916
+ record: {
917
+ id: string;
918
+ modelKey: string;
919
+ naturalKey: string;
920
+ metadata: Record<string, unknown> | null;
921
+ };
922
+ /** Selected variant */
923
+ variant: {
924
+ id: string;
925
+ variantKey: string;
926
+ } | null;
927
+ /** Resolved content with fields and layout */
928
+ content: ResolvedContent;
929
+ /** Grid configuration */
930
+ gridConfig: GridConfig | null;
931
+ /** Resolution context used */
932
+ resolvedWith: {
933
+ locale: string;
934
+ contexts: Record<string, string>;
935
+ };
936
+ }
937
+
938
+ /**
939
+ * Single entity resolution result in batch
940
+ */
941
+ export interface ResolvedEntityBatchResult {
942
+ /** The natural key that was requested */
943
+ naturalKey: string;
944
+ /** Whether the entity was found and resolved */
945
+ found: boolean;
946
+ /** Full resolution result */
947
+ resolution: ResolvedEntityContent | null;
948
+ }
949
+
950
+ /**
951
+ * Result of batch entity resolution
952
+ */
953
+ export interface ResolveEntitiesResult {
954
+ /** Results in same order as input naturalKeys */
955
+ results: ResolvedEntityBatchResult[];
956
+ /** Natural keys that were not found */
957
+ notFound: string[];
958
+ /** Resolution context used */
959
+ resolvedWith: {
960
+ locale: string;
961
+ contexts: Record<string, string>;
962
+ };
963
+ }
964
+
965
+ /**
966
+ * GraphQL query for batch entity resolution
967
+ */
968
+ const RESOLVE_ENTITIES_QUERY = gql\\\`
969
+ query ResolveEntities($input: ResolveEntitiesInput!) {
970
+ resolveEntities(input: $input) {
971
+ results {
972
+ naturalKey
973
+ found
974
+ resolution {
975
+ record {
976
+ id
977
+ modelKey
978
+ naturalKey
979
+ metadata
980
+ }
981
+ variant {
982
+ id
983
+ variantKey
984
+ }
985
+ content {
986
+ template {
987
+ id
988
+ modelKey
989
+ naturalKey
990
+ content
991
+ }
992
+ layoutMode
993
+ fields {
994
+ key
995
+ type
996
+ label
997
+ required
998
+ value
999
+ layout {
1000
+ lg { x y w h minW maxW minH maxH }
1001
+ md { x y w h minW maxW minH maxH }
1002
+ sm { x y w h minW maxW minH maxH }
1003
+ xs { x y w h minW maxW minH maxH }
1004
+ }
1005
+ order
1006
+ source
1007
+ }
1008
+ extraContent {
1009
+ key
1010
+ type
1011
+ label
1012
+ required
1013
+ value
1014
+ layout {
1015
+ lg { x y w h minW maxW minH maxH }
1016
+ md { x y w h minW maxW minH maxH }
1017
+ sm { x y w h minW maxW minH maxH }
1018
+ xs { x y w h minW maxW minH maxH }
1019
+ }
1020
+ order
1021
+ source
1022
+ }
1023
+ }
1024
+ gridConfig {
1025
+ breakpoints
1026
+ cols
1027
+ rowHeight
1028
+ containerPadding
1029
+ margin
1030
+ }
1031
+ resolvedWith {
1032
+ locale
1033
+ contexts
1034
+ }
1035
+ }
1036
+ }
1037
+ notFound
1038
+ resolvedWith {
1039
+ locale
1040
+ contexts
1041
+ }
1042
+ }
1043
+ }
1044
+ \\\`;
1045
+
1046
+ /**
1047
+ * Resolve full content for multiple entities by natural key
1048
+ *
1049
+ * Use when you need full content with templates, zones, layouts.
1050
+ * More expensive than useGetEntitiesRoutes - use that for just URLs.
1051
+ *
1052
+ * @param modelKey - Entity model key (e.g., 'shopify-product')
1053
+ * @param naturalKeys - Array of entity natural keys
1054
+ * @param options - Resolution context and options
1055
+ *
1056
+ * @example
1057
+ * const { data, loading } = useResolveEntities('shopify-product', [
1058
+ * 'winter-jacket',
1059
+ * 'summer-dress',
1060
+ * ], {
1061
+ * locale: 'en-US',
1062
+ * contexts: { market: 'us' },
1063
+ * includeTemplate: true,
1064
+ * });
1065
+ *
1066
+ * // data.results contains full resolved content for each product
1067
+ */
1068
+ export function useResolveEntities(
1069
+ modelKey: string,
1070
+ naturalKeys: string[],
1071
+ options?: {
1072
+ locale?: string;
1073
+ contexts?: Record<string, string>;
1074
+ includeTemplate?: boolean;
1075
+ referenceOptions?: {
1076
+ maxDepth?: number | null;
1077
+ resolveMedia?: boolean;
1078
+ resolveEntities?: boolean;
1079
+ };
1080
+ queryOptions?: Omit<QueryHookOptions, 'variables'>;
1081
+ }
1082
+ ) {
1083
+ const { queryOptions, ...inputOptions } = options ?? {};
1084
+
1085
+ // Memoize naturalKeys to prevent unnecessary re-renders
1086
+ const stableNaturalKeys = useMemo(() => naturalKeys, [JSON.stringify(naturalKeys)]);
1087
+
1088
+ const result = useQuery(RESOLVE_ENTITIES_QUERY, {
1089
+ ...queryOptions,
1090
+ variables: {
1091
+ input: {
1092
+ modelKey,
1093
+ naturalKeys: stableNaturalKeys,
1094
+ locale: inputOptions.locale,
1095
+ contexts: inputOptions.contexts,
1096
+ includeTemplate: inputOptions.includeTemplate,
1097
+ referenceOptions: inputOptions.referenceOptions,
1098
+ },
1099
+ },
1100
+ skip: !modelKey || stableNaturalKeys.length === 0,
1101
+ });
1102
+
1103
+ return {
1104
+ ...result,
1105
+ data: result.data?.resolveEntities as ResolveEntitiesResult | undefined,
1106
+ };
1107
+ }
1108
+
1109
+ /**
1110
+ * List of model keys that support content resolution
1111
+ */
1112
+ export const ROUTABLE_MODELS = [\${modelKeys.map(k => \\\`'\${k}'\\\`).join(', ')}] as const;
1113
+
1114
+ /**
1115
+ * Type for routable model keys
1116
+ */
1117
+ export type RoutableModelKey = typeof ROUTABLE_MODELS[number];
1118
+ `;
1119
+ }
1120
+ /**
1121
+ * Generate Remix/Hydrogen function for batch entity content resolution
1122
+ */
1123
+ export function generateResolveEntitiesRemix(routableModels) {
1124
+ const modelKeys = routableModels.map(m => m.key);
1125
+ return `/**
1126
+ * Batch Entity Content Resolution Function
1127
+ *
1128
+ * Server-side function for resolving full content for multiple entities.
1129
+ * Use in Remix loaders or Hydrogen server functions.
1130
+ *
1131
+ * Handles models: ${modelKeys.join(', ')}
1132
+ *
1133
+ * @generated by UniformGen - DO NOT EDIT MANUALLY
1134
+ */
1135
+
1136
+ import type {
1137
+ ResolvedContent,
1138
+ ResolvedField,
1139
+ ResolvedTemplateRef,
1140
+ FieldLayout,
1141
+ GridConfig,
1142
+ } from './types.js';
1143
+
1144
+ /**
1145
+ * Resolved entity content result
1146
+ */
1147
+ export interface ResolvedEntityContent {
1148
+ /** Entity record info */
1149
+ record: {
1150
+ id: string;
1151
+ modelKey: string;
1152
+ naturalKey: string;
1153
+ metadata: Record<string, unknown> | null;
1154
+ };
1155
+ /** Selected variant */
1156
+ variant: {
1157
+ id: string;
1158
+ variantKey: string;
1159
+ } | null;
1160
+ /** Resolved content with fields and layout */
1161
+ content: ResolvedContent;
1162
+ /** Grid configuration */
1163
+ gridConfig: GridConfig | null;
1164
+ /** Resolution context used */
1165
+ resolvedWith: {
1166
+ locale: string;
1167
+ contexts: Record<string, string>;
1168
+ };
1169
+ }
1170
+
1171
+ /**
1172
+ * Single entity resolution result in batch
1173
+ */
1174
+ export interface ResolvedEntityBatchResult {
1175
+ /** The natural key that was requested */
1176
+ naturalKey: string;
1177
+ /** Whether the entity was found and resolved */
1178
+ found: boolean;
1179
+ /** Full resolution result */
1180
+ resolution: ResolvedEntityContent | null;
1181
+ }
1182
+
1183
+ /**
1184
+ * Result of batch entity resolution
1185
+ */
1186
+ export interface ResolveEntitiesResult {
1187
+ /** Results in same order as input naturalKeys */
1188
+ results: ResolvedEntityBatchResult[];
1189
+ /** Natural keys that were not found */
1190
+ notFound: string[];
1191
+ /** Resolution context used */
1192
+ resolvedWith: {
1193
+ locale: string;
1194
+ contexts: Record<string, string>;
1195
+ };
1196
+ }
1197
+
1198
+ /** GraphQL client interface */
1199
+ export interface GraphQLClient {
1200
+ request<T>(query: string, variables?: Record<string, unknown>): Promise<T>;
1201
+ }
1202
+
1203
+ /**
1204
+ * GraphQL query for batch entity resolution
1205
+ */
1206
+ const RESOLVE_ENTITIES_QUERY = \\\`
1207
+ query ResolveEntities($input: ResolveEntitiesInput!) {
1208
+ resolveEntities(input: $input) {
1209
+ results {
1210
+ naturalKey
1211
+ found
1212
+ resolution {
1213
+ record {
1214
+ id
1215
+ modelKey
1216
+ naturalKey
1217
+ metadata
1218
+ }
1219
+ variant {
1220
+ id
1221
+ variantKey
1222
+ }
1223
+ content {
1224
+ template {
1225
+ id
1226
+ modelKey
1227
+ naturalKey
1228
+ content
1229
+ }
1230
+ layoutMode
1231
+ fields {
1232
+ key
1233
+ type
1234
+ label
1235
+ required
1236
+ value
1237
+ layout {
1238
+ lg { x y w h minW maxW minH maxH }
1239
+ md { x y w h minW maxW minH maxH }
1240
+ sm { x y w h minW maxW minH maxH }
1241
+ xs { x y w h minW maxW minH maxH }
1242
+ }
1243
+ order
1244
+ source
1245
+ }
1246
+ extraContent {
1247
+ key
1248
+ type
1249
+ label
1250
+ required
1251
+ value
1252
+ layout {
1253
+ lg { x y w h minW maxW minH maxH }
1254
+ md { x y w h minW maxW minH maxH }
1255
+ sm { x y w h minW maxW minH maxH }
1256
+ xs { x y w h minW maxW minH maxH }
1257
+ }
1258
+ order
1259
+ source
1260
+ }
1261
+ }
1262
+ gridConfig {
1263
+ breakpoints
1264
+ cols
1265
+ rowHeight
1266
+ containerPadding
1267
+ margin
1268
+ }
1269
+ resolvedWith {
1270
+ locale
1271
+ contexts
1272
+ }
1273
+ }
1274
+ }
1275
+ notFound
1276
+ resolvedWith {
1277
+ locale
1278
+ contexts
1279
+ }
1280
+ }
1281
+ }
1282
+ \\\`;
1283
+
1284
+ /**
1285
+ * Resolve full content for multiple entities by natural key
1286
+ *
1287
+ * Use when you need full content with templates, zones, layouts.
1288
+ * More expensive than getEntitiesRoutes - use that for just URLs.
1289
+ *
1290
+ * @param client - GraphQL client
1291
+ * @param modelKey - Entity model key (e.g., 'shopify-product')
1292
+ * @param naturalKeys - Array of entity natural keys
1293
+ * @param options - Resolution context and options
1294
+ *
1295
+ * @example
1296
+ * // In a Remix loader - resolve content for products
1297
+ * export async function loader({ context }: LoaderFunctionArgs) {
1298
+ * // Get products from Shopify
1299
+ * const { products } = await context.storefront.query(PRODUCTS_QUERY);
1300
+ *
1301
+ * // Resolve CMS content for those products
1302
+ * const content = await resolveEntities(
1303
+ * context.graphql,
1304
+ * 'shopify-product',
1305
+ * products.nodes.map(p => p.handle),
1306
+ * { locale: 'en-US', contexts: { market: 'us' } }
1307
+ * );
1308
+ *
1309
+ * return json({ products, content });
1310
+ * }
1311
+ */
1312
+ export async function resolveEntities(
1313
+ client: GraphQLClient,
1314
+ modelKey: string,
1315
+ naturalKeys: string[],
1316
+ options?: {
1317
+ locale?: string;
1318
+ contexts?: Record<string, string>;
1319
+ includeTemplate?: boolean;
1320
+ referenceOptions?: {
1321
+ maxDepth?: number | null;
1322
+ resolveMedia?: boolean;
1323
+ resolveEntities?: boolean;
1324
+ };
1325
+ }
1326
+ ): Promise<ResolveEntitiesResult> {
1327
+ if (naturalKeys.length === 0) {
1328
+ return {
1329
+ results: [],
1330
+ notFound: [],
1331
+ resolvedWith: {
1332
+ locale: options?.locale || 'default',
1333
+ contexts: options?.contexts || {},
1334
+ },
1335
+ };
1336
+ }
1337
+
1338
+ const result = await client.request<{ resolveEntities: ResolveEntitiesResult }>(
1339
+ RESOLVE_ENTITIES_QUERY,
1340
+ {
1341
+ input: {
1342
+ modelKey,
1343
+ naturalKeys,
1344
+ locale: options?.locale,
1345
+ contexts: options?.contexts,
1346
+ includeTemplate: options?.includeTemplate,
1347
+ referenceOptions: options?.referenceOptions,
1348
+ },
1349
+ }
1350
+ );
1351
+ return result.resolveEntities;
1352
+ }
1353
+
1354
+ /**
1355
+ * List of model keys that support content resolution
1356
+ */
1357
+ export const ROUTABLE_MODELS = [\${modelKeys.map(k => \\\`'\${k}'\\\`).join(', ')}] as const;
1358
+
1359
+ /**
1360
+ * Type for routable model keys
1361
+ */
1362
+ export type RoutableModelKey = typeof ROUTABLE_MODELS[number];
1363
+ `;
1364
+ }