@eide/uniformgen 0.1.2 → 0.1.3
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/dist/generators/cms/index.d.ts +1 -1
- package/dist/generators/cms/index.d.ts.map +1 -1
- package/dist/generators/cms/index.js +35 -1
- package/dist/generators/cms/route.d.ts +16 -0
- package/dist/generators/cms/route.d.ts.map +1 -1
- package/dist/generators/cms/route.js +937 -0
- package/dist/graphql/generated/graphql.d.ts +140 -13
- package/dist/graphql/generated/graphql.d.ts.map +1 -1
- package/package.json +14 -14
|
@@ -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
|
+
}
|