@xleddyl/nuxt-cms 0.1.41 → 0.1.42

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/README.md CHANGED
@@ -57,8 +57,9 @@ Then declare your content types in a `cms.config.ts` at the project root with `d
57
57
  ### Disabling the CMS
58
58
 
59
59
  Keep the module in `modules[]` at all times and turn it off with the `enabled` option or the
60
- `NUXT_CMS_ENABLED` env var. When disabled the module registers no-op `useCms` / `$cmsQuery` stubs and
61
- nothing else, so components can call them unconditionally and simply render their empty states:
60
+ `NUXT_CMS_ENABLED` env var. When disabled the module registers no-op query composables and keeps the
61
+ generated types, and nothing else, so components can call them unconditionally and simply render
62
+ their empty states:
62
63
 
63
64
  ```ts
64
65
  export default defineNuxtConfig({
@@ -97,7 +98,7 @@ Full documentation lives in [`docs/`](docs/README.md):
97
98
  - [Configuration](docs/configuration.md) — every `cms.*` option and the `NUXT_CMS_*` env vars.
98
99
  - [Database](docs/database.md) — SQLite, Postgres, libSQL/Turso and D1 drivers, migrations, studio.
99
100
  - [Schema](docs/schema.md) — `defineCmsConfig`, entries, field types, relations, blocks, i18n.
100
- - [Querying content](docs/querying.md) — GraphQL API, `useCms` / `$cmsQuery`, filters, sorting, pagination.
101
+ - [Querying content](docs/querying.md) — GraphQL API, `useCmsSingle` / `useCmsCollection` / `useCms` / `$cmsQuery`, filters, sorting, pagination.
101
102
  - [Admin panel & security](docs/admin.md) — pages, authentication, sessions, admin REST API.
102
103
  - [Media](docs/media.md) — S3-compatible storage or local mode backed by your `public/` folder, upload flow, allowed file types.
103
104
  - [Deployment](docs/deployment.md) — host/driver matrix, migrations on serverless, horizontal scaling, Cloudflare Workers.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xleddyl/nuxt-cms",
3
3
  "configKey": "cms",
4
- "version": "0.1.41",
4
+ "version": "0.1.42",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -3,14 +3,14 @@ import { existsSync } from 'node:fs';
3
3
  import { createRequire } from 'node:module';
4
4
  import { join, isAbsolute, resolve, relative, dirname } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { defineNuxtModule, createResolver, useLogger, addImports, resolvePath, addTemplate, addServerPlugin, addTypeTemplate, addVitePlugin, addComponentsDir, addRouteMiddleware, extendPages, addServerHandler } from '@nuxt/kit';
6
+ import { defineNuxtModule, createResolver, useLogger, addImports, addTemplate, addServerPlugin, addTypeTemplate, addVitePlugin, addComponentsDir, addRouteMiddleware, extendPages, addServerHandler, resolvePath } from '@nuxt/kit';
7
7
  import tailwindcss from '@tailwindcss/vite';
8
8
  import svgLoader from 'vite-svg-loader';
9
9
  import { introspectionFromSchema, buildSchema } from 'graphql';
10
10
  import { minifyIntrospection, outputIntrospectionFile } from 'gql.tada/internal';
11
11
  import { createJiti } from 'jiti';
12
12
  import { typeName, blockTypeName, blocksFieldTypeName, renderGraphqlSdl } from '../dist/runtime/shared/graphql-sdl.js';
13
- import { isMultiSelect, fieldConditions, isTranslatableField, isTranslatableMediaField, isRequiredField, isPrivateField, mediaTypeFilter, DEFAULT_MEDIA_MAX_FILE_SIZE } from '../dist/runtime/shared/index.js';
13
+ import { isTranslatableField, isTranslatableMediaField, isMultiSelect, fieldConditions, isRequiredField, isPrivateField, mediaTypeFilter, DEFAULT_MEDIA_MAX_FILE_SIZE } from '../dist/runtime/shared/index.js';
14
14
  import { scanMediaDirectory, readMediaFileMeta } from '../dist/runtime/server/utils/media-sync.js';
15
15
  import { createHash } from 'node:crypto';
16
16
  import { readFile } from 'node:fs/promises';
@@ -450,6 +450,80 @@ ${[mediaTableExpr(dialect), ...tables, ...joins].join(
450
450
  `;
451
451
  }
452
452
 
453
+ const MEDIA_SELECTION = "key url type alt folder mime size width height";
454
+ function blocksSelection(entryName, key, field) {
455
+ const parts = ["type"];
456
+ for (const [blockName, block] of Object.entries(field.blocks ?? {})) {
457
+ const fields = Object.entries(block.fields).map(
458
+ ([blockFieldKey, blockField]) => blockField.type === "media" ? `${blockFieldKey} { ${MEDIA_SELECTION} }` : blockFieldKey
459
+ );
460
+ if (!fields.length) continue;
461
+ parts.push(`... on ${blockTypeName(entryName, key, blockName)} { ${fields.join(" ")} }`);
462
+ }
463
+ return parts.join(" ");
464
+ }
465
+ function entrySelection(config, name, entry, withRelations) {
466
+ const parts = ["id"];
467
+ for (const [key, field] of Object.entries(entry.fields)) {
468
+ if (isPrivateField(field)) continue;
469
+ if (field.type === "relation") {
470
+ const target = config[field.to];
471
+ if (!withRelations || !target) continue;
472
+ parts.push(`${key} { ${entrySelection(config, field.to, target, false)} }`);
473
+ continue;
474
+ }
475
+ if (field.type === "media") {
476
+ parts.push(`${key} { ${MEDIA_SELECTION} }`);
477
+ continue;
478
+ }
479
+ if (field.type === "blocks") {
480
+ parts.push(`${key} { ${blocksSelection(name, key, field)} }`);
481
+ continue;
482
+ }
483
+ parts.push(key);
484
+ }
485
+ if (entry.kind === "collection") parts.push("createdAt");
486
+ parts.push("updatedAt");
487
+ return parts.join(" ");
488
+ }
489
+ function singleQuery(config, name, entry) {
490
+ const selection = entrySelection(config, name, entry, true);
491
+ return `query CmsSingle($locale: String) { ${name}(locale: $locale) { ${selection} } }`;
492
+ }
493
+ function collectionQuery(config, name, entry) {
494
+ const selection = entrySelection(config, name, entry, true);
495
+ const gqlType = typeName(name);
496
+ const args = [
497
+ "$locale: String",
498
+ `$filters: ${gqlType}Filters`,
499
+ `$sort: [${gqlType}Sort!]`,
500
+ "$limit: Int",
501
+ "$offset: Int"
502
+ ].join(", ");
503
+ return `query CmsCollection(${args}) { ${name}(locale: $locale, filters: $filters, sort: $sort, limit: $limit, offset: $offset) { ${selection} } }`;
504
+ }
505
+ function renderQueriesFile(config) {
506
+ const singles = [];
507
+ const collections = [];
508
+ for (const [name, entry] of Object.entries(config)) {
509
+ const line = ` ${JSON.stringify(name)}: ${JSON.stringify(
510
+ entry.kind === "single" ? singleQuery(config, name, entry) : collectionQuery(config, name, entry)
511
+ )},`;
512
+ if (entry.kind === "single") singles.push(line);
513
+ else collections.push(line);
514
+ }
515
+ return [
516
+ `export const cmsSingleQueries: Record<string, string> = {`,
517
+ ...singles,
518
+ `}`,
519
+ ``,
520
+ `export const cmsCollectionQueries: Record<string, string> = {`,
521
+ ...collections,
522
+ `}`,
523
+ ``
524
+ ].join("\n");
525
+ }
526
+
453
527
  function mediaTsType(field) {
454
528
  const types = mediaTypeFilter(field.mediaType);
455
529
  return types ? `CmsMedia<${types.map((type) => `'${type}'`).join(" | ")}>` : "CmsMedia";
@@ -522,6 +596,54 @@ function entryTs(config, name, entry) {
522
596
  ${lines.join("\n")}
523
597
  }`;
524
598
  }
599
+ function relationKeys(entry) {
600
+ return Object.entries(entry.fields).filter(([, field]) => field.type === "relation" && !isPrivateField(field)).map(([key]) => key);
601
+ }
602
+ function quotedKeys(keys) {
603
+ return keys.map((key) => `'${key}'`).join(" | ");
604
+ }
605
+ function shallowTypeTs(config, name) {
606
+ const target = typeName(name);
607
+ const entry = config[name];
608
+ const keys = entry ? relationKeys(entry) : [];
609
+ return keys.length ? `Omit<${target}, ${quotedKeys(keys)}>` : target;
610
+ }
611
+ function autoTypeTs(config, name, entry) {
612
+ const auto = `${typeName(name)}Auto`;
613
+ const keys = relationKeys(entry);
614
+ if (!keys.length) return `export type ${auto} = ${typeName(name)}`;
615
+ const lines = keys.map((key) => {
616
+ const field = entry.fields[key];
617
+ const shallow = shallowTypeTs(config, field.to);
618
+ if (field.cardinality === "many-to-many") return ` ${key}: ${shallow}[]`;
619
+ const nonNull = isRequiredField(field) && !config[field.to]?.drafts;
620
+ return ` ${key}: ${shallow}${nonNull ? "" : " | null"}`;
621
+ });
622
+ return `export type ${auto} = Omit<${typeName(name)}, ${quotedKeys(keys)}> & {
623
+ ${lines.join(
624
+ "\n"
625
+ )}
626
+ }`;
627
+ }
628
+ function entryMapsTs(config) {
629
+ const singles = [];
630
+ const collections = [];
631
+ for (const [name, entry] of Object.entries(config)) {
632
+ const line = ` ${JSON.stringify(name)}: ${typeName(name)}Auto`;
633
+ if (entry.kind === "single") singles.push(line);
634
+ else collections.push(line);
635
+ }
636
+ return [
637
+ `export interface CmsSingleTypes {
638
+ ${singles.join("\n")}
639
+ }`,
640
+ `export interface CmsCollectionTypes {
641
+ ${collections.join("\n")}
642
+ }`,
643
+ `export type CmsSingleName = keyof CmsSingleTypes`,
644
+ `export type CmsCollectionName = keyof CmsCollectionTypes`
645
+ ];
646
+ }
525
647
  function renderTypesFile(config) {
526
648
  const parts = [
527
649
  `export type CmsMediaType = 'image' | 'video' | 'file'`,
@@ -544,6 +666,8 @@ function renderTypesFile(config) {
544
666
  }
545
667
  parts.push(entryTs(config, name, entry));
546
668
  }
669
+ for (const [name, entry] of Object.entries(config)) parts.push(autoTypeTs(config, name, entry));
670
+ parts.push(...entryMapsTs(config));
547
671
  return `${parts.join("\n\n")}
548
672
  `;
549
673
  }
@@ -632,6 +756,104 @@ function resolveModuleOptions(options) {
632
756
  }
633
757
  };
634
758
  }
759
+ const moduleRequire = createRequire(import.meta.url);
760
+ function resolveImport(specifier) {
761
+ try {
762
+ return fileURLToPath(import.meta.resolve(specifier)).replace(/\\/g, "/");
763
+ } catch {
764
+ return moduleRequire.resolve(specifier).replace(/\\/g, "/");
765
+ }
766
+ }
767
+ async function loadCmsConfig(nuxt, resolver, configPathOption, i18n, logger) {
768
+ const configPath = await resolvePath(configPathOption, { cwd: nuxt.options.rootDir });
769
+ nuxt.options.alias["#nuxt-cms"] = resolver.resolve("./runtime/shared/index");
770
+ nuxt.options.watch.push(configPath);
771
+ let cmsConfig = {};
772
+ if (existsSync(configPath)) {
773
+ nuxt.options.alias["#cms-config"] = configPath;
774
+ const jiti = createJiti(import.meta.url, {
775
+ moduleCache: false,
776
+ alias: { "#nuxt-cms": resolver.resolve("./runtime/shared/index") }
777
+ });
778
+ cmsConfig = await jiti.import(configPath, { default: true });
779
+ } else {
780
+ logger.warn(
781
+ `[nuxt-cms] Config file not found: ${configPath}. Using an empty registry \u2014 create a ${configPathOption}.ts with defineCmsConfig().`
782
+ );
783
+ nuxt.options.alias["#cms-config"] = resolver.resolve("./runtime/shared/empty-config");
784
+ }
785
+ const configErrors = validateConfig(cmsConfig, i18n);
786
+ if (configErrors.length) {
787
+ for (const error of configErrors) logger.error(error);
788
+ throw new Error(
789
+ `[nuxt-cms] Invalid cms config (${configErrors.length} error${configErrors.length > 1 ? "s" : ""})`
790
+ );
791
+ }
792
+ return cmsConfig;
793
+ }
794
+ function addCmsTypeTemplates(nuxt, cmsConfig) {
795
+ addTemplate({
796
+ filename: "cms/schema.graphql",
797
+ write: true,
798
+ getContents: () => renderGraphqlSdl(cmsConfig)
799
+ });
800
+ const typesTemplate = addTemplate({
801
+ filename: "cms/types.ts",
802
+ write: true,
803
+ getContents: () => renderTypesFile(cmsConfig)
804
+ });
805
+ nuxt.options.alias["#cms-types"] = typesTemplate.dst;
806
+ const queriesTemplate = addTemplate({
807
+ filename: "cms/queries.ts",
808
+ write: true,
809
+ getContents: () => renderQueriesFile(cmsConfig)
810
+ });
811
+ nuxt.options.alias["#cms-queries"] = queriesTemplate.dst;
812
+ addTemplate({
813
+ filename: "cms/graphql-env.d.ts",
814
+ write: true,
815
+ getContents: () => {
816
+ const introspection = minifyIntrospection(
817
+ introspectionFromSchema(buildSchema(renderGraphqlSdl(cmsConfig)))
818
+ );
819
+ return outputIntrospectionFile(introspection, {
820
+ fileType: ".d.ts",
821
+ shouldPreprocess: true
822
+ }).split("import * as gqlTada from 'gql.tada';")[0];
823
+ }
824
+ });
825
+ const gqlTadaTypesPath = resolveImport("gql.tada").replace(/\.[mc]?js$/, "");
826
+ const graphqlTemplate = addTemplate({
827
+ filename: "cms/graphql.ts",
828
+ write: true,
829
+ getContents: () => [
830
+ `import type { initGraphQLTada, ResultOf, VariablesOf } from '${gqlTadaTypesPath}'`,
831
+ `import type { introspection } from './graphql-env'`,
832
+ ``,
833
+ `export type CmsGraphql = initGraphQLTada<{`,
834
+ ` introspection: introspection`,
835
+ ` scalars: {`,
836
+ ` JSON: unknown`,
837
+ ` }`,
838
+ `}>`,
839
+ ``,
840
+ `declare const graphql: CmsGraphql`,
841
+ ``,
842
+ `// @ts-ignore: gql.tada's cache overload rejects this instantiation, but the parse overload still resolves`,
843
+ `export type CmsDocument<Query extends string> = ReturnType<typeof graphql<Query, []>>`,
844
+ ``,
845
+ `export type CmsResult<Query extends string> = string extends Query`,
846
+ ` ? Record<string, unknown>`,
847
+ ` : ResultOf<CmsDocument<Query>>`,
848
+ ``,
849
+ `export type CmsVariables<Query extends string> = string extends Query`,
850
+ ` ? Record<string, unknown>`,
851
+ ` : VariablesOf<CmsDocument<Query>>`,
852
+ ``
853
+ ].join("\n")
854
+ });
855
+ nuxt.options.alias["#cms-graphql"] = graphqlTemplate.dst;
856
+ }
635
857
  const module$1 = defineNuxtModule({
636
858
  meta: {
637
859
  name: "@xleddyl/nuxt-cms",
@@ -676,11 +898,18 @@ const module$1 = defineNuxtModule({
676
898
  const logger = useLogger("nuxt-cms");
677
899
  const resolved = resolveModuleOptions(options);
678
900
  if (!resolveCmsEnabled(options.enabled, process.env[CMS_ENABLED_ENV])) {
679
- const stub = resolver.resolve("./runtime/app/composables/cms-query-disabled");
901
+ const queryStub = resolver.resolve("./runtime/app/composables/cms-query-disabled");
902
+ const entryStub = resolver.resolve("./runtime/app/composables/cms-entry-disabled");
680
903
  addImports([
681
- { name: "useCms", from: stub },
682
- { name: "$cmsQuery", from: stub }
904
+ { name: "useCms", from: queryStub },
905
+ { name: "$cmsQuery", from: queryStub },
906
+ { name: "useCmsSingle", from: entryStub },
907
+ { name: "useCmsCollection", from: entryStub }
683
908
  ]);
909
+ addCmsTypeTemplates(
910
+ nuxt,
911
+ await loadCmsConfig(nuxt, resolver, resolved.configPath, resolved.i18n, logger)
912
+ );
684
913
  nuxt.options.runtimeConfig.public.cms = {
685
914
  mediaBaseUrl: resolved.media.publicBaseUrl,
686
915
  mediaStorage: resolved.media.storage,
@@ -688,7 +917,7 @@ const module$1 = defineNuxtModule({
688
917
  i18n: resolved.i18n
689
918
  };
690
919
  logger.info(
691
- "[nuxt-cms] disabled: registering no-op useCms/$cmsQuery stubs, skipping admin, server and database setup"
920
+ "[nuxt-cms] disabled: registering no-op query composables and generated types, skipping admin, server and database setup"
692
921
  );
693
922
  return;
694
923
  }
@@ -713,38 +942,13 @@ const module$1 = defineNuxtModule({
713
942
  "[nuxt-cms] media.storage is 's3' with credentials configured but no publicBaseUrl (media.publicBaseUrl or NUXT_PUBLIC_CMS_MEDIA_BASE_URL); uploaded media URLs will be null."
714
943
  );
715
944
  }
716
- const configPath = await resolvePath(resolved.configPath, { cwd: nuxt.options.rootDir });
717
- nuxt.options.alias["#nuxt-cms"] = resolver.resolve("./runtime/shared/index");
718
- nuxt.options.watch.push(configPath);
719
- let cmsConfig = {};
720
- if (existsSync(configPath)) {
721
- nuxt.options.alias["#cms-config"] = configPath;
722
- const jiti = createJiti(import.meta.url, {
723
- moduleCache: false,
724
- alias: { "#nuxt-cms": resolver.resolve("./runtime/shared/index") }
725
- });
726
- cmsConfig = await jiti.import(configPath, { default: true });
727
- } else {
728
- logger.warn(
729
- `[nuxt-cms] Config file not found: ${configPath}. Using an empty registry \u2014 create a ${resolved.configPath}.ts with defineCmsConfig().`
730
- );
731
- nuxt.options.alias["#cms-config"] = resolver.resolve("./runtime/shared/empty-config");
732
- }
733
- const configErrors = validateConfig(cmsConfig, resolved.i18n);
734
- if (configErrors.length) {
735
- for (const error of configErrors) logger.error(error);
736
- throw new Error(
737
- `[nuxt-cms] Invalid cms config (${configErrors.length} error${configErrors.length > 1 ? "s" : ""})`
738
- );
739
- }
740
- const moduleRequire = createRequire(import.meta.url);
741
- const resolveImport = (specifier) => {
742
- try {
743
- return fileURLToPath(import.meta.resolve(specifier)).replace(/\\/g, "/");
744
- } catch {
745
- return moduleRequire.resolve(specifier).replace(/\\/g, "/");
746
- }
747
- };
945
+ const cmsConfig = await loadCmsConfig(
946
+ nuxt,
947
+ resolver,
948
+ resolved.configPath,
949
+ resolved.i18n,
950
+ logger
951
+ );
748
952
  const schemaTemplate = addTemplate({
749
953
  filename: "cms/schema.ts",
750
954
  write: true,
@@ -755,64 +959,14 @@ const module$1 = defineNuxtModule({
755
959
  )
756
960
  });
757
961
  nuxt.options.alias["#cms-tables"] = schemaTemplate.dst;
758
- addTemplate({
759
- filename: "cms/schema.graphql",
760
- write: true,
761
- getContents: () => renderGraphqlSdl(cmsConfig)
762
- });
763
- const typesTemplate = addTemplate({
764
- filename: "cms/types.ts",
765
- write: true,
766
- getContents: () => renderTypesFile(cmsConfig)
767
- });
768
- nuxt.options.alias["#cms-types"] = typesTemplate.dst;
769
- addTemplate({
770
- filename: "cms/graphql-env.d.ts",
771
- write: true,
772
- getContents: () => {
773
- const introspection = minifyIntrospection(
774
- introspectionFromSchema(buildSchema(renderGraphqlSdl(cmsConfig)))
775
- );
776
- return outputIntrospectionFile(introspection, {
777
- fileType: ".d.ts",
778
- shouldPreprocess: true
779
- }).split("import * as gqlTada from 'gql.tada';")[0];
780
- }
781
- });
782
- const gqlTadaTypesPath = resolveImport("gql.tada").replace(/\.[mc]?js$/, "");
783
- const graphqlTemplate = addTemplate({
784
- filename: "cms/graphql.ts",
785
- write: true,
786
- getContents: () => [
787
- `import type { initGraphQLTada, ResultOf, VariablesOf } from '${gqlTadaTypesPath}'`,
788
- `import type { introspection } from './graphql-env'`,
789
- ``,
790
- `export type CmsGraphql = initGraphQLTada<{`,
791
- ` introspection: introspection`,
792
- ` scalars: {`,
793
- ` JSON: unknown`,
794
- ` }`,
795
- `}>`,
796
- ``,
797
- `declare const graphql: CmsGraphql`,
798
- ``,
799
- `// @ts-ignore: gql.tada's cache overload rejects this instantiation, but the parse overload still resolves`,
800
- `export type CmsDocument<Query extends string> = ReturnType<typeof graphql<Query, []>>`,
801
- ``,
802
- `export type CmsResult<Query extends string> = string extends Query`,
803
- ` ? Record<string, unknown>`,
804
- ` : ResultOf<CmsDocument<Query>>`,
805
- ``,
806
- `export type CmsVariables<Query extends string> = string extends Query`,
807
- ` ? Record<string, unknown>`,
808
- ` : VariablesOf<CmsDocument<Query>>`,
809
- ``
810
- ].join("\n")
811
- });
812
- nuxt.options.alias["#cms-graphql"] = graphqlTemplate.dst;
962
+ addCmsTypeTemplates(nuxt, cmsConfig);
963
+ const queryComposables = resolver.resolve("./runtime/app/composables/cms-query");
964
+ const entryComposables = resolver.resolve("./runtime/app/composables/cms-entry");
813
965
  addImports([
814
- { name: "useCms", from: resolver.resolve("./runtime/app/composables/cms-query") },
815
- { name: "$cmsQuery", from: resolver.resolve("./runtime/app/composables/cms-query") }
966
+ { name: "useCms", from: queryComposables },
967
+ { name: "$cmsQuery", from: queryComposables },
968
+ { name: "useCmsSingle", from: entryComposables },
969
+ { name: "useCmsCollection", from: entryComposables }
816
970
  ]);
817
971
  const {
818
972
  driver,
@@ -0,0 +1,19 @@
1
+ import type { AsyncData } from 'nuxt/app';
2
+ import type { CmsCollectionName, CmsCollectionTypes, CmsSingleName, CmsSingleTypes } from '#cms-types';
3
+ import type { CmsAsyncDataOptions } from './cms-query-disabled.js';
4
+ export interface CmsSortInput<Entry> {
5
+ field: Extract<keyof Entry, string>;
6
+ direction?: 'asc' | 'desc';
7
+ }
8
+ export interface CmsSingleOptions<ResT, DefaultT> extends CmsAsyncDataOptions<ResT, DefaultT> {
9
+ locale?: string;
10
+ }
11
+ export interface CmsCollectionOptions<ResT, DefaultT, Entry> extends CmsAsyncDataOptions<ResT, DefaultT> {
12
+ locale?: string;
13
+ filters?: Record<string, unknown>;
14
+ sort?: CmsSortInput<Entry>[];
15
+ limit?: number;
16
+ offset?: number;
17
+ }
18
+ export declare function useCmsSingle<K extends CmsSingleName, DefaultT = null>(name: K, options?: CmsSingleOptions<CmsSingleTypes[K] | null, DefaultT>): AsyncData<CmsSingleTypes[K] | DefaultT | null, Error | undefined>;
19
+ export declare function useCmsCollection<K extends CmsCollectionName, DefaultT = CmsCollectionTypes[K][]>(name: K, options?: CmsCollectionOptions<CmsCollectionTypes[K][], DefaultT, CmsCollectionTypes[K]>): AsyncData<CmsCollectionTypes[K][] | DefaultT, Error | undefined>;
@@ -0,0 +1,16 @@
1
+ import { useAsyncData } from "#imports";
2
+ export function useCmsSingle(name, options = {}) {
3
+ const { locale, key, default: fallback } = options;
4
+ return useAsyncData(
5
+ key ?? `cms-single:${String(name)}:${locale ?? ""}`,
6
+ async () => fallback ? fallback() : null
7
+ );
8
+ }
9
+ export function useCmsCollection(name, options = {}) {
10
+ const { locale, filters, sort, limit, offset, key, default: fallback } = options;
11
+ const variables = { locale, filters, sort, limit, offset };
12
+ return useAsyncData(
13
+ key ?? `cms-collection:${String(name)}:${JSON.stringify(variables)}`,
14
+ async () => fallback ? fallback() : []
15
+ );
16
+ }
@@ -0,0 +1,19 @@
1
+ import type { AsyncData } from 'nuxt/app';
2
+ import type { CmsCollectionName, CmsCollectionTypes, CmsSingleName, CmsSingleTypes } from '#cms-types';
3
+ import type { CmsAsyncDataOptions } from './cms-query.js';
4
+ export interface CmsSortInput<Entry> {
5
+ field: Extract<keyof Entry, string>;
6
+ direction?: 'asc' | 'desc';
7
+ }
8
+ export interface CmsSingleOptions<ResT, DefaultT> extends CmsAsyncDataOptions<ResT, DefaultT> {
9
+ locale?: string;
10
+ }
11
+ export interface CmsCollectionOptions<ResT, DefaultT, Entry> extends CmsAsyncDataOptions<ResT, DefaultT> {
12
+ locale?: string;
13
+ filters?: Record<string, unknown>;
14
+ sort?: CmsSortInput<Entry>[];
15
+ limit?: number;
16
+ offset?: number;
17
+ }
18
+ export declare function useCmsSingle<K extends CmsSingleName, DefaultT = null>(name: K, options?: CmsSingleOptions<CmsSingleTypes[K] | null, DefaultT>): AsyncData<CmsSingleTypes[K] | DefaultT | null, Error | undefined>;
19
+ export declare function useCmsCollection<K extends CmsCollectionName, DefaultT = CmsCollectionTypes[K][]>(name: K, options?: CmsCollectionOptions<CmsCollectionTypes[K][], DefaultT, CmsCollectionTypes[K]>): AsyncData<CmsCollectionTypes[K][] | DefaultT, Error | undefined>;
@@ -0,0 +1,50 @@
1
+ import { cmsCollectionQueries, cmsSingleQueries } from "#cms-queries";
2
+ import { useAsyncData } from "#imports";
3
+ import { $cmsQuery } from "./cms-query.js";
4
+ function unknownEntry(name) {
5
+ throw new Error(`[nuxt-cms] no generated query for "${name}"; check cms.config.ts`);
6
+ }
7
+ export function useCmsSingle(name, options = {}) {
8
+ const { locale, key, default: fallback, ...asyncDataOptions } = options;
9
+ const entryName = String(name);
10
+ const query = cmsSingleQueries[entryName];
11
+ return useAsyncData(
12
+ key ?? `cms-single:${entryName}:${locale ?? ""}`,
13
+ async () => {
14
+ if (!query) unknownEntry(entryName);
15
+ const result = await $cmsQuery(query, { locale });
16
+ return result?.[entryName] ?? null;
17
+ },
18
+ {
19
+ default: fallback ?? (() => null),
20
+ ...asyncDataOptions
21
+ }
22
+ );
23
+ }
24
+ export function useCmsCollection(name, options = {}) {
25
+ const {
26
+ locale,
27
+ filters,
28
+ sort,
29
+ limit,
30
+ offset,
31
+ key,
32
+ default: fallback,
33
+ ...asyncDataOptions
34
+ } = options;
35
+ const entryName = String(name);
36
+ const query = cmsCollectionQueries[entryName];
37
+ const variables = { locale, filters, sort, limit, offset };
38
+ return useAsyncData(
39
+ key ?? `cms-collection:${entryName}:${JSON.stringify(variables)}`,
40
+ async () => {
41
+ if (!query) unknownEntry(entryName);
42
+ const result = await $cmsQuery(query, variables);
43
+ return result?.[entryName] ?? [];
44
+ },
45
+ {
46
+ default: fallback ?? (() => []),
47
+ ...asyncDataOptions
48
+ }
49
+ );
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xleddyl/nuxt-cms",
3
- "version": "0.1.41",
3
+ "version": "0.1.42",
4
4
  "description": "Lightweight CMS that ships with your Nuxt app: runs on the Nitro server, content types defined in code, /cms admin panel, GraphQL API, SQLite or Postgres. No external CMS needed!",
5
5
  "license": "MIT",
6
6
  "author": "Edoardo Alberti (https://github.com/xleddyl)",