@strapi2front/generators 0.4.0 → 0.5.0

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/index.js CHANGED
@@ -1476,6 +1476,303 @@ ${findParams.join("\n")}
1476
1476
  };
1477
1477
  `;
1478
1478
  }
1479
+
1480
+ // src/shared/blocks-schema.ts
1481
+ function getCompactBlocksSchema() {
1482
+ return `z.array(
1483
+ z.discriminatedUnion('type', [
1484
+ z.object({ type: z.literal('paragraph'), children: z.array(z.object({ type: z.literal('text'), text: z.string() }).passthrough()) }),
1485
+ z.object({ type: z.literal('heading'), level: z.number().int().min(1).max(6), children: z.array(z.object({ type: z.literal('text'), text: z.string() }).passthrough()) }),
1486
+ z.object({ type: z.literal('list'), format: z.enum(['ordered', 'unordered']), children: z.array(z.object({ type: z.literal('list-item'), children: z.array(z.unknown()) })) }),
1487
+ z.object({ type: z.literal('quote'), children: z.array(z.object({ type: z.literal('text'), text: z.string() }).passthrough()) }),
1488
+ z.object({ type: z.literal('code'), children: z.array(z.object({ type: z.literal('text'), text: z.string() })) }),
1489
+ z.object({ type: z.literal('image'), image: z.object({ url: z.string(), width: z.number(), height: z.number() }).passthrough(), children: z.array(z.unknown()) }),
1490
+ ])
1491
+ )`;
1492
+ }
1493
+
1494
+ // src/shared/zod-mapper.ts
1495
+ var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
1496
+ "id",
1497
+ "documentId",
1498
+ "createdAt",
1499
+ "updatedAt",
1500
+ "publishedAt",
1501
+ "createdBy",
1502
+ "updatedBy",
1503
+ "localizations",
1504
+ "locale"
1505
+ ]);
1506
+ function isSystemField(fieldName) {
1507
+ return SYSTEM_FIELDS.has(fieldName);
1508
+ }
1509
+ function mapAttributeToZodSchema(attr, options = {}) {
1510
+ const { isUpdate = false } = options;
1511
+ let schema = buildBaseSchema(attr, options);
1512
+ if (!schema) {
1513
+ return { schema: "", skip: true, skipReason: "Unsupported type" };
1514
+ }
1515
+ if (isUpdate || !attr.required) {
1516
+ schema = `${schema}.optional()`;
1517
+ }
1518
+ if (!isUpdate && attr.default !== void 0 && attr.default !== null) {
1519
+ const defaultValue = formatDefaultValue(attr.default, attr.type);
1520
+ if (defaultValue !== null) {
1521
+ schema = `${schema}.default(${defaultValue})`;
1522
+ }
1523
+ }
1524
+ return { schema, skip: false };
1525
+ }
1526
+ function buildBaseSchema(attr, options) {
1527
+ switch (attr.type) {
1528
+ // String types
1529
+ case "string":
1530
+ case "text":
1531
+ case "richtext":
1532
+ case "uid":
1533
+ return buildStringSchema(attr);
1534
+ case "email":
1535
+ return buildEmailSchema(attr);
1536
+ case "password":
1537
+ return buildPasswordSchema(attr);
1538
+ // Blocks (Strapi v5 rich text)
1539
+ // Uses structured schema with all block types: paragraph, heading, list, quote, code, image
1540
+ case "blocks":
1541
+ return getCompactBlocksSchema();
1542
+ // Number types
1543
+ case "integer":
1544
+ case "biginteger":
1545
+ return buildIntegerSchema(attr);
1546
+ case "float":
1547
+ case "decimal":
1548
+ return buildFloatSchema(attr);
1549
+ // Boolean
1550
+ case "boolean":
1551
+ return "z.boolean()";
1552
+ // Date types
1553
+ case "date":
1554
+ return "z.string().date()";
1555
+ case "time":
1556
+ return "z.string().time()";
1557
+ case "datetime":
1558
+ case "timestamp":
1559
+ return "z.string().datetime({ offset: true })";
1560
+ // JSON
1561
+ case "json":
1562
+ return "z.record(z.unknown())";
1563
+ // Enumeration
1564
+ case "enumeration":
1565
+ return buildEnumSchema(attr);
1566
+ // Media - accepts file IDs (numbers) from upload API
1567
+ case "media":
1568
+ return buildMediaSchema(attr);
1569
+ // Relation - accepts documentIds (v5) or ids (v4)
1570
+ case "relation":
1571
+ return buildRelationSchema(attr, options);
1572
+ // Component
1573
+ case "component":
1574
+ return buildComponentSchema(attr, options);
1575
+ // Dynamic zone
1576
+ case "dynamiczone":
1577
+ return buildDynamicZoneSchema(attr, options);
1578
+ default:
1579
+ return "z.unknown()";
1580
+ }
1581
+ }
1582
+ function buildStringSchema(attr) {
1583
+ const parts = ["z.string()"];
1584
+ if (attr.minLength !== void 0) {
1585
+ parts.push(`.min(${attr.minLength})`);
1586
+ }
1587
+ if (attr.maxLength !== void 0) {
1588
+ parts.push(`.max(${attr.maxLength})`);
1589
+ }
1590
+ if (attr.regex) {
1591
+ const escapedRegex = attr.regex.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
1592
+ parts.push(`.regex(new RegExp('${escapedRegex}'))`);
1593
+ }
1594
+ return parts.join("");
1595
+ }
1596
+ function buildEmailSchema(attr) {
1597
+ let schema = "z.string().email()";
1598
+ if (attr.minLength !== void 0) {
1599
+ schema += `.min(${attr.minLength})`;
1600
+ }
1601
+ if (attr.maxLength !== void 0) {
1602
+ schema += `.max(${attr.maxLength})`;
1603
+ }
1604
+ return schema;
1605
+ }
1606
+ function buildPasswordSchema(attr) {
1607
+ let schema = "z.string()";
1608
+ const minLength = attr.minLength ?? 6;
1609
+ schema += `.min(${minLength})`;
1610
+ if (attr.maxLength !== void 0) {
1611
+ schema += `.max(${attr.maxLength})`;
1612
+ }
1613
+ return schema;
1614
+ }
1615
+ function buildIntegerSchema(attr) {
1616
+ const parts = ["z.number().int()"];
1617
+ if (attr.min !== void 0) {
1618
+ parts.push(`.min(${attr.min})`);
1619
+ }
1620
+ if (attr.max !== void 0) {
1621
+ parts.push(`.max(${attr.max})`);
1622
+ }
1623
+ return parts.join("");
1624
+ }
1625
+ function buildFloatSchema(attr) {
1626
+ const parts = ["z.number()"];
1627
+ if (attr.min !== void 0) {
1628
+ parts.push(`.min(${attr.min})`);
1629
+ }
1630
+ if (attr.max !== void 0) {
1631
+ parts.push(`.max(${attr.max})`);
1632
+ }
1633
+ return parts.join("");
1634
+ }
1635
+ function buildEnumSchema(attr) {
1636
+ if (!attr.enum || attr.enum.length === 0) {
1637
+ return "z.string()";
1638
+ }
1639
+ const enumValues = attr.enum.map((v) => `'${v}'`).join(", ");
1640
+ return `z.enum([${enumValues}])`;
1641
+ }
1642
+ function buildMediaSchema(attr) {
1643
+ if (attr.multiple) {
1644
+ return "z.array(z.number().int().positive()).default([])";
1645
+ }
1646
+ return "z.number().int().positive().nullable()";
1647
+ }
1648
+ function buildRelationSchema(attr, options = {}) {
1649
+ const isMany = attr.relation === "oneToMany" || attr.relation === "manyToMany";
1650
+ const isV5 = options.strapiVersion !== "v4";
1651
+ const useAdvanced = options.useAdvancedRelations && isV5;
1652
+ const idSchema = isV5 ? "z.string()" : "z.number().int().positive()";
1653
+ if (useAdvanced) {
1654
+ const positionSchema = `z.object({
1655
+ before: z.string().optional(),
1656
+ after: z.string().optional(),
1657
+ start: z.boolean().optional(),
1658
+ end: z.boolean().optional(),
1659
+ }).optional()`;
1660
+ const relationItemSchema = `z.object({
1661
+ documentId: ${idSchema},
1662
+ /** Locale for i18n content types */
1663
+ locale: z.string().optional(),
1664
+ /** Target draft or published version */
1665
+ status: z.enum(['draft', 'published']).optional(),
1666
+ /** Position for ordering */
1667
+ position: ${positionSchema},
1668
+ })`;
1669
+ const itemOrIdSchema = `z.union([${idSchema}, ${relationItemSchema}])`;
1670
+ const disconnectItemSchema = `z.union([
1671
+ ${idSchema},
1672
+ z.object({
1673
+ documentId: ${idSchema},
1674
+ locale: z.string().optional(),
1675
+ status: z.enum(['draft', 'published']).optional(),
1676
+ })
1677
+ ])`;
1678
+ return `z.union([
1679
+ // Shorthand: array of documentIds (equivalent to set)
1680
+ z.array(${idSchema}),
1681
+ // Longhand: object with connect/disconnect/set
1682
+ z.object({
1683
+ /** Add relations while preserving existing ones */
1684
+ connect: z.array(${itemOrIdSchema}).optional(),
1685
+ /** Remove specific relations */
1686
+ disconnect: z.array(${disconnectItemSchema}).optional(),
1687
+ /** Replace ALL relations (cannot combine with connect/disconnect) */
1688
+ set: z.array(${itemOrIdSchema}).optional(),
1689
+ })
1690
+ ]).optional()`;
1691
+ }
1692
+ if (isMany) {
1693
+ return `z.array(${idSchema}).default([])`;
1694
+ }
1695
+ return `${idSchema}.nullable()`;
1696
+ }
1697
+ function buildComponentSchema(attr, options = {}) {
1698
+ const schemaName = options.componentSchemaNames?.get(attr.component);
1699
+ if (schemaName) {
1700
+ if (attr.repeatable) {
1701
+ return `z.array(${schemaName})`;
1702
+ }
1703
+ return `${schemaName}.nullable()`;
1704
+ }
1705
+ if (attr.repeatable) {
1706
+ return "z.array(z.record(z.unknown()))";
1707
+ }
1708
+ return "z.record(z.unknown()).nullable()";
1709
+ }
1710
+ function buildDynamicZoneSchema(attr, options = {}) {
1711
+ if (options.componentSchemaNames && attr.components && attr.components.length > 0) {
1712
+ const members = attr.components.map((uid) => {
1713
+ const schemaName = options.componentSchemaNames.get(uid);
1714
+ if (!schemaName) return null;
1715
+ return `${schemaName}.extend({ __component: z.literal('${uid}') })`;
1716
+ }).filter(Boolean);
1717
+ if (members.length > 0) {
1718
+ return `z.array(
1719
+ z.discriminatedUnion('__component', [
1720
+ ${members.join(",\n ")},
1721
+ ])
1722
+ )`;
1723
+ }
1724
+ }
1725
+ return "z.array(z.record(z.unknown()))";
1726
+ }
1727
+ function formatDefaultValue(value, type) {
1728
+ if (value === null || value === void 0) {
1729
+ return null;
1730
+ }
1731
+ switch (type) {
1732
+ case "string":
1733
+ case "text":
1734
+ case "richtext":
1735
+ case "email":
1736
+ case "uid":
1737
+ return typeof value === "string" ? `'${value.replace(/'/g, "\\'")}'` : null;
1738
+ case "integer":
1739
+ case "biginteger":
1740
+ case "float":
1741
+ case "decimal":
1742
+ return typeof value === "number" ? String(value) : null;
1743
+ case "boolean":
1744
+ return typeof value === "boolean" ? String(value) : null;
1745
+ case "enumeration":
1746
+ return typeof value === "string" ? `'${value}'` : null;
1747
+ default:
1748
+ return null;
1749
+ }
1750
+ }
1751
+ function generateZodObjectSchema(attributes, options = {}) {
1752
+ const fields = [];
1753
+ const skippedFields = [];
1754
+ for (const [name, attr] of Object.entries(attributes)) {
1755
+ if (isSystemField(name)) {
1756
+ continue;
1757
+ }
1758
+ if (attr.private) {
1759
+ skippedFields.push({ name, reason: "Private field" });
1760
+ continue;
1761
+ }
1762
+ const mapped = mapAttributeToZodSchema(attr, options);
1763
+ if (mapped.skip) {
1764
+ skippedFields.push({ name, reason: mapped.skipReason || "Unknown" });
1765
+ continue;
1766
+ }
1767
+ fields.push(` ${name}: ${mapped.schema},`);
1768
+ }
1769
+ const schema = `z.object({
1770
+ ${fields.join("\n")}
1771
+ })`;
1772
+ return { schema, skippedFields };
1773
+ }
1774
+
1775
+ // src/frameworks/astro/actions.ts
1479
1776
  function isAstroActionsSupported(astroVersion) {
1480
1777
  if (!astroVersion) return false;
1481
1778
  const match = astroVersion.replace(/^[\^~]/, "").match(/^(\d+)/);
@@ -1483,26 +1780,26 @@ function isAstroActionsSupported(astroVersion) {
1483
1780
  return majorVersion !== null && majorVersion >= 4;
1484
1781
  }
1485
1782
  async function generateAstroActions(schema, options) {
1486
- const { outputDir, servicesImportPath, strapiVersion = "v5" } = options;
1783
+ const { outputDir, servicesImportPath, strapiVersion = "v5", useTypedSchemas = true } = options;
1487
1784
  const generatedFiles = [];
1488
1785
  await ensureDir(outputDir);
1489
1786
  for (const collection of schema.collections) {
1490
1787
  const fileName = `${toKebabCase(collection.singularName)}.ts`;
1491
1788
  const filePath = path9.join(outputDir, fileName);
1492
- const content = generateCollectionActions(collection, servicesImportPath, strapiVersion);
1789
+ const content = generateCollectionActions(collection, servicesImportPath, strapiVersion, useTypedSchemas);
1493
1790
  await writeFile(filePath, await formatCode(content));
1494
1791
  generatedFiles.push(filePath);
1495
1792
  }
1496
1793
  for (const single of schema.singles) {
1497
1794
  const fileName = `${toKebabCase(single.singularName)}.ts`;
1498
1795
  const filePath = path9.join(outputDir, fileName);
1499
- const content = generateSingleActions(single, servicesImportPath);
1796
+ const content = generateSingleActions(single, servicesImportPath, useTypedSchemas);
1500
1797
  await writeFile(filePath, await formatCode(content));
1501
1798
  generatedFiles.push(filePath);
1502
1799
  }
1503
1800
  return generatedFiles;
1504
1801
  }
1505
- function generateCollectionActions(collection, servicesImportPath, strapiVersion) {
1802
+ function generateCollectionActions(collection, servicesImportPath, strapiVersion, useTypedSchemas = false) {
1506
1803
  const serviceName = toCamelCase(collection.singularName) + "Service";
1507
1804
  const actionsName = toCamelCase(collection.singularName);
1508
1805
  const fileName = toKebabCase(collection.singularName);
@@ -1511,6 +1808,26 @@ function generateCollectionActions(collection, servicesImportPath, strapiVersion
1511
1808
  const idParamName = isV4 ? "id" : "documentId";
1512
1809
  const idComment = isV4 ? "id" : "documentId";
1513
1810
  const hasSlug = "slug" in collection.attributes;
1811
+ let createDataSchema = "z.record(z.unknown())";
1812
+ let updateDataSchema = "z.record(z.unknown())";
1813
+ let schemaDefinitions = "";
1814
+ if (useTypedSchemas) {
1815
+ const createResult = generateZodObjectSchema(collection.attributes, { isUpdate: false });
1816
+ const updateResult = generateZodObjectSchema(collection.attributes, { isUpdate: true });
1817
+ schemaDefinitions = `
1818
+ /**
1819
+ * Create schema for ${collection.displayName}
1820
+ */
1821
+ const createSchema = ${createResult.schema};
1822
+
1823
+ /**
1824
+ * Update schema for ${collection.displayName}
1825
+ */
1826
+ const updateSchema = ${updateResult.schema};
1827
+ `;
1828
+ createDataSchema = "createSchema";
1829
+ updateDataSchema = "updateSchema";
1830
+ }
1514
1831
  return `/**
1515
1832
  * ${collection.displayName} Actions
1516
1833
  * ${collection.description || ""}
@@ -1530,6 +1847,7 @@ const paginationSchema = z.object({
1530
1847
  page: z.number().int().positive().optional().default(1),
1531
1848
  pageSize: z.number().int().positive().max(100).optional().default(25),
1532
1849
  }).optional();
1850
+ ${schemaDefinitions}
1533
1851
 
1534
1852
  /**
1535
1853
  * ${collection.displayName} actions
@@ -1625,7 +1943,7 @@ ${hasSlug ? `
1625
1943
  */
1626
1944
  create: defineAction({
1627
1945
  input: z.object({
1628
- data: z.record(z.unknown()),
1946
+ data: ${createDataSchema},
1629
1947
  }),
1630
1948
  handler: async ({ data }) => {
1631
1949
  try {
@@ -1646,7 +1964,7 @@ ${hasSlug ? `
1646
1964
  update: defineAction({
1647
1965
  input: z.object({
1648
1966
  ${idParamName}: ${idInputSchema},
1649
- data: z.record(z.unknown()),
1967
+ data: ${updateDataSchema},
1650
1968
  }),
1651
1969
  handler: async ({ ${idParamName}, data }) => {
1652
1970
  try {
@@ -1703,10 +2021,22 @@ ${hasSlug ? `
1703
2021
  };
1704
2022
  `;
1705
2023
  }
1706
- function generateSingleActions(single, servicesImportPath) {
2024
+ function generateSingleActions(single, servicesImportPath, useTypedSchemas = false) {
1707
2025
  const serviceName = toCamelCase(single.singularName) + "Service";
1708
2026
  const actionsName = toCamelCase(single.singularName);
1709
2027
  const fileName = toKebabCase(single.singularName);
2028
+ let updateDataSchema = "z.record(z.unknown())";
2029
+ let schemaDefinitions = "";
2030
+ if (useTypedSchemas) {
2031
+ const updateResult = generateZodObjectSchema(single.attributes, { isUpdate: true });
2032
+ schemaDefinitions = `
2033
+ /**
2034
+ * Update schema for ${single.displayName}
2035
+ */
2036
+ const updateSchema = ${updateResult.schema};
2037
+ `;
2038
+ updateDataSchema = "updateSchema";
2039
+ }
1710
2040
  return `/**
1711
2041
  * ${single.displayName} Actions (Single Type)
1712
2042
  * ${single.description || ""}
@@ -1717,7 +2047,7 @@ function generateSingleActions(single, servicesImportPath) {
1717
2047
  import { defineAction, ActionError } from 'astro:actions';
1718
2048
  import { z } from 'astro:schema';
1719
2049
  import { ${serviceName} } from '${servicesImportPath}/${fileName}.service';
1720
-
2050
+ ${schemaDefinitions}
1721
2051
  /**
1722
2052
  * ${single.displayName} actions
1723
2053
  */
@@ -1758,7 +2088,7 @@ export const ${actionsName} = {
1758
2088
  */
1759
2089
  update: defineAction({
1760
2090
  input: z.object({
1761
- data: z.record(z.unknown()),
2091
+ data: ${updateDataSchema},
1762
2092
  }),
1763
2093
  handler: async ({ data }) => {
1764
2094
  try {
@@ -1806,23 +2136,20 @@ function generateV5ClientFile(apiPrefix) {
1806
2136
  return `/**
1807
2137
  * Strapi Client (v5)
1808
2138
  * Generated by strapi2front
2139
+ *
2140
+ * Using official @strapi/client
2141
+ * @see https://docs.strapi.io/cms/api/client
1809
2142
  */
1810
2143
 
1811
- import Strapi from 'strapi-sdk-js';
2144
+ import { strapi } from '@strapi/client';
1812
2145
 
1813
2146
  // Initialize the Strapi client
1814
- const strapiUrl = import.meta.env.STRAPI_URL || process.env.STRAPI_URL || 'http://localhost:1337';
1815
- const strapiToken = import.meta.env.STRAPI_TOKEN || process.env.STRAPI_TOKEN;
1816
- const strapiApiPrefix = import.meta.env.STRAPI_API_PREFIX || process.env.STRAPI_API_PREFIX || '${normalizedPrefix}';
1817
-
1818
- export const strapi = new Strapi({
1819
- url: strapiUrl,
1820
- prefix: strapiApiPrefix,
1821
- axiosOptions: {
1822
- headers: strapiToken ? {
1823
- Authorization: \`Bearer \${strapiToken}\`,
1824
- } : {},
1825
- },
2147
+ const baseURL = (import.meta.env.STRAPI_URL || process.env.STRAPI_URL || 'http://localhost:1337') + '${normalizedPrefix}';
2148
+ const authToken = import.meta.env.STRAPI_TOKEN || process.env.STRAPI_TOKEN;
2149
+
2150
+ export const strapiClient = strapi({
2151
+ baseURL,
2152
+ auth: authToken,
1826
2153
  });
1827
2154
 
1828
2155
  // Pagination type
@@ -1833,6 +2160,53 @@ export interface StrapiPagination {
1833
2160
  total: number;
1834
2161
  }
1835
2162
 
2163
+ // Media types
2164
+ export interface StrapiMediaFormat {
2165
+ name: string;
2166
+ hash: string;
2167
+ ext: string;
2168
+ mime: string;
2169
+ width: number;
2170
+ height: number;
2171
+ size: number;
2172
+ url: string;
2173
+ }
2174
+
2175
+ export interface StrapiMedia {
2176
+ id: number;
2177
+ documentId: string;
2178
+ name: string;
2179
+ alternativeText: string | null;
2180
+ caption: string | null;
2181
+ width: number;
2182
+ height: number;
2183
+ formats: {
2184
+ thumbnail?: StrapiMediaFormat;
2185
+ small?: StrapiMediaFormat;
2186
+ medium?: StrapiMediaFormat;
2187
+ large?: StrapiMediaFormat;
2188
+ } | null;
2189
+ hash: string;
2190
+ ext: string;
2191
+ mime: string;
2192
+ size: number;
2193
+ url: string;
2194
+ previewUrl: string | null;
2195
+ provider: string;
2196
+ createdAt: string;
2197
+ updatedAt: string;
2198
+ }
2199
+
2200
+ /**
2201
+ * File metadata for uploads
2202
+ * @see https://docs.strapi.io/cms/api/client#upload
2203
+ */
2204
+ export interface StrapiFileInfo {
2205
+ name?: string;
2206
+ alternativeText?: string;
2207
+ caption?: string;
2208
+ }
2209
+
1836
2210
  // Default pagination for fallback
1837
2211
  const defaultPagination: StrapiPagination = {
1838
2212
  page: 1,
@@ -1841,24 +2215,12 @@ const defaultPagination: StrapiPagination = {
1841
2215
  total: 0,
1842
2216
  };
1843
2217
 
1844
- // Response types
1845
- interface StrapiListResponse<T> {
1846
- data: T[];
1847
- meta: {
1848
- pagination?: StrapiPagination;
1849
- };
1850
- }
1851
-
1852
- interface StrapiSingleResponse<T> {
1853
- data: T;
1854
- meta?: Record<string, unknown>;
1855
- }
1856
-
1857
2218
  // Helper to get typed collection
1858
2219
  export function collection<T>(pluralName: string) {
2220
+ const col = strapiClient.collection(pluralName);
1859
2221
  return {
1860
2222
  async find(params?: Record<string, unknown>): Promise<{ data: T[]; meta: { pagination: StrapiPagination } }> {
1861
- const response = await strapi.find(pluralName, params) as unknown as StrapiListResponse<T>;
2223
+ const response = await col.find(params) as any;
1862
2224
  return {
1863
2225
  data: Array.isArray(response.data) ? response.data : [],
1864
2226
  meta: {
@@ -1867,39 +2229,68 @@ export function collection<T>(pluralName: string) {
1867
2229
  };
1868
2230
  },
1869
2231
  async findOne(documentId: string, params?: Record<string, unknown>): Promise<{ data: T }> {
1870
- const response = await strapi.findOne(pluralName, documentId, params) as unknown as StrapiSingleResponse<T>;
2232
+ const response = await col.findOne(documentId, params) as any;
1871
2233
  return { data: response.data };
1872
2234
  },
1873
- async create(data: { data: Partial<T> }): Promise<{ data: T }> {
1874
- const response = await strapi.create(pluralName, data.data as Record<string, unknown>) as unknown as StrapiSingleResponse<T>;
2235
+ async create(data: Partial<T>): Promise<{ data: T }> {
2236
+ const response = await col.create(data as any) as any;
1875
2237
  return { data: response.data };
1876
2238
  },
1877
- async update(documentId: string, data: { data: Partial<T> }): Promise<{ data: T }> {
1878
- const response = await strapi.update(pluralName, documentId, data.data as Record<string, unknown>) as unknown as StrapiSingleResponse<T>;
2239
+ async update(documentId: string, data: Partial<T>): Promise<{ data: T }> {
2240
+ const response = await col.update(documentId, data as any) as any;
1879
2241
  return { data: response.data };
1880
2242
  },
1881
2243
  async delete(documentId: string): Promise<void> {
1882
- await strapi.delete(pluralName, documentId);
2244
+ await col.delete(documentId);
1883
2245
  },
1884
2246
  };
1885
2247
  }
1886
2248
 
1887
2249
  // Helper to get typed single type
1888
2250
  export function single<T>(singularName: string) {
2251
+ const singleType = strapiClient.single(singularName);
1889
2252
  return {
1890
2253
  async find(params?: Record<string, unknown>): Promise<{ data: T }> {
1891
- const response = await strapi.find(singularName, params) as unknown as StrapiSingleResponse<T>;
2254
+ const response = await singleType.find(params) as any;
1892
2255
  return { data: response.data };
1893
2256
  },
1894
- async update(data: { data: Partial<T> }): Promise<{ data: T }> {
1895
- const response = await strapi.update(singularName, 1 as unknown as string, data.data as Record<string, unknown>) as unknown as StrapiSingleResponse<T>;
2257
+ async update(data: Partial<T>): Promise<{ data: T }> {
2258
+ const response = await singleType.update(data as any) as any;
1896
2259
  return { data: response.data };
1897
2260
  },
1898
2261
  async delete(): Promise<void> {
1899
- await strapi.delete(singularName, 1 as unknown as string);
2262
+ await singleType.delete();
1900
2263
  },
1901
2264
  };
1902
2265
  }
2266
+
2267
+ /**
2268
+ * File management helpers
2269
+ * Wraps @strapi/client file methods with proper typing
2270
+ * @see https://docs.strapi.io/cms/api/client#working-with-files
2271
+ */
2272
+ export const files = {
2273
+ async upload(file: File | Blob, options?: { fileInfo?: StrapiFileInfo }): Promise<StrapiMedia> {
2274
+ const response = await strapiClient.files.upload(file, options) as any;
2275
+ return response;
2276
+ },
2277
+ async find(params?: Record<string, unknown>): Promise<StrapiMedia[]> {
2278
+ const response = await strapiClient.files.find(params) as any;
2279
+ return Array.isArray(response) ? response : [];
2280
+ },
2281
+ async findOne(fileId: number): Promise<StrapiMedia> {
2282
+ const response = await strapiClient.files.findOne(fileId) as any;
2283
+ return response;
2284
+ },
2285
+ async update(fileId: number, fileInfo: StrapiFileInfo): Promise<StrapiMedia> {
2286
+ const response = await strapiClient.files.update(fileId, fileInfo) as any;
2287
+ return response;
2288
+ },
2289
+ async delete(fileId: number): Promise<StrapiMedia> {
2290
+ const response = await strapiClient.files.delete(fileId) as any;
2291
+ return response;
2292
+ },
2293
+ };
1903
2294
  `;
1904
2295
  }
1905
2296
  function generateV4ClientFile(apiPrefix) {
@@ -1907,23 +2298,20 @@ function generateV4ClientFile(apiPrefix) {
1907
2298
  return `/**
1908
2299
  * Strapi Client (v4)
1909
2300
  * Generated by strapi2front
2301
+ *
2302
+ * Note: @strapi/client officially supports Strapi v5+
2303
+ * This v4 client uses a compatibility layer for the nested attributes structure
1910
2304
  */
1911
2305
 
1912
- import Strapi from 'strapi-sdk-js';
2306
+ import { strapi as createStrapi } from '@strapi/client';
1913
2307
 
1914
2308
  // Initialize the Strapi client
1915
- const strapiUrl = import.meta.env.STRAPI_URL || process.env.STRAPI_URL || 'http://localhost:1337';
1916
- const strapiToken = import.meta.env.STRAPI_TOKEN || process.env.STRAPI_TOKEN;
1917
- const strapiApiPrefix = import.meta.env.STRAPI_API_PREFIX || process.env.STRAPI_API_PREFIX || '${normalizedPrefix}';
1918
-
1919
- export const strapi = new Strapi({
1920
- url: strapiUrl,
1921
- prefix: strapiApiPrefix,
1922
- axiosOptions: {
1923
- headers: strapiToken ? {
1924
- Authorization: \`Bearer \${strapiToken}\`,
1925
- } : {},
1926
- },
2309
+ const baseURL = (import.meta.env.STRAPI_URL || process.env.STRAPI_URL || 'http://localhost:1337') + '${normalizedPrefix}';
2310
+ const authToken = import.meta.env.STRAPI_TOKEN || process.env.STRAPI_TOKEN;
2311
+
2312
+ export const strapiClient = createStrapi({
2313
+ baseURL,
2314
+ auth: authToken,
1927
2315
  });
1928
2316
 
1929
2317
  // Pagination type
@@ -1934,6 +2322,52 @@ export interface StrapiPagination {
1934
2322
  total: number;
1935
2323
  }
1936
2324
 
2325
+ // Media types
2326
+ export interface StrapiMediaFormat {
2327
+ name: string;
2328
+ hash: string;
2329
+ ext: string;
2330
+ mime: string;
2331
+ width: number;
2332
+ height: number;
2333
+ size: number;
2334
+ url: string;
2335
+ }
2336
+
2337
+ export interface StrapiMedia {
2338
+ id: number;
2339
+ name: string;
2340
+ alternativeText: string | null;
2341
+ caption: string | null;
2342
+ width: number;
2343
+ height: number;
2344
+ formats: {
2345
+ thumbnail?: StrapiMediaFormat;
2346
+ small?: StrapiMediaFormat;
2347
+ medium?: StrapiMediaFormat;
2348
+ large?: StrapiMediaFormat;
2349
+ } | null;
2350
+ hash: string;
2351
+ ext: string;
2352
+ mime: string;
2353
+ size: number;
2354
+ url: string;
2355
+ previewUrl: string | null;
2356
+ provider: string;
2357
+ createdAt: string;
2358
+ updatedAt: string;
2359
+ }
2360
+
2361
+ /**
2362
+ * File metadata for uploads
2363
+ * @see https://docs.strapi.io/cms/api/client#upload
2364
+ */
2365
+ export interface StrapiFileInfo {
2366
+ name?: string;
2367
+ alternativeText?: string;
2368
+ caption?: string;
2369
+ }
2370
+
1937
2371
  // Default pagination for fallback
1938
2372
  const defaultPagination: StrapiPagination = {
1939
2373
  page: 1,
@@ -1948,18 +2382,6 @@ interface StrapiV4RawItem<T> {
1948
2382
  attributes: Omit<T, 'id'>;
1949
2383
  }
1950
2384
 
1951
- interface StrapiV4RawListResponse<T> {
1952
- data: StrapiV4RawItem<T>[];
1953
- meta: {
1954
- pagination?: StrapiPagination;
1955
- };
1956
- }
1957
-
1958
- interface StrapiV4RawSingleResponse<T> {
1959
- data: StrapiV4RawItem<T>;
1960
- meta?: Record<string, unknown>;
1961
- }
1962
-
1963
2385
  /**
1964
2386
  * Flatten a Strapi v4 response item (merges id with attributes)
1965
2387
  */
@@ -2003,55 +2425,83 @@ function flattenRelations<T>(data: T): T {
2003
2425
  return data;
2004
2426
  }
2005
2427
 
2006
- // Helper to get typed collection
2428
+ // Helper to get typed collection (v4 compatibility wrapper)
2007
2429
  export function collection<T>(pluralName: string) {
2430
+ const col = strapiClient.collection(pluralName);
2008
2431
  return {
2009
2432
  async find(params?: Record<string, unknown>): Promise<{ data: T[]; meta: { pagination: StrapiPagination } }> {
2010
- const response = await strapi.find(pluralName, params) as unknown as StrapiV4RawListResponse<T>;
2433
+ const response = await col.find(params) as any;
2011
2434
  const flattenedData = Array.isArray(response.data)
2012
- ? response.data.map(item => flattenRelations(flattenItem<T>(item)))
2435
+ ? response.data.map((item: StrapiV4RawItem<T>) => flattenRelations(flattenItem<T>(item)))
2013
2436
  : [];
2014
2437
  return {
2015
2438
  data: flattenedData,
2016
- meta: {
2017
- pagination: response.meta?.pagination || defaultPagination,
2018
- },
2439
+ meta: { pagination: response.meta?.pagination || defaultPagination },
2019
2440
  };
2020
2441
  },
2021
2442
  async findOne(id: number | string, params?: Record<string, unknown>): Promise<{ data: T }> {
2022
- const response = await strapi.findOne(pluralName, String(id), params) as unknown as StrapiV4RawSingleResponse<T>;
2443
+ const response = await col.findOne(String(id), params) as any;
2023
2444
  return { data: flattenRelations(flattenItem<T>(response.data)) };
2024
2445
  },
2025
- async create(data: { data: Partial<T> }): Promise<{ data: T }> {
2026
- const response = await strapi.create(pluralName, data.data as Record<string, unknown>) as unknown as StrapiV4RawSingleResponse<T>;
2446
+ async create(data: Partial<T>): Promise<{ data: T }> {
2447
+ const response = await col.create(data as any) as any;
2027
2448
  return { data: flattenRelations(flattenItem<T>(response.data)) };
2028
2449
  },
2029
- async update(id: number | string, data: { data: Partial<T> }): Promise<{ data: T }> {
2030
- const response = await strapi.update(pluralName, String(id), data.data as Record<string, unknown>) as unknown as StrapiV4RawSingleResponse<T>;
2450
+ async update(id: number | string, data: Partial<T>): Promise<{ data: T }> {
2451
+ const response = await col.update(String(id), data as any) as any;
2031
2452
  return { data: flattenRelations(flattenItem<T>(response.data)) };
2032
2453
  },
2033
2454
  async delete(id: number | string): Promise<void> {
2034
- await strapi.delete(pluralName, String(id));
2455
+ await col.delete(String(id));
2035
2456
  },
2036
2457
  };
2037
2458
  }
2038
2459
 
2039
- // Helper to get typed single type
2460
+ // Helper to get typed single type (v4 compatibility wrapper)
2040
2461
  export function single<T>(singularName: string) {
2462
+ const singleType = strapiClient.single(singularName);
2041
2463
  return {
2042
2464
  async find(params?: Record<string, unknown>): Promise<{ data: T }> {
2043
- const response = await strapi.find(singularName, params) as unknown as StrapiV4RawSingleResponse<T>;
2465
+ const response = await singleType.find(params) as any;
2044
2466
  return { data: flattenRelations(flattenItem<T>(response.data)) };
2045
2467
  },
2046
- async update(data: { data: Partial<T> }): Promise<{ data: T }> {
2047
- const response = await strapi.update(singularName, 1 as unknown as string, data.data as Record<string, unknown>) as unknown as StrapiV4RawSingleResponse<T>;
2468
+ async update(data: Partial<T>): Promise<{ data: T }> {
2469
+ const response = await singleType.update(data as any) as any;
2048
2470
  return { data: flattenRelations(flattenItem<T>(response.data)) };
2049
2471
  },
2050
2472
  async delete(): Promise<void> {
2051
- await strapi.delete(singularName, 1 as unknown as string);
2473
+ await singleType.delete();
2052
2474
  },
2053
2475
  };
2054
2476
  }
2477
+
2478
+ /**
2479
+ * File management helpers
2480
+ * Wraps @strapi/client file methods with proper typing
2481
+ * @see https://docs.strapi.io/cms/api/client#working-with-files
2482
+ */
2483
+ export const files = {
2484
+ async upload(file: File | Blob, options?: { fileInfo?: StrapiFileInfo }): Promise<StrapiMedia> {
2485
+ const response = await strapiClient.files.upload(file, options) as any;
2486
+ return response;
2487
+ },
2488
+ async find(params?: Record<string, unknown>): Promise<StrapiMedia[]> {
2489
+ const response = await strapiClient.files.find(params) as any;
2490
+ return Array.isArray(response) ? response : [];
2491
+ },
2492
+ async findOne(fileId: number): Promise<StrapiMedia> {
2493
+ const response = await strapiClient.files.findOne(fileId) as any;
2494
+ return response;
2495
+ },
2496
+ async update(fileId: number, fileInfo: StrapiFileInfo): Promise<StrapiMedia> {
2497
+ const response = await strapiClient.files.update(fileId, fileInfo) as any;
2498
+ return response;
2499
+ },
2500
+ async delete(fileId: number): Promise<StrapiMedia> {
2501
+ const response = await strapiClient.files.delete(fileId) as any;
2502
+ return response;
2503
+ },
2504
+ };
2055
2505
  `;
2056
2506
  }
2057
2507
  async function generateLocales(locales, options) {
@@ -2132,6 +2582,7 @@ async function generateByFeature(schema, locales, options) {
2132
2582
  const {
2133
2583
  outputDir,
2134
2584
  features,
2585
+ schemaOptions = {},
2135
2586
  blocksRendererInstalled = false,
2136
2587
  strapiVersion = "v5",
2137
2588
  apiPrefix = "/api",
@@ -2139,8 +2590,10 @@ async function generateByFeature(schema, locales, options) {
2139
2590
  moduleType = "commonjs"
2140
2591
  } = options;
2141
2592
  const generatedFiles = [];
2593
+ const advancedRelations = schemaOptions.advancedRelations ?? false;
2142
2594
  const useESM = outputFormat === "jsdoc" && moduleType === "esm";
2143
2595
  const ext = outputFormat === "jsdoc" ? "js" : "ts";
2596
+ const generateSchemas = features.schemas ?? outputFormat === "typescript";
2144
2597
  await ensureDir(path9.join(outputDir, "collections"));
2145
2598
  await ensureDir(path9.join(outputDir, "singles"));
2146
2599
  await ensureDir(path9.join(outputDir, "components"));
@@ -2158,6 +2611,18 @@ async function generateByFeature(schema, locales, options) {
2158
2611
  const localesContent = outputFormat === "jsdoc" ? generateLocalesFileJSDoc(locales, useESM) : generateLocalesFile2(locales);
2159
2612
  await writeFile(localesPath, await formatCode(localesContent));
2160
2613
  generatedFiles.push(localesPath);
2614
+ if (features.upload) {
2615
+ const uploadClientPath = path9.join(sharedDir, `upload-client.${ext}`);
2616
+ const uploadClientContent = outputFormat === "jsdoc" ? generateUploadClientJSDoc(useESM) : generateUploadClientTS();
2617
+ await writeFile(uploadClientPath, await formatCode(uploadClientContent));
2618
+ generatedFiles.push(uploadClientPath);
2619
+ if (features.actions && outputFormat === "typescript") {
2620
+ const uploadActionPath = path9.join(sharedDir, "upload-action.ts");
2621
+ const uploadActionContent = generateUploadActionTS();
2622
+ await writeFile(uploadActionPath, await formatCode(uploadActionContent));
2623
+ generatedFiles.push(uploadActionPath);
2624
+ }
2625
+ }
2161
2626
  for (const collection of schema.collections) {
2162
2627
  const featureDir = path9.join(outputDir, "collections", toKebabCase(collection.singularName));
2163
2628
  await ensureDir(featureDir);
@@ -2167,6 +2632,12 @@ async function generateByFeature(schema, locales, options) {
2167
2632
  await writeFile(typesPath, await formatCode(content));
2168
2633
  generatedFiles.push(typesPath);
2169
2634
  }
2635
+ if (generateSchemas) {
2636
+ const schemasPath = path9.join(featureDir, `schemas.${ext}`);
2637
+ const schemasContent = generateCollectionSchemas(collection, schema, strapiVersion, advancedRelations);
2638
+ await writeFile(schemasPath, await formatCode(schemasContent));
2639
+ generatedFiles.push(schemasPath);
2640
+ }
2170
2641
  if (features.services) {
2171
2642
  const servicePath = path9.join(featureDir, `service.${ext}`);
2172
2643
  const content = outputFormat === "jsdoc" ? generateCollectionServiceJSDoc(collection, strapiVersion, useESM) : generateCollectionService3(collection, strapiVersion);
@@ -2175,7 +2646,12 @@ async function generateByFeature(schema, locales, options) {
2175
2646
  }
2176
2647
  if (features.actions) {
2177
2648
  const actionsPath = path9.join(featureDir, `actions.${ext}`);
2178
- await writeFile(actionsPath, await formatCode(generateCollectionActions2(collection, strapiVersion)));
2649
+ const actionsContent = generateCollectionActions2(collection, {
2650
+ strapiVersion,
2651
+ useExternalSchemas: generateSchemas,
2652
+ draftAndPublish: collection.draftAndPublish
2653
+ });
2654
+ await writeFile(actionsPath, await formatCode(actionsContent));
2179
2655
  generatedFiles.push(actionsPath);
2180
2656
  }
2181
2657
  }
@@ -2188,6 +2664,12 @@ async function generateByFeature(schema, locales, options) {
2188
2664
  await writeFile(typesPath, await formatCode(content));
2189
2665
  generatedFiles.push(typesPath);
2190
2666
  }
2667
+ if (generateSchemas) {
2668
+ const schemasPath = path9.join(featureDir, `schemas.${ext}`);
2669
+ const schemasContent = generateSingleSchemas(single, schema, strapiVersion, advancedRelations);
2670
+ await writeFile(schemasPath, await formatCode(schemasContent));
2671
+ generatedFiles.push(schemasPath);
2672
+ }
2191
2673
  if (features.services) {
2192
2674
  const servicePath = path9.join(featureDir, `service.${ext}`);
2193
2675
  const content = outputFormat === "jsdoc" ? generateSingleServiceJSDoc(single, strapiVersion, useESM) : generateSingleService3(single, strapiVersion);
@@ -2197,7 +2679,7 @@ async function generateByFeature(schema, locales, options) {
2197
2679
  }
2198
2680
  for (const component of schema.components) {
2199
2681
  const componentPath = path9.join(outputDir, "components", `${toKebabCase(component.name)}.${ext}`);
2200
- const content = outputFormat === "jsdoc" ? generateComponentTypesJSDoc(component, schema, useESM) : generateComponentTypes(component, schema);
2682
+ const content = outputFormat === "jsdoc" ? generateComponentTypesJSDoc(component, schema, useESM) : generateComponentTypes(component, schema, generateSchemas, strapiVersion, advancedRelations);
2201
2683
  await writeFile(componentPath, await formatCode(content));
2202
2684
  generatedFiles.push(componentPath);
2203
2685
  }
@@ -2334,6 +2816,16 @@ export interface StrapiMediaFormat {
2334
2816
  url: string;
2335
2817
  }
2336
2818
 
2819
+ /**
2820
+ * File metadata for uploads
2821
+ * @see https://docs.strapi.io/cms/api/client#upload
2822
+ */
2823
+ export interface StrapiFileInfo {
2824
+ name?: string;
2825
+ alternativeText?: string;
2826
+ caption?: string;
2827
+ }
2828
+
2337
2829
  export interface StrapiPagination {
2338
2830
  page: number;
2339
2831
  pageSize: number;
@@ -2367,25 +2859,55 @@ function generateClient2(strapiVersion, apiPrefix = "/api") {
2367
2859
  return `/**
2368
2860
  * Strapi Client (v4)
2369
2861
  * Generated by strapi2front
2862
+ *
2863
+ * Note: @strapi/client officially supports Strapi v5+
2864
+ * This v4 client uses a compatibility layer for the nested attributes structure
2370
2865
  */
2371
2866
 
2372
- import Strapi from 'strapi-sdk-js';
2373
- import type { StrapiPagination } from './utils';
2867
+ import { strapi as createStrapi } from '@strapi/client';
2868
+ import type { StrapiPagination, StrapiMedia, StrapiFileInfo } from './utils';
2374
2869
 
2375
- // Initialize the Strapi client
2376
- const strapiUrl = import.meta.env.STRAPI_URL || process.env.STRAPI_URL || 'http://localhost:1337';
2377
- const strapiToken = import.meta.env.STRAPI_TOKEN || process.env.STRAPI_TOKEN;
2378
- const strapiApiPrefix = import.meta.env.STRAPI_API_PREFIX || process.env.STRAPI_API_PREFIX || '${normalizedPrefix}';
2379
-
2380
- export const strapi = new Strapi({
2381
- url: strapiUrl,
2382
- prefix: strapiApiPrefix,
2383
- axiosOptions: {
2384
- headers: strapiToken ? {
2385
- Authorization: \`Bearer \${strapiToken}\`,
2386
- } : {},
2387
- },
2388
- });
2870
+ // Default configuration from environment
2871
+ const defaultBaseURL = import.meta.env.STRAPI_URL || process.env.STRAPI_URL || 'http://localhost:1337';
2872
+ const defaultAuthToken = import.meta.env.STRAPI_TOKEN || process.env.STRAPI_TOKEN;
2873
+ const apiPrefix = '${normalizedPrefix}';
2874
+
2875
+ /**
2876
+ * Client options for authentication and connection
2877
+ * @beta This API is in beta and may change
2878
+ */
2879
+ export interface ClientOptions {
2880
+ /** JWT token or API token for authentication */
2881
+ authToken?: string;
2882
+ /** Base URL of the Strapi instance (without /api prefix) */
2883
+ baseURL?: string;
2884
+ }
2885
+
2886
+ /**
2887
+ * Create a configured Strapi client instance
2888
+ *
2889
+ * @param options - Optional configuration (authToken, baseURL)
2890
+ * @returns Configured Strapi client
2891
+ *
2892
+ * @example
2893
+ * // Default client (uses STRAPI_URL and STRAPI_TOKEN from env)
2894
+ * const client = createStrapiClient();
2895
+ *
2896
+ * @example
2897
+ * // Client with user JWT token
2898
+ * const userClient = createStrapiClient({ authToken: session.jwt });
2899
+ *
2900
+ * @beta This API is in beta and may change
2901
+ */
2902
+ export function createStrapiClient(options?: ClientOptions) {
2903
+ const baseURL = (options?.baseURL || defaultBaseURL) + apiPrefix;
2904
+ const auth = options?.authToken || defaultAuthToken;
2905
+
2906
+ return createStrapi({ baseURL, auth });
2907
+ }
2908
+
2909
+ // Default client instance (uses environment variables)
2910
+ export const strapiClient = createStrapiClient();
2389
2911
 
2390
2912
  // Default pagination for fallback
2391
2913
  const defaultPagination: StrapiPagination = {
@@ -2401,18 +2923,6 @@ interface StrapiV4RawItem<T> {
2401
2923
  attributes: Omit<T, 'id'>;
2402
2924
  }
2403
2925
 
2404
- interface StrapiV4RawListResponse<T> {
2405
- data: StrapiV4RawItem<T>[];
2406
- meta: {
2407
- pagination?: StrapiPagination;
2408
- };
2409
- }
2410
-
2411
- interface StrapiV4RawSingleResponse<T> {
2412
- data: StrapiV4RawItem<T>;
2413
- meta?: Record<string, unknown>;
2414
- }
2415
-
2416
2926
  /**
2417
2927
  * Flatten a Strapi v4 response item (merges id with attributes)
2418
2928
  */
@@ -2431,18 +2941,15 @@ function flattenRelations<T>(data: T): T {
2431
2941
  if (typeof data === 'object') {
2432
2942
  const result: Record<string, unknown> = {};
2433
2943
  for (const [key, value] of Object.entries(data as Record<string, unknown>)) {
2434
- // Check if this is a Strapi v4 relation response { data: { id, attributes } }
2435
2944
  if (value && typeof value === 'object' && 'data' in value) {
2436
2945
  const relationData = (value as { data: unknown }).data;
2437
2946
  if (relationData === null) {
2438
2947
  result[key] = null;
2439
2948
  } else if (Array.isArray(relationData)) {
2440
- // To-many relation
2441
2949
  result[key] = relationData.map((item: StrapiV4RawItem<unknown>) =>
2442
2950
  flattenRelations(flattenItem(item))
2443
2951
  );
2444
2952
  } else if (typeof relationData === 'object' && 'id' in relationData && 'attributes' in relationData) {
2445
- // To-one relation
2446
2953
  result[key] = flattenRelations(flattenItem(relationData as StrapiV4RawItem<unknown>));
2447
2954
  } else {
2448
2955
  result[key] = flattenRelations(value);
@@ -2456,79 +2963,187 @@ function flattenRelations<T>(data: T): T {
2456
2963
  return data;
2457
2964
  }
2458
2965
 
2459
- // Helper to get typed collection
2460
- export function collection<T>(pluralName: string) {
2966
+ /**
2967
+ * Get or create a Strapi client based on options
2968
+ * @internal
2969
+ */
2970
+ function getClient(options?: ClientOptions) {
2971
+ if (options?.authToken || options?.baseURL) {
2972
+ return createStrapiClient(options);
2973
+ }
2974
+ return strapiClient;
2975
+ }
2976
+
2977
+ /**
2978
+ * Helper to get typed collection (v4 compatibility wrapper)
2979
+ *
2980
+ * @param pluralName - The plural name of the collection (e.g., 'articles')
2981
+ * @param clientOptions - Optional client configuration
2982
+ */
2983
+ export function collection<T>(pluralName: string, clientOptions?: ClientOptions) {
2984
+ const client = getClient(clientOptions);
2985
+ const col = client.collection(pluralName);
2461
2986
  return {
2462
2987
  async find(params?: Record<string, unknown>): Promise<{ data: T[]; meta: { pagination: StrapiPagination } }> {
2463
- const response = await strapi.find(pluralName, params) as unknown as StrapiV4RawListResponse<T>;
2988
+ const response = await col.find(params) as any;
2464
2989
  const flattenedData = Array.isArray(response.data)
2465
- ? response.data.map(item => flattenRelations(flattenItem<T>(item)))
2990
+ ? response.data.map((item: StrapiV4RawItem<T>) => flattenRelations(flattenItem<T>(item)))
2466
2991
  : [];
2467
2992
  return {
2468
2993
  data: flattenedData,
2469
- meta: {
2470
- pagination: response.meta?.pagination || defaultPagination,
2471
- },
2994
+ meta: { pagination: response.meta?.pagination || defaultPagination },
2472
2995
  };
2473
2996
  },
2474
2997
  async findOne(id: number | string, params?: Record<string, unknown>): Promise<{ data: T }> {
2475
- const response = await strapi.findOne(pluralName, String(id), params) as unknown as StrapiV4RawSingleResponse<T>;
2998
+ const response = await col.findOne(String(id), params) as any;
2476
2999
  return { data: flattenRelations(flattenItem<T>(response.data)) };
2477
3000
  },
2478
- async create(data: { data: Partial<T> }): Promise<{ data: T }> {
2479
- const response = await strapi.create(pluralName, data.data as Record<string, unknown>) as unknown as StrapiV4RawSingleResponse<T>;
3001
+ async create(data: Partial<T>): Promise<{ data: T }> {
3002
+ const response = await col.create(data as any) as any;
2480
3003
  return { data: flattenRelations(flattenItem<T>(response.data)) };
2481
3004
  },
2482
- async update(id: number | string, data: { data: Partial<T> }): Promise<{ data: T }> {
2483
- const response = await strapi.update(pluralName, String(id), data.data as Record<string, unknown>) as unknown as StrapiV4RawSingleResponse<T>;
3005
+ async update(id: number | string, data: Partial<T>): Promise<{ data: T }> {
3006
+ const response = await col.update(String(id), data as any) as any;
2484
3007
  return { data: flattenRelations(flattenItem<T>(response.data)) };
2485
3008
  },
2486
3009
  async delete(id: number | string): Promise<void> {
2487
- await strapi.delete(pluralName, String(id));
3010
+ await col.delete(String(id));
2488
3011
  },
2489
3012
  };
2490
3013
  }
2491
3014
 
2492
- // Helper to get typed single type
2493
- export function single<T>(singularName: string) {
3015
+ /**
3016
+ * Helper to get typed single type (v4 compatibility wrapper)
3017
+ *
3018
+ * @param singularName - The singular name of the single type (e.g., 'homepage')
3019
+ * @param clientOptions - Optional client configuration
3020
+ */
3021
+ export function single<T>(singularName: string, clientOptions?: ClientOptions) {
3022
+ const client = getClient(clientOptions);
3023
+ const singleType = client.single(singularName);
2494
3024
  return {
2495
3025
  async find(params?: Record<string, unknown>): Promise<{ data: T }> {
2496
- const response = await strapi.find(singularName, params) as unknown as StrapiV4RawSingleResponse<T>;
3026
+ const response = await singleType.find(params) as any;
2497
3027
  return { data: flattenRelations(flattenItem<T>(response.data)) };
2498
3028
  },
2499
- async update(data: { data: Partial<T> }): Promise<{ data: T }> {
2500
- const response = await strapi.update(singularName, 1 as unknown as string, data.data as Record<string, unknown>) as unknown as StrapiV4RawSingleResponse<T>;
3029
+ async update(data: Partial<T>): Promise<{ data: T }> {
3030
+ const response = await singleType.update(data as any) as any;
2501
3031
  return { data: flattenRelations(flattenItem<T>(response.data)) };
2502
3032
  },
2503
3033
  async delete(): Promise<void> {
2504
- await strapi.delete(singularName, 1 as unknown as string);
3034
+ await singleType.delete();
2505
3035
  },
2506
3036
  };
2507
3037
  }
3038
+
3039
+ /**
3040
+ * File management helpers
3041
+ * Wraps @strapi/client file methods with proper typing
3042
+ * @see https://docs.strapi.io/cms/api/client#working-with-files
3043
+ */
3044
+ export const files = {
3045
+ /**
3046
+ * Upload a file to Strapi
3047
+ * @see https://docs.strapi.io/cms/api/client#upload
3048
+ */
3049
+ async upload(file: File | Blob, options?: { fileInfo?: StrapiFileInfo }): Promise<StrapiMedia> {
3050
+ const response = await strapiClient.files.upload(file, options) as any;
3051
+ return response;
3052
+ },
3053
+
3054
+ /**
3055
+ * Find files with optional filtering and sorting
3056
+ */
3057
+ async find(params?: Record<string, unknown>): Promise<StrapiMedia[]> {
3058
+ const response = await strapiClient.files.find(params) as any;
3059
+ return Array.isArray(response) ? response : [];
3060
+ },
3061
+
3062
+ /**
3063
+ * Get a single file by ID
3064
+ */
3065
+ async findOne(fileId: number): Promise<StrapiMedia> {
3066
+ const response = await strapiClient.files.findOne(fileId) as any;
3067
+ return response;
3068
+ },
3069
+
3070
+ /**
3071
+ * Update file metadata (name, alternativeText, caption)
3072
+ */
3073
+ async update(fileId: number, fileInfo: StrapiFileInfo): Promise<StrapiMedia> {
3074
+ const response = await strapiClient.files.update(fileId, fileInfo) as any;
3075
+ return response;
3076
+ },
3077
+
3078
+ /**
3079
+ * Delete a file by ID
3080
+ */
3081
+ async delete(fileId: number): Promise<StrapiMedia> {
3082
+ const response = await strapiClient.files.delete(fileId) as any;
3083
+ return response;
3084
+ },
3085
+ };
2508
3086
  `;
2509
3087
  }
2510
3088
  return `/**
2511
3089
  * Strapi Client (v5)
2512
3090
  * Generated by strapi2front
3091
+ *
3092
+ * Using official @strapi/client
3093
+ * @see https://docs.strapi.io/cms/api/client
2513
3094
  */
2514
3095
 
2515
- import Strapi from 'strapi-sdk-js';
2516
- import type { StrapiPagination } from './utils';
3096
+ import { strapi } from '@strapi/client';
3097
+ import type { StrapiPagination, StrapiMedia, StrapiFileInfo } from './utils';
2517
3098
 
2518
- // Initialize the Strapi client
2519
- const strapiUrl = import.meta.env.STRAPI_URL || process.env.STRAPI_URL || 'http://localhost:1337';
2520
- const strapiToken = import.meta.env.STRAPI_TOKEN || process.env.STRAPI_TOKEN;
2521
- const strapiApiPrefix = import.meta.env.STRAPI_API_PREFIX || process.env.STRAPI_API_PREFIX || '${normalizedPrefix}';
2522
-
2523
- export const strapi = new Strapi({
2524
- url: strapiUrl,
2525
- prefix: strapiApiPrefix,
2526
- axiosOptions: {
2527
- headers: strapiToken ? {
2528
- Authorization: \`Bearer \${strapiToken}\`,
2529
- } : {},
2530
- },
2531
- });
3099
+ // Default configuration from environment
3100
+ const defaultBaseURL = import.meta.env.STRAPI_URL || process.env.STRAPI_URL || 'http://localhost:1337';
3101
+ const defaultAuthToken = import.meta.env.STRAPI_TOKEN || process.env.STRAPI_TOKEN;
3102
+ const apiPrefix = '${normalizedPrefix}';
3103
+
3104
+ /**
3105
+ * Client options for authentication and connection
3106
+ * @beta This API is in beta and may change
3107
+ */
3108
+ export interface ClientOptions {
3109
+ /** JWT token or API token for authentication */
3110
+ authToken?: string;
3111
+ /** Base URL of the Strapi instance (without /api prefix) */
3112
+ baseURL?: string;
3113
+ }
3114
+
3115
+ /**
3116
+ * Create a configured Strapi client instance
3117
+ *
3118
+ * @param options - Optional configuration (authToken, baseURL)
3119
+ * @returns Configured Strapi client
3120
+ *
3121
+ * @example
3122
+ * // Default client (uses STRAPI_URL and STRAPI_TOKEN from env)
3123
+ * const client = createStrapiClient();
3124
+ *
3125
+ * @example
3126
+ * // Client with user JWT token
3127
+ * const userClient = createStrapiClient({ authToken: session.jwt });
3128
+ *
3129
+ * @example
3130
+ * // Client with custom URL (multi-tenant)
3131
+ * const tenantClient = createStrapiClient({
3132
+ * baseURL: 'https://tenant.example.com',
3133
+ * authToken: tenantToken
3134
+ * });
3135
+ *
3136
+ * @beta This API is in beta and may change
3137
+ */
3138
+ export function createStrapiClient(options?: ClientOptions) {
3139
+ const baseURL = (options?.baseURL || defaultBaseURL) + apiPrefix;
3140
+ const auth = options?.authToken || defaultAuthToken;
3141
+
3142
+ return strapi({ baseURL, auth });
3143
+ }
3144
+
3145
+ // Default client instance (uses environment variables)
3146
+ export const strapiClient = createStrapiClient();
2532
3147
 
2533
3148
  // Default pagination for fallback
2534
3149
  const defaultPagination: StrapiPagination = {
@@ -2538,65 +3153,146 @@ const defaultPagination: StrapiPagination = {
2538
3153
  total: 0,
2539
3154
  };
2540
3155
 
2541
- // Response types from strapi-sdk-js
2542
- interface StrapiListResponse<T> {
2543
- data: T[];
2544
- meta: {
2545
- pagination?: StrapiPagination;
2546
- };
2547
- }
2548
-
2549
- interface StrapiSingleResponse<T> {
2550
- data: T;
2551
- meta?: Record<string, unknown>;
3156
+ /**
3157
+ * Get or create a Strapi client based on options
3158
+ * @internal
3159
+ */
3160
+ function getClient(options?: ClientOptions) {
3161
+ if (options?.authToken || options?.baseURL) {
3162
+ return createStrapiClient(options);
3163
+ }
3164
+ return strapiClient;
2552
3165
  }
2553
3166
 
2554
- // Helper to get typed collection
2555
- export function collection<T>(pluralName: string) {
3167
+ /**
3168
+ * Helper to get typed collection
3169
+ * Wraps @strapi/client collection methods with proper typing
3170
+ *
3171
+ * @param pluralName - The plural name of the collection (e.g., 'articles')
3172
+ * @param clientOptions - Optional client configuration
3173
+ */
3174
+ export function collection<T>(pluralName: string, clientOptions?: ClientOptions) {
3175
+ const client = getClient(clientOptions);
3176
+ const col = client.collection(pluralName);
2556
3177
  return {
2557
3178
  async find(params?: Record<string, unknown>): Promise<{ data: T[]; meta: { pagination: StrapiPagination } }> {
2558
- const response = await strapi.find(pluralName, params) as unknown as StrapiListResponse<T>;
3179
+ const response = await col.find(params) as any;
2559
3180
  return {
2560
3181
  data: Array.isArray(response.data) ? response.data : [],
2561
- meta: {
2562
- pagination: response.meta?.pagination || defaultPagination,
2563
- },
3182
+ meta: { pagination: response.meta?.pagination || defaultPagination },
2564
3183
  };
2565
3184
  },
2566
3185
  async findOne(documentId: string, params?: Record<string, unknown>): Promise<{ data: T }> {
2567
- const response = await strapi.findOne(pluralName, documentId, params) as unknown as StrapiSingleResponse<T>;
3186
+ const response = await col.findOne(documentId, params) as any;
2568
3187
  return { data: response.data };
2569
3188
  },
2570
- async create(data: { data: Partial<T> }): Promise<{ data: T }> {
2571
- const response = await strapi.create(pluralName, data.data as Record<string, unknown>) as unknown as StrapiSingleResponse<T>;
3189
+ async create(data: Partial<T>): Promise<{ data: T }> {
3190
+ const response = await col.create(data as any) as any;
2572
3191
  return { data: response.data };
2573
3192
  },
2574
- async update(documentId: string, data: { data: Partial<T> }): Promise<{ data: T }> {
2575
- const response = await strapi.update(pluralName, documentId, data.data as Record<string, unknown>) as unknown as StrapiSingleResponse<T>;
3193
+ async update(documentId: string, data: Partial<T>): Promise<{ data: T }> {
3194
+ const response = await col.update(documentId, data as any) as any;
2576
3195
  return { data: response.data };
2577
3196
  },
2578
3197
  async delete(documentId: string): Promise<void> {
2579
- await strapi.delete(pluralName, documentId);
3198
+ await col.delete(documentId);
2580
3199
  },
2581
3200
  };
2582
3201
  }
2583
3202
 
2584
- // Helper to get typed single type
2585
- export function single<T>(singularName: string) {
3203
+ /**
3204
+ * Helper to get typed single type
3205
+ * Wraps @strapi/client single methods with proper typing
3206
+ *
3207
+ * @param singularName - The singular name of the single type (e.g., 'homepage')
3208
+ * @param clientOptions - Optional client configuration
3209
+ */
3210
+ export function single<T>(singularName: string, clientOptions?: ClientOptions) {
3211
+ const client = getClient(clientOptions);
3212
+ const singleType = client.single(singularName);
2586
3213
  return {
2587
3214
  async find(params?: Record<string, unknown>): Promise<{ data: T }> {
2588
- const response = await strapi.find(singularName, params) as unknown as StrapiSingleResponse<T>;
3215
+ const response = await singleType.find(params) as any;
2589
3216
  return { data: response.data };
2590
3217
  },
2591
- async update(data: { data: Partial<T> }): Promise<{ data: T }> {
2592
- const response = await strapi.update(singularName, 1 as unknown as string, data.data as Record<string, unknown>) as unknown as StrapiSingleResponse<T>;
3218
+ async update(data: Partial<T>): Promise<{ data: T }> {
3219
+ const response = await singleType.update(data as any) as any;
2593
3220
  return { data: response.data };
2594
3221
  },
2595
3222
  async delete(): Promise<void> {
2596
- await strapi.delete(singularName, 1 as unknown as string);
3223
+ await singleType.delete();
2597
3224
  },
2598
3225
  };
2599
3226
  }
3227
+
3228
+ /**
3229
+ * File management helpers
3230
+ * Wraps @strapi/client file methods with proper typing
3231
+ * @see https://docs.strapi.io/cms/api/client#working-with-files
3232
+ */
3233
+ export const files = {
3234
+ /**
3235
+ * Upload a file to Strapi
3236
+ *
3237
+ * @example
3238
+ * // Browser: upload from file input
3239
+ * const file = fileInput.files[0];
3240
+ * const uploaded = await files.upload(file, {
3241
+ * fileInfo: { alternativeText: 'My image', caption: 'A caption' }
3242
+ * });
3243
+ *
3244
+ * @example
3245
+ * // Use the returned ID to link to an entry
3246
+ * await collection('articles').create({
3247
+ * title: 'My article',
3248
+ * cover: uploaded.id,
3249
+ * });
3250
+ *
3251
+ * @see https://docs.strapi.io/cms/api/client#upload
3252
+ */
3253
+ async upload(file: File | Blob, options?: { fileInfo?: StrapiFileInfo }): Promise<StrapiMedia> {
3254
+ const response = await strapiClient.files.upload(file, options) as any;
3255
+ return response;
3256
+ },
3257
+
3258
+ /**
3259
+ * Find files with optional filtering and sorting
3260
+ *
3261
+ * @example
3262
+ * const images = await files.find({
3263
+ * filters: { mime: { $contains: 'image' } },
3264
+ * sort: ['name:asc'],
3265
+ * });
3266
+ */
3267
+ async find(params?: Record<string, unknown>): Promise<StrapiMedia[]> {
3268
+ const response = await strapiClient.files.find(params) as any;
3269
+ return Array.isArray(response) ? response : [];
3270
+ },
3271
+
3272
+ /**
3273
+ * Get a single file by ID
3274
+ */
3275
+ async findOne(fileId: number): Promise<StrapiMedia> {
3276
+ const response = await strapiClient.files.findOne(fileId) as any;
3277
+ return response;
3278
+ },
3279
+
3280
+ /**
3281
+ * Update file metadata (name, alternativeText, caption)
3282
+ */
3283
+ async update(fileId: number, fileInfo: StrapiFileInfo): Promise<StrapiMedia> {
3284
+ const response = await strapiClient.files.update(fileId, fileInfo) as any;
3285
+ return response;
3286
+ },
3287
+
3288
+ /**
3289
+ * Delete a file by ID
3290
+ */
3291
+ async delete(fileId: number): Promise<StrapiMedia> {
3292
+ const response = await strapiClient.files.delete(fileId) as any;
3293
+ return response;
3294
+ },
3295
+ };
2600
3296
  `;
2601
3297
  }
2602
3298
  function generateLocalesFile2(locales) {
@@ -2692,11 +3388,12 @@ ${attributes}
2692
3388
  }
2693
3389
  `;
2694
3390
  }
2695
- function generateComponentTypes(component, schema) {
3391
+ function generateComponentTypes(component, schema, includeSchemas = false, strapiVersion = "v5", advancedRelations = false) {
2696
3392
  const typeName = toPascalCase(component.name);
3393
+ const schemaName = toCamelCase(component.name);
2697
3394
  const attributes = generateAttributes2(component.attributes);
2698
3395
  const imports = generateTypeImports(component.attributes, schema, "component");
2699
- return `/**
3396
+ let content = `/**
2700
3397
  * ${component.displayName} component
2701
3398
  * Category: ${component.category}
2702
3399
  * ${component.description || ""}
@@ -2710,6 +3407,32 @@ export interface ${typeName} {
2710
3407
  ${attributes}
2711
3408
  }
2712
3409
  `;
3410
+ if (includeSchemas) {
3411
+ const componentSchemaNames = buildComponentSchemaNames(component.attributes, schema);
3412
+ const schemaImports = generateSchemaImports(component.attributes, schema, "component");
3413
+ const schemaOptions = {
3414
+ isUpdate: false,
3415
+ strapiVersion,
3416
+ useAdvancedRelations: advancedRelations,
3417
+ componentSchemaNames
3418
+ };
3419
+ const schemaResult = generateZodObjectSchema(component.attributes, schemaOptions);
3420
+ content += `
3421
+ import { z } from 'zod';
3422
+ ${schemaImports}
3423
+ /**
3424
+ * Zod schema for ${component.displayName} component
3425
+ * Use this for validating component data in forms
3426
+ */
3427
+ export const ${schemaName}Schema = ${schemaResult.schema};
3428
+
3429
+ /**
3430
+ * Inferred type from schema
3431
+ */
3432
+ export type ${typeName}Input = z.infer<typeof ${schemaName}Schema>;
3433
+ `;
3434
+ }
3435
+ return content;
2713
3436
  }
2714
3437
  function generateTypeImports(attributes, schema, context) {
2715
3438
  const utilsImports = [];
@@ -2863,7 +3586,7 @@ function generateCollectionService3(collection, strapiVersion) {
2863
3586
  const omitFields = isV4 ? "'id' | 'createdAt' | 'updatedAt' | 'publishedAt'" : "'id' | 'documentId' | 'createdAt' | 'updatedAt' | 'publishedAt'";
2864
3587
  const omitFieldsUpdate = isV4 ? "'id' | 'createdAt' | 'updatedAt'" : "'id' | 'documentId' | 'createdAt' | 'updatedAt'";
2865
3588
  const imports = [
2866
- `import { collection } from '../../shared/client';`,
3589
+ `import { collection, type ClientOptions } from '../../shared/client';`,
2867
3590
  `import type { ${typeName}, ${typeName}Filters } from './types';`,
2868
3591
  `import type { StrapiPagination } from '../../shared/utils';`
2869
3592
  ];
@@ -2914,6 +3637,18 @@ function generateCollectionService3(collection, strapiVersion) {
2914
3637
  findOneParams += `
2915
3638
  status: options.status,`;
2916
3639
  }
3640
+ const createOptionsInterface = draftAndPublish ? `
3641
+ export interface CreateOptions {
3642
+ /** Publish immediately or create as draft. Default: 'draft' */
3643
+ status?: 'draft' | 'published';
3644
+ }
3645
+ ` : "";
3646
+ const updateOptionsInterface = draftAndPublish ? `
3647
+ export interface UpdateOptions {
3648
+ /** Change publication status */
3649
+ status?: 'draft' | 'published';
3650
+ }
3651
+ ` : "";
2917
3652
  return `/**
2918
3653
  * ${collection.displayName} Service
2919
3654
  * ${collection.description || ""}
@@ -2930,13 +3665,24 @@ ${findManyOptionsFields}
2930
3665
  export interface FindOneOptions {
2931
3666
  ${findOneOptionsFields}
2932
3667
  }
2933
-
2934
- // Create typed collection helper
2935
- const ${toCamelCase(collection.singularName)}Collection = collection<${typeName}>('${endpoint}');
3668
+ ${createOptionsInterface}${updateOptionsInterface}
3669
+ /**
3670
+ * Get a typed collection helper, optionally with custom client options
3671
+ * @internal
3672
+ */
3673
+ function getCollection(clientOptions?: ClientOptions) {
3674
+ return collection<${typeName}>('${endpoint}', clientOptions);
3675
+ }
2936
3676
 
2937
3677
  export const ${serviceName} = {
2938
- async findMany(options: FindManyOptions = {}): Promise<{ data: ${typeName}[]; pagination: StrapiPagination }> {
2939
- const response = await ${toCamelCase(collection.singularName)}Collection.find({
3678
+ /**
3679
+ * Find multiple ${collection.displayName} entries
3680
+ * @param options - Query options (filters, pagination, sort, populate)
3681
+ * @param clientOptions - Optional client configuration (authToken, baseURL) - beta
3682
+ */
3683
+ async findMany(options: FindManyOptions = {}, clientOptions?: ClientOptions): Promise<{ data: ${typeName}[]; pagination: StrapiPagination }> {
3684
+ const col = getCollection(clientOptions);
3685
+ const response = await col.find({
2940
3686
  ${findParams}
2941
3687
  });
2942
3688
 
@@ -2946,7 +3692,12 @@ ${findParams}
2946
3692
  };
2947
3693
  },
2948
3694
 
2949
- async findAll(options: Omit<FindManyOptions, 'pagination'> = {}): Promise<${typeName}[]> {
3695
+ /**
3696
+ * Find all ${collection.displayName} entries (handles pagination automatically)
3697
+ * @param options - Query options (filters, sort, populate)
3698
+ * @param clientOptions - Optional client configuration (authToken, baseURL) - beta
3699
+ */
3700
+ async findAll(options: Omit<FindManyOptions, 'pagination'> = {}, clientOptions?: ClientOptions): Promise<${typeName}[]> {
2950
3701
  const allItems: ${typeName}[] = [];
2951
3702
  let page = 1;
2952
3703
  let hasMore = true;
@@ -2955,7 +3706,7 @@ ${findParams}
2955
3706
  const { data, pagination } = await this.findMany({
2956
3707
  ...options,
2957
3708
  pagination: { page, pageSize: 100 },
2958
- });
3709
+ }, clientOptions);
2959
3710
 
2960
3711
  allItems.push(...data);
2961
3712
  hasMore = page < pagination.pageCount;
@@ -2965,9 +3716,16 @@ ${findParams}
2965
3716
  return allItems;
2966
3717
  },
2967
3718
 
2968
- async findOne(${idParam}, options: FindOneOptions = {}): Promise<${typeName} | null> {
3719
+ /**
3720
+ * Find a single ${collection.displayName} by ID
3721
+ * @param ${idName} - The ${isV4 ? "numeric ID" : "document ID"}
3722
+ * @param options - Query options (populate)
3723
+ * @param clientOptions - Optional client configuration (authToken, baseURL) - beta
3724
+ */
3725
+ async findOne(${idParam}, options: FindOneOptions = {}, clientOptions?: ClientOptions): Promise<${typeName} | null> {
2969
3726
  try {
2970
- const response = await ${toCamelCase(collection.singularName)}Collection.findOne(${idName}, {
3727
+ const col = getCollection(clientOptions);
3728
+ const response = await col.findOne(${idName}, {
2971
3729
  ${findOneParams}
2972
3730
  });
2973
3731
 
@@ -2980,35 +3738,65 @@ ${findOneParams}
2980
3738
  }
2981
3739
  },
2982
3740
  ${hasSlug ? `
2983
- async findBySlug(slug: string, options: FindOneOptions = {}): Promise<${typeName} | null> {
3741
+ /**
3742
+ * Find a single ${collection.displayName} by slug
3743
+ * @param slug - The slug value
3744
+ * @param options - Query options (populate)
3745
+ * @param clientOptions - Optional client configuration (authToken, baseURL) - beta
3746
+ */
3747
+ async findBySlug(slug: string, options: FindOneOptions = {}, clientOptions?: ClientOptions): Promise<${typeName} | null> {
2984
3748
  const { data } = await this.findMany({
2985
3749
  filters: { slug: { $eq: slug } } as ${typeName}Filters,
2986
3750
  pagination: { pageSize: 1 },
2987
3751
  populate: options.populate,${localized ? "\n locale: options.locale," : ""}${draftAndPublish ? "\n status: options.status," : ""}
2988
- });
3752
+ }, clientOptions);
2989
3753
 
2990
3754
  return data[0] || null;
2991
3755
  },
2992
3756
  ` : ""}
2993
- async create(data: Partial<Omit<${typeName}, ${omitFields}>>): Promise<${typeName}> {
2994
- const response = await ${toCamelCase(collection.singularName)}Collection.create({ data });
3757
+ /**
3758
+ * Create a new ${collection.displayName}
3759
+ * @param data - The data to create
3760
+ * @param clientOptions - Optional client configuration (authToken, baseURL) - beta
3761
+ */
3762
+ async create(data: Partial<Omit<${typeName}, ${omitFields}>>${draftAndPublish ? ", options: CreateOptions = {}" : ""}, clientOptions?: ClientOptions): Promise<${typeName}> {
3763
+ const col = getCollection(clientOptions);
3764
+ const response = await col.create(${draftAndPublish ? "{ ...data, status: options.status }" : "data"});
2995
3765
  return response.data;
2996
3766
  },
2997
3767
 
2998
- async update(${idParam}, data: Partial<Omit<${typeName}, ${omitFieldsUpdate}>>): Promise<${typeName}> {
2999
- const response = await ${toCamelCase(collection.singularName)}Collection.update(${idName}, { data });
3768
+ /**
3769
+ * Update an existing ${collection.displayName}
3770
+ * @param ${idName} - The ${isV4 ? "numeric ID" : "document ID"}
3771
+ * @param data - The data to update
3772
+ * @param clientOptions - Optional client configuration (authToken, baseURL) - beta
3773
+ */
3774
+ async update(${idParam}, data: Partial<Omit<${typeName}, ${omitFieldsUpdate}>>${draftAndPublish ? ", options: UpdateOptions = {}" : ""}, clientOptions?: ClientOptions): Promise<${typeName}> {
3775
+ const col = getCollection(clientOptions);
3776
+ const response = await col.update(${idName}, ${draftAndPublish ? "{ ...data, status: options.status }" : "data"});
3000
3777
  return response.data;
3001
3778
  },
3002
3779
 
3003
- async delete(${idParam}): Promise<void> {
3004
- await ${toCamelCase(collection.singularName)}Collection.delete(${idName});
3780
+ /**
3781
+ * Delete a ${collection.displayName}
3782
+ * @param ${idName} - The ${isV4 ? "numeric ID" : "document ID"}
3783
+ * @param clientOptions - Optional client configuration (authToken, baseURL) - beta
3784
+ */
3785
+ async delete(${idParam}, clientOptions?: ClientOptions): Promise<void> {
3786
+ const col = getCollection(clientOptions);
3787
+ await col.delete(${idName});
3005
3788
  },
3006
3789
 
3007
- async count(filters?: ${typeName}Filters): Promise<number> {
3790
+ /**
3791
+ * Count ${collection.displayName} entries
3792
+ * @param filters - Optional filters
3793
+ * @param clientOptions - Optional client configuration (authToken, baseURL) - beta
3794
+ */
3795
+ async count(filters?: ${typeName}Filters, clientOptions?: ClientOptions): Promise<number> {
3008
3796
  const { pagination } = await this.findMany({
3009
3797
  filters,
3010
3798
  pagination: { pageSize: 1 },
3011
- });
3799
+ }, clientOptions);
3012
3800
 
3013
3801
  return pagination.total;
3014
3802
  },
@@ -3023,7 +3811,7 @@ function generateSingleService3(single, strapiVersion) {
3023
3811
  const isV4 = strapiVersion === "v4";
3024
3812
  const omitFields = isV4 ? "'id' | 'createdAt' | 'updatedAt'" : "'id' | 'documentId' | 'createdAt' | 'updatedAt'";
3025
3813
  const imports = [
3026
- `import { single } from '../../shared/client';`,
3814
+ `import { single, type ClientOptions } from '../../shared/client';`,
3027
3815
  `import type { ${typeName} } from './types';`
3028
3816
  ];
3029
3817
  if (localized) {
@@ -3060,43 +3848,208 @@ export interface FindOptions {
3060
3848
  ${findOptionsFields}
3061
3849
  }
3062
3850
 
3063
- // Create typed single helper
3064
- const ${toCamelCase(single.singularName)}Single = single<${typeName}>('${endpoint}');
3851
+ /**
3852
+ * Get a typed single helper, optionally with custom client options
3853
+ * @internal
3854
+ */
3855
+ function getSingle(clientOptions?: ClientOptions) {
3856
+ return single<${typeName}>('${endpoint}', clientOptions);
3857
+ }
3065
3858
 
3066
3859
  export const ${serviceName} = {
3067
- async find(options: FindOptions = {}): Promise<${typeName} | null> {
3860
+ /**
3861
+ * Find the ${single.displayName} single type
3862
+ * @param options - Query options (populate)
3863
+ * @param clientOptions - Optional client configuration (authToken, baseURL) - beta
3864
+ */
3865
+ async find(options: FindOptions = {}, clientOptions?: ClientOptions): Promise<${typeName} | null> {
3068
3866
  try {
3069
- const response = await ${toCamelCase(single.singularName)}Single.find({
3867
+ const s = getSingle(clientOptions);
3868
+ const response = await s.find({
3070
3869
  ${findParams}
3071
3870
  });
3072
3871
 
3073
- return response.data;
3074
- } catch (error) {
3075
- if (error instanceof Error && error.message.includes('404')) {
3076
- return null;
3077
- }
3078
- throw error;
3079
- }
3080
- },
3872
+ return response.data;
3873
+ } catch (error) {
3874
+ if (error instanceof Error && error.message.includes('404')) {
3875
+ return null;
3876
+ }
3877
+ throw error;
3878
+ }
3879
+ },
3880
+
3881
+ /**
3882
+ * Update the ${single.displayName} single type
3883
+ * @param data - The data to update
3884
+ * @param clientOptions - Optional client configuration (authToken, baseURL) - beta
3885
+ */
3886
+ async update(data: Partial<Omit<${typeName}, ${omitFields}>>, clientOptions?: ClientOptions): Promise<${typeName}> {
3887
+ const s = getSingle(clientOptions);
3888
+ const response = await s.update(data);
3889
+ return response.data;
3890
+ },
3891
+
3892
+ /**
3893
+ * Delete the ${single.displayName} single type
3894
+ * @param clientOptions - Optional client configuration (authToken, baseURL) - beta
3895
+ */
3896
+ async delete(clientOptions?: ClientOptions): Promise<void> {
3897
+ const s = getSingle(clientOptions);
3898
+ await s.delete();
3899
+ },
3900
+ };
3901
+ `;
3902
+ }
3903
+ function buildComponentSchemaNames(attributes, schema) {
3904
+ const map = /* @__PURE__ */ new Map();
3905
+ for (const attr of Object.values(attributes)) {
3906
+ if (attr.type === "component" && "component" in attr && attr.component) {
3907
+ const uid = attr.component;
3908
+ if (!map.has(uid)) {
3909
+ const componentName = uid.split(".").pop() || "";
3910
+ const exists = schema.components.some((c) => c.name === componentName);
3911
+ if (exists && componentName) {
3912
+ map.set(uid, `${toCamelCase(componentName)}Schema`);
3913
+ }
3914
+ }
3915
+ }
3916
+ if (attr.type === "dynamiczone" && "components" in attr && attr.components) {
3917
+ for (const uid of attr.components) {
3918
+ if (!map.has(uid)) {
3919
+ const componentName = uid.split(".").pop() || "";
3920
+ const exists = schema.components.some((c) => c.name === componentName);
3921
+ if (exists && componentName) {
3922
+ map.set(uid, `${toCamelCase(componentName)}Schema`);
3923
+ }
3924
+ }
3925
+ }
3926
+ }
3927
+ }
3928
+ return map;
3929
+ }
3930
+ function generateSchemaImports(attributes, schema, context) {
3931
+ const imports = /* @__PURE__ */ new Map();
3932
+ const relativePrefix = context === "component" ? "." : "../../components";
3933
+ for (const attr of Object.values(attributes)) {
3934
+ if (attr.type === "component" && "component" in attr && attr.component) {
3935
+ addComponentImport(attr.component, imports, schema, relativePrefix);
3936
+ }
3937
+ if (attr.type === "dynamiczone" && "components" in attr && attr.components) {
3938
+ for (const uid of attr.components) {
3939
+ addComponentImport(uid, imports, schema, relativePrefix);
3940
+ }
3941
+ }
3942
+ }
3943
+ if (imports.size === 0) return "";
3944
+ const lines = [];
3945
+ for (const [schemaVarName, importPath] of imports) {
3946
+ lines.push(`import { ${schemaVarName} } from '${importPath}';`);
3947
+ }
3948
+ return lines.join("\n") + "\n";
3949
+ }
3950
+ function addComponentImport(uid, imports, schema, relativePrefix) {
3951
+ const componentName = uid.split(".").pop() || "";
3952
+ if (!componentName) return;
3953
+ const exists = schema.components.some((c) => c.name === componentName);
3954
+ if (!exists) return;
3955
+ const schemaVarName = `${toCamelCase(componentName)}Schema`;
3956
+ if (imports.has(schemaVarName)) return;
3957
+ const fileName = toKebabCase(componentName);
3958
+ imports.set(schemaVarName, `${relativePrefix}/${fileName}`);
3959
+ }
3960
+ function generateCollectionSchemas(collection, schema, strapiVersion = "v5", advancedRelations = false) {
3961
+ const name = toCamelCase(collection.singularName);
3962
+ const pascalName = toPascalCase(collection.singularName);
3963
+ const componentSchemaNames = buildComponentSchemaNames(collection.attributes, schema);
3964
+ const schemaImports = generateSchemaImports(collection.attributes, schema, "collection");
3965
+ const schemaOptions = {
3966
+ isUpdate: false,
3967
+ strapiVersion,
3968
+ useAdvancedRelations: advancedRelations,
3969
+ componentSchemaNames
3970
+ };
3971
+ const createResult = generateZodObjectSchema(collection.attributes, schemaOptions);
3972
+ const updateResult = generateZodObjectSchema(collection.attributes, { ...schemaOptions, isUpdate: true });
3973
+ return `/**
3974
+ * ${collection.displayName} Zod Schemas
3975
+ * ${collection.description || ""}
3976
+ * Generated by strapi2front
3977
+ *
3978
+ * These schemas can be used for:
3979
+ * - Form validation (React Hook Form, TanStack Form, Formik, etc.)
3980
+ * - API request/response validation
3981
+ * - Type inference
3982
+ */
3983
+
3984
+ import { z } from 'zod';
3985
+ ${schemaImports}
3986
+ /**
3987
+ * Schema for creating a new ${collection.displayName}
3988
+ */
3989
+ export const ${name}CreateSchema = ${createResult.schema};
3990
+
3991
+ /**
3992
+ * Schema for updating a ${collection.displayName}
3993
+ * All fields are optional for partial updates
3994
+ */
3995
+ export const ${name}UpdateSchema = ${updateResult.schema};
3081
3996
 
3082
- async update(data: Partial<Omit<${typeName}, ${omitFields}>>): Promise<${typeName}> {
3083
- const response = await ${toCamelCase(single.singularName)}Single.update({ data });
3084
- return response.data;
3085
- },
3997
+ /**
3998
+ * Inferred types from schemas
3999
+ * Use these for type-safe form handling
4000
+ */
4001
+ export type ${pascalName}CreateInput = z.infer<typeof ${name}CreateSchema>;
4002
+ export type ${pascalName}UpdateInput = z.infer<typeof ${name}UpdateSchema>;
4003
+ `;
4004
+ }
4005
+ function generateSingleSchemas(single, schema, strapiVersion = "v5", advancedRelations = false) {
4006
+ const name = toCamelCase(single.singularName);
4007
+ const pascalName = toPascalCase(single.singularName);
4008
+ const componentSchemaNames = buildComponentSchemaNames(single.attributes, schema);
4009
+ const schemaImports = generateSchemaImports(single.attributes, schema, "single");
4010
+ const updateResult = generateZodObjectSchema(single.attributes, {
4011
+ isUpdate: true,
4012
+ strapiVersion,
4013
+ useAdvancedRelations: advancedRelations,
4014
+ componentSchemaNames
4015
+ });
4016
+ return `/**
4017
+ * ${single.displayName} Zod Schemas (Single Type)
4018
+ * ${single.description || ""}
4019
+ * Generated by strapi2front
4020
+ *
4021
+ * These schemas can be used for:
4022
+ * - Form validation (React Hook Form, TanStack Form, Formik, etc.)
4023
+ * - API request/response validation
4024
+ * - Type inference
4025
+ */
3086
4026
 
3087
- async delete(): Promise<void> {
3088
- await ${toCamelCase(single.singularName)}Single.delete();
3089
- },
3090
- };
4027
+ import { z } from 'zod';
4028
+ ${schemaImports}
4029
+ /**
4030
+ * Schema for updating ${single.displayName}
4031
+ * All fields are optional for partial updates
4032
+ */
4033
+ export const ${name}UpdateSchema = ${updateResult.schema};
4034
+
4035
+ /**
4036
+ * Inferred type from schema
4037
+ */
4038
+ export type ${pascalName}UpdateInput = z.infer<typeof ${name}UpdateSchema>;
3091
4039
  `;
3092
4040
  }
3093
- function generateCollectionActions2(collection, strapiVersion) {
4041
+ function generateCollectionActions2(collection, options) {
4042
+ const { strapiVersion, useExternalSchemas, draftAndPublish = false } = options;
3094
4043
  const typeName = toPascalCase(collection.singularName);
3095
4044
  const serviceName = toCamelCase(collection.singularName) + "Service";
3096
4045
  const actionPrefix = toCamelCase(collection.singularName);
3097
4046
  const isV4 = strapiVersion === "v4";
3098
4047
  const idInputSchema = isV4 ? "z.number().int().positive()" : "z.string()";
3099
4048
  const idParamName = isV4 ? "id" : "documentId";
4049
+ const statusSchema = "z.enum(['draft', 'published']).optional()";
4050
+ const schemaImport = useExternalSchemas ? `import { ${actionPrefix}CreateSchema, ${actionPrefix}UpdateSchema } from './schemas';` : "";
4051
+ const createSchema = useExternalSchemas ? `${actionPrefix}CreateSchema` : "z.record(z.unknown())";
4052
+ const updateSchema = useExternalSchemas ? `${actionPrefix}UpdateSchema` : "z.record(z.unknown())";
3100
4053
  return `/**
3101
4054
  * ${collection.displayName} Astro Actions
3102
4055
  * ${collection.description || ""}
@@ -3108,6 +4061,7 @@ import { defineAction } from 'astro:actions';
3108
4061
  import { z } from 'astro:schema';
3109
4062
  import { ${serviceName} } from './service';
3110
4063
  import type { ${typeName} } from './types';
4064
+ ${schemaImport}
3111
4065
 
3112
4066
  export const ${actionPrefix}Actions = {
3113
4067
  getMany: defineAction({
@@ -3137,10 +4091,11 @@ export const ${actionPrefix}Actions = {
3137
4091
 
3138
4092
  create: defineAction({
3139
4093
  input: z.object({
3140
- data: z.record(z.unknown()),
4094
+ data: ${createSchema},${draftAndPublish ? `
4095
+ status: ${statusSchema},` : ""}
3141
4096
  }),
3142
4097
  handler: async (input) => {
3143
- const data = await ${serviceName}.create(input.data as Partial<${typeName}>);
4098
+ const data = await ${serviceName}.create(input.data as Partial<${typeName}>${draftAndPublish ? ", { status: input.status }" : ""});
3144
4099
  return { data };
3145
4100
  },
3146
4101
  }),
@@ -3148,10 +4103,11 @@ export const ${actionPrefix}Actions = {
3148
4103
  update: defineAction({
3149
4104
  input: z.object({
3150
4105
  ${idParamName}: ${idInputSchema},
3151
- data: z.record(z.unknown()),
4106
+ data: ${updateSchema},${draftAndPublish ? `
4107
+ status: ${statusSchema},` : ""}
3152
4108
  }),
3153
4109
  handler: async (input) => {
3154
- const data = await ${serviceName}.update(input.${idParamName}, input.data as Partial<${typeName}>);
4110
+ const data = await ${serviceName}.update(input.${idParamName}, input.data as Partial<${typeName}>${draftAndPublish ? ", { status: input.status }" : ""});
3155
4111
  return { data };
3156
4112
  },
3157
4113
  }),
@@ -3314,6 +4270,15 @@ ${mediaType}
3314
4270
  * @property {string} url
3315
4271
  */
3316
4272
 
4273
+ /**
4274
+ * File metadata for uploads
4275
+ * @see https://docs.strapi.io/cms/api/client#upload
4276
+ * @typedef {Object} StrapiFileInfo
4277
+ * @property {string} [name]
4278
+ * @property {string} [alternativeText]
4279
+ * @property {string} [caption]
4280
+ */
4281
+
3317
4282
  /**
3318
4283
  * @typedef {Object} StrapiPagination
3319
4284
  * @property {number} page
@@ -3348,29 +4313,26 @@ ${useESM ? "export {};" : "module.exports = {};"}
3348
4313
  function generateClientJSDoc(strapiVersion, apiPrefix = "/api", useESM = false) {
3349
4314
  const isV4 = strapiVersion === "v4";
3350
4315
  const normalizedPrefix = apiPrefix.startsWith("/") ? apiPrefix : "/" + apiPrefix;
3351
- const importStatement = useESM ? `import Strapi from 'strapi-sdk-js';` : `const Strapi = require('strapi-sdk-js').default;`;
4316
+ const importStatement = useESM ? `import { strapi as createStrapi } from '@strapi/client';` : `const { strapi: createStrapi } = require('@strapi/client');`;
3352
4317
  if (isV4) {
3353
4318
  return `// @ts-check
3354
4319
  /**
3355
4320
  * Strapi Client (v4)
3356
4321
  * Generated by strapi2front
4322
+ *
4323
+ * Note: @strapi/client officially supports Strapi v5+
4324
+ * This v4 client uses a compatibility layer for the nested attributes structure
3357
4325
  */
3358
4326
 
3359
4327
  ${importStatement}
3360
4328
 
3361
4329
  // Initialize the Strapi client
3362
- const strapiUrl = process.env.STRAPI_URL || 'http://localhost:1337';
3363
- const strapiToken = process.env.STRAPI_TOKEN;
3364
- const strapiApiPrefix = process.env.STRAPI_API_PREFIX || '${normalizedPrefix}';
3365
-
3366
- const strapi = new Strapi({
3367
- url: strapiUrl,
3368
- prefix: strapiApiPrefix,
3369
- axiosOptions: {
3370
- headers: strapiToken ? {
3371
- Authorization: \`Bearer \${strapiToken}\`,
3372
- } : {},
3373
- },
4330
+ const baseURL = (process.env.STRAPI_URL || 'http://localhost:1337') + '${normalizedPrefix}';
4331
+ const authToken = process.env.STRAPI_TOKEN;
4332
+
4333
+ const strapiClient = createStrapi({
4334
+ baseURL,
4335
+ auth: authToken,
3374
4336
  });
3375
4337
 
3376
4338
  /** @type {import('./utils').StrapiPagination} */
@@ -3437,18 +4399,19 @@ function flattenRelations(data) {
3437
4399
  }
3438
4400
 
3439
4401
  /**
3440
- * Helper to get typed collection
4402
+ * Helper to get typed collection (v4 compatibility wrapper)
3441
4403
  * @template T
3442
4404
  * @param {string} pluralName
3443
4405
  */
3444
4406
  function collection(pluralName) {
4407
+ const col = strapiClient.collection(pluralName);
3445
4408
  return {
3446
4409
  /**
3447
4410
  * @param {Record<string, unknown>} [params]
3448
4411
  * @returns {Promise<{ data: T[], meta: { pagination: import('./utils').StrapiPagination } }>}
3449
4412
  */
3450
4413
  async find(params) {
3451
- const response = await strapi.find(pluralName, params);
4414
+ const response = await col.find(params);
3452
4415
  /** @type {any} */
3453
4416
  const rawData = response.data;
3454
4417
  /** @type {T[]} */
@@ -3474,7 +4437,7 @@ function collection(pluralName) {
3474
4437
  * @returns {Promise<{ data: T }>}
3475
4438
  */
3476
4439
  async findOne(id, params) {
3477
- const response = await strapi.findOne(pluralName, String(id), params);
4440
+ const response = await col.findOne(String(id), params);
3478
4441
  /** @type {any} */
3479
4442
  const rawData = response.data;
3480
4443
  /** @type {T} */
@@ -3482,11 +4445,11 @@ function collection(pluralName) {
3482
4445
  return { data };
3483
4446
  },
3484
4447
  /**
3485
- * @param {{ data: Partial<T> }} data
4448
+ * @param {Partial<T>} data
3486
4449
  * @returns {Promise<{ data: T }>}
3487
4450
  */
3488
4451
  async create(data) {
3489
- const response = await strapi.create(pluralName, data.data);
4452
+ const response = await col.create(data);
3490
4453
  /** @type {any} */
3491
4454
  const rawData = response.data;
3492
4455
  /** @type {T} */
@@ -3495,11 +4458,11 @@ function collection(pluralName) {
3495
4458
  },
3496
4459
  /**
3497
4460
  * @param {number|string} id
3498
- * @param {{ data: Partial<T> }} data
4461
+ * @param {Partial<T>} data
3499
4462
  * @returns {Promise<{ data: T }>}
3500
4463
  */
3501
4464
  async update(id, data) {
3502
- const response = await strapi.update(pluralName, String(id), data.data);
4465
+ const response = await col.update(String(id), data);
3503
4466
  /** @type {any} */
3504
4467
  const rawData = response.data;
3505
4468
  /** @type {T} */
@@ -3511,24 +4474,25 @@ function collection(pluralName) {
3511
4474
  * @returns {Promise<void>}
3512
4475
  */
3513
4476
  async delete(id) {
3514
- await strapi.delete(pluralName, String(id));
4477
+ await col.delete(String(id));
3515
4478
  },
3516
4479
  };
3517
4480
  }
3518
4481
 
3519
4482
  /**
3520
- * Helper to get typed single type
4483
+ * Helper to get typed single type (v4 compatibility wrapper)
3521
4484
  * @template T
3522
4485
  * @param {string} singularName
3523
4486
  */
3524
4487
  function single(singularName) {
4488
+ const singleType = strapiClient.single(singularName);
3525
4489
  return {
3526
4490
  /**
3527
4491
  * @param {Record<string, unknown>} [params]
3528
4492
  * @returns {Promise<{ data: T }>}
3529
4493
  */
3530
4494
  async find(params) {
3531
- const response = await strapi.find(singularName, params);
4495
+ const response = await singleType.find(params);
3532
4496
  /** @type {any} */
3533
4497
  const rawData = response.data;
3534
4498
  /** @type {T} */
@@ -3536,11 +4500,11 @@ function single(singularName) {
3536
4500
  return { data };
3537
4501
  },
3538
4502
  /**
3539
- * @param {{ data: Partial<T> }} data
4503
+ * @param {Partial<T>} data
3540
4504
  * @returns {Promise<{ data: T }>}
3541
4505
  */
3542
4506
  async update(data) {
3543
- const response = await strapi.update(singularName, String(1), data.data);
4507
+ const response = await singleType.update(data);
3544
4508
  /** @type {any} */
3545
4509
  const rawData = response.data;
3546
4510
  /** @type {T} */
@@ -3551,35 +4515,97 @@ function single(singularName) {
3551
4515
  * @returns {Promise<void>}
3552
4516
  */
3553
4517
  async delete() {
3554
- await strapi.delete(singularName, String(1));
4518
+ await singleType.delete();
3555
4519
  },
3556
4520
  };
3557
4521
  }
3558
4522
 
3559
- ${useESM ? "export { strapi, collection, single };" : "module.exports = { strapi, collection, single };"}
4523
+ /**
4524
+ * File management helpers
4525
+ * Wraps @strapi/client file methods with proper typing
4526
+ * @see https://docs.strapi.io/cms/api/client#working-with-files
4527
+ */
4528
+ const files = {
4529
+ /**
4530
+ * Upload a file to Strapi
4531
+ * @param {File|Blob} file
4532
+ * @param {{ fileInfo?: import('./utils').StrapiFileInfo }} [options]
4533
+ * @returns {Promise<import('./utils').StrapiMedia>}
4534
+ * @see https://docs.strapi.io/cms/api/client#upload
4535
+ */
4536
+ async upload(file, options) {
4537
+ /** @type {any} */
4538
+ const response = await strapiClient.files.upload(file, options);
4539
+ return response;
4540
+ },
4541
+
4542
+ /**
4543
+ * Find files with optional filtering and sorting
4544
+ * @param {Record<string, unknown>} [params]
4545
+ * @returns {Promise<import('./utils').StrapiMedia[]>}
4546
+ */
4547
+ async find(params) {
4548
+ /** @type {any} */
4549
+ const response = await strapiClient.files.find(params);
4550
+ return Array.isArray(response) ? response : [];
4551
+ },
4552
+
4553
+ /**
4554
+ * Get a single file by ID
4555
+ * @param {number} fileId
4556
+ * @returns {Promise<import('./utils').StrapiMedia>}
4557
+ */
4558
+ async findOne(fileId) {
4559
+ /** @type {any} */
4560
+ const response = await strapiClient.files.findOne(fileId);
4561
+ return response;
4562
+ },
4563
+
4564
+ /**
4565
+ * Update file metadata (name, alternativeText, caption)
4566
+ * @param {number} fileId
4567
+ * @param {import('./utils').StrapiFileInfo} fileInfo
4568
+ * @returns {Promise<import('./utils').StrapiMedia>}
4569
+ */
4570
+ async update(fileId, fileInfo) {
4571
+ /** @type {any} */
4572
+ const response = await strapiClient.files.update(fileId, fileInfo);
4573
+ return response;
4574
+ },
4575
+
4576
+ /**
4577
+ * Delete a file by ID
4578
+ * @param {number} fileId
4579
+ * @returns {Promise<import('./utils').StrapiMedia>}
4580
+ */
4581
+ async delete(fileId) {
4582
+ /** @type {any} */
4583
+ const response = await strapiClient.files.delete(fileId);
4584
+ return response;
4585
+ },
4586
+ };
4587
+
4588
+ ${useESM ? "export { strapiClient, collection, single, files };" : "module.exports = { strapiClient, collection, single, files };"}
3560
4589
  `;
3561
4590
  }
3562
4591
  return `// @ts-check
3563
4592
  /**
3564
4593
  * Strapi Client (v5)
3565
4594
  * Generated by strapi2front
4595
+ *
4596
+ * Using official @strapi/client
4597
+ * @see https://docs.strapi.io/cms/api/client
3566
4598
  */
3567
4599
 
3568
4600
  ${importStatement}
3569
4601
 
3570
4602
  // Initialize the Strapi client
3571
- const strapiUrl = process.env.STRAPI_URL || 'http://localhost:1337';
3572
- const strapiToken = process.env.STRAPI_TOKEN;
3573
- const strapiApiPrefix = process.env.STRAPI_API_PREFIX || '${normalizedPrefix}';
3574
-
3575
- const strapi = new Strapi({
3576
- url: strapiUrl,
3577
- prefix: strapiApiPrefix,
3578
- axiosOptions: {
3579
- headers: strapiToken ? {
3580
- Authorization: \`Bearer \${strapiToken}\`,
3581
- } : {},
3582
- },
4603
+ const baseURL = (process.env.STRAPI_URL || 'http://localhost:1337') + '${normalizedPrefix}';
4604
+ const authToken = process.env.STRAPI_TOKEN;
4605
+
4606
+ const strapiClient = createStrapi({
4607
+ baseURL,
4608
+ auth: authToken,
3583
4609
  });
3584
4610
 
3585
4611
  /** @type {import('./utils').StrapiPagination} */
@@ -3596,13 +4622,14 @@ const defaultPagination = {
3596
4622
  * @param {string} pluralName
3597
4623
  */
3598
4624
  function collection(pluralName) {
4625
+ const col = strapiClient.collection(pluralName);
3599
4626
  return {
3600
4627
  /**
3601
4628
  * @param {Record<string, unknown>} [params]
3602
4629
  * @returns {Promise<{ data: T[], meta: { pagination: import('./utils').StrapiPagination } }>}
3603
4630
  */
3604
4631
  async find(params) {
3605
- const response = await strapi.find(pluralName, params);
4632
+ const response = await col.find(params);
3606
4633
  /** @type {any} */
3607
4634
  const rawMeta = response.meta;
3608
4635
  /** @type {any} */
@@ -3626,7 +4653,7 @@ function collection(pluralName) {
3626
4653
  * @returns {Promise<{ data: T }>}
3627
4654
  */
3628
4655
  async findOne(documentId, params) {
3629
- const response = await strapi.findOne(pluralName, documentId, params);
4656
+ const response = await col.findOne(documentId, params);
3630
4657
  /** @type {any} */
3631
4658
  const rawData = response.data;
3632
4659
  /** @type {T} */
@@ -3634,11 +4661,11 @@ function collection(pluralName) {
3634
4661
  return { data };
3635
4662
  },
3636
4663
  /**
3637
- * @param {{ data: Partial<T> }} data
4664
+ * @param {Partial<T>} data
3638
4665
  * @returns {Promise<{ data: T }>}
3639
4666
  */
3640
4667
  async create(data) {
3641
- const response = await strapi.create(pluralName, data.data);
4668
+ const response = await col.create(data);
3642
4669
  /** @type {any} */
3643
4670
  const rawData = response.data;
3644
4671
  /** @type {T} */
@@ -3647,11 +4674,11 @@ function collection(pluralName) {
3647
4674
  },
3648
4675
  /**
3649
4676
  * @param {string} documentId
3650
- * @param {{ data: Partial<T> }} data
4677
+ * @param {Partial<T>} data
3651
4678
  * @returns {Promise<{ data: T }>}
3652
4679
  */
3653
4680
  async update(documentId, data) {
3654
- const response = await strapi.update(pluralName, documentId, data.data);
4681
+ const response = await col.update(documentId, data);
3655
4682
  /** @type {any} */
3656
4683
  const rawData = response.data;
3657
4684
  /** @type {T} */
@@ -3663,7 +4690,7 @@ function collection(pluralName) {
3663
4690
  * @returns {Promise<void>}
3664
4691
  */
3665
4692
  async delete(documentId) {
3666
- await strapi.delete(pluralName, documentId);
4693
+ await col.delete(documentId);
3667
4694
  },
3668
4695
  };
3669
4696
  }
@@ -3674,13 +4701,14 @@ function collection(pluralName) {
3674
4701
  * @param {string} singularName
3675
4702
  */
3676
4703
  function single(singularName) {
4704
+ const singleType = strapiClient.single(singularName);
3677
4705
  return {
3678
4706
  /**
3679
4707
  * @param {Record<string, unknown>} [params]
3680
4708
  * @returns {Promise<{ data: T }>}
3681
4709
  */
3682
4710
  async find(params) {
3683
- const response = await strapi.find(singularName, params);
4711
+ const response = await singleType.find(params);
3684
4712
  /** @type {any} */
3685
4713
  const rawData = response.data;
3686
4714
  /** @type {T} */
@@ -3688,11 +4716,11 @@ function single(singularName) {
3688
4716
  return { data };
3689
4717
  },
3690
4718
  /**
3691
- * @param {{ data: Partial<T> }} data
4719
+ * @param {Partial<T>} data
3692
4720
  * @returns {Promise<{ data: T }>}
3693
4721
  */
3694
4722
  async update(data) {
3695
- const response = await strapi.update(singularName, String(1), data.data);
4723
+ const response = await singleType.update(data);
3696
4724
  /** @type {any} */
3697
4725
  const rawData = response.data;
3698
4726
  /** @type {T} */
@@ -3703,12 +4731,77 @@ function single(singularName) {
3703
4731
  * @returns {Promise<void>}
3704
4732
  */
3705
4733
  async delete() {
3706
- await strapi.delete(singularName, String(1));
4734
+ await singleType.delete();
3707
4735
  },
3708
4736
  };
3709
4737
  }
3710
4738
 
3711
- ${useESM ? "export { strapi, collection, single };" : "module.exports = { strapi, collection, single };"}
4739
+ /**
4740
+ * File management helpers
4741
+ * Wraps @strapi/client file methods with proper typing
4742
+ * @see https://docs.strapi.io/cms/api/client#working-with-files
4743
+ */
4744
+ const files = {
4745
+ /**
4746
+ * Upload a file to Strapi
4747
+ * @param {File|Blob} file
4748
+ * @param {{ fileInfo?: import('./utils').StrapiFileInfo }} [options]
4749
+ * @returns {Promise<import('./utils').StrapiMedia>}
4750
+ * @see https://docs.strapi.io/cms/api/client#upload
4751
+ */
4752
+ async upload(file, options) {
4753
+ /** @type {any} */
4754
+ const response = await strapiClient.files.upload(file, options);
4755
+ return response;
4756
+ },
4757
+
4758
+ /**
4759
+ * Find files with optional filtering and sorting
4760
+ * @param {Record<string, unknown>} [params]
4761
+ * @returns {Promise<import('./utils').StrapiMedia[]>}
4762
+ */
4763
+ async find(params) {
4764
+ /** @type {any} */
4765
+ const response = await strapiClient.files.find(params);
4766
+ return Array.isArray(response) ? response : [];
4767
+ },
4768
+
4769
+ /**
4770
+ * Get a single file by ID
4771
+ * @param {number} fileId
4772
+ * @returns {Promise<import('./utils').StrapiMedia>}
4773
+ */
4774
+ async findOne(fileId) {
4775
+ /** @type {any} */
4776
+ const response = await strapiClient.files.findOne(fileId);
4777
+ return response;
4778
+ },
4779
+
4780
+ /**
4781
+ * Update file metadata (name, alternativeText, caption)
4782
+ * @param {number} fileId
4783
+ * @param {import('./utils').StrapiFileInfo} fileInfo
4784
+ * @returns {Promise<import('./utils').StrapiMedia>}
4785
+ */
4786
+ async update(fileId, fileInfo) {
4787
+ /** @type {any} */
4788
+ const response = await strapiClient.files.update(fileId, fileInfo);
4789
+ return response;
4790
+ },
4791
+
4792
+ /**
4793
+ * Delete a file by ID
4794
+ * @param {number} fileId
4795
+ * @returns {Promise<import('./utils').StrapiMedia>}
4796
+ */
4797
+ async delete(fileId) {
4798
+ /** @type {any} */
4799
+ const response = await strapiClient.files.delete(fileId);
4800
+ return response;
4801
+ },
4802
+ };
4803
+
4804
+ ${useESM ? "export { strapiClient, collection, single, files };" : "module.exports = { strapiClient, collection, single, files };"}
3712
4805
  `;
3713
4806
  }
3714
4807
  function generateLocalesFileJSDoc(locales, useESM = false) {
@@ -4086,7 +5179,7 @@ ${hasSlug ? `
4086
5179
  * @returns {Promise<import('./types').${typeName}>}
4087
5180
  */
4088
5181
  async create(data) {
4089
- const response = await ${toCamelCase(collection.singularName)}Collection.create({ data });
5182
+ const response = await ${toCamelCase(collection.singularName)}Collection.create(data);
4090
5183
  return response.data;
4091
5184
  },
4092
5185
 
@@ -4096,7 +5189,7 @@ ${hasSlug ? `
4096
5189
  * @returns {Promise<import('./types').${typeName}>}
4097
5190
  */
4098
5191
  async update(${idParam}, data) {
4099
- const response = await ${toCamelCase(collection.singularName)}Collection.update(${idParam}, { data });
5192
+ const response = await ${toCamelCase(collection.singularName)}Collection.update(${idParam}, data);
4100
5193
  return response.data;
4101
5194
  },
4102
5195
 
@@ -4174,7 +5267,7 @@ const ${serviceName} = {
4174
5267
  * @returns {Promise<import('./types').${typeName}>}
4175
5268
  */
4176
5269
  async update(data) {
4177
- const response = await ${toCamelCase(single.singularName)}Single.update({ data });
5270
+ const response = await ${toCamelCase(single.singularName)}Single.update(data);
4178
5271
  return response.data;
4179
5272
  },
4180
5273
 
@@ -4189,6 +5282,259 @@ const ${serviceName} = {
4189
5282
  ${useESM ? `export { ${serviceName} };` : `module.exports = { ${serviceName} };`}
4190
5283
  `;
4191
5284
  }
5285
+ function generateUploadClientTS() {
5286
+ return `/**
5287
+ * Public Upload Client
5288
+ * Generated by strapi2front
5289
+ *
5290
+ * Uploads files directly from the browser to Strapi using a restricted public token.
5291
+ * This token should ONLY have upload permissions (no delete, no update).
5292
+ *
5293
+ * Required environment variables:
5294
+ * PUBLIC_STRAPI_URL - Your Strapi CMS base URL
5295
+ * PUBLIC_STRAPI_UPLOAD_TOKEN - Restricted API token (upload-only)
5296
+ *
5297
+ * Create the token in: Strapi Admin > Settings > API Tokens
5298
+ * Set permissions: Upload > upload (only)
5299
+ */
5300
+
5301
+ import type { StrapiMedia, StrapiFileInfo } from './utils';
5302
+
5303
+ const STRAPI_URL = import.meta.env.PUBLIC_STRAPI_URL || '';
5304
+ const UPLOAD_TOKEN = import.meta.env.PUBLIC_STRAPI_UPLOAD_TOKEN || '';
5305
+
5306
+ /**
5307
+ * Upload a single file to Strapi from the browser
5308
+ */
5309
+ export async function uploadFile(
5310
+ file: File,
5311
+ fileInfo?: StrapiFileInfo
5312
+ ): Promise<StrapiMedia> {
5313
+ const formData = new FormData();
5314
+ formData.append('files', file);
5315
+
5316
+ if (fileInfo) {
5317
+ formData.append('fileInfo', JSON.stringify(fileInfo));
5318
+ }
5319
+
5320
+ const response = await fetch(\`\${STRAPI_URL}/api/upload\`, {
5321
+ method: 'POST',
5322
+ headers: {
5323
+ Authorization: \`Bearer \${UPLOAD_TOKEN}\`,
5324
+ },
5325
+ body: formData,
5326
+ });
5327
+
5328
+ if (!response.ok) {
5329
+ throw new Error(\`Upload failed: \${response.status} \${response.statusText}\`);
5330
+ }
5331
+
5332
+ const data = await response.json();
5333
+ return Array.isArray(data) ? data[0] : data;
5334
+ }
5335
+
5336
+ /**
5337
+ * Upload multiple files to Strapi from the browser
5338
+ */
5339
+ export async function uploadFiles(
5340
+ files: File[],
5341
+ fileInfo?: StrapiFileInfo
5342
+ ): Promise<StrapiMedia[]> {
5343
+ const formData = new FormData();
5344
+ files.forEach((f) => formData.append('files', f));
5345
+
5346
+ if (fileInfo) {
5347
+ formData.append('fileInfo', JSON.stringify(fileInfo));
5348
+ }
5349
+
5350
+ const response = await fetch(\`\${STRAPI_URL}/api/upload\`, {
5351
+ method: 'POST',
5352
+ headers: {
5353
+ Authorization: \`Bearer \${UPLOAD_TOKEN}\`,
5354
+ },
5355
+ body: formData,
5356
+ });
5357
+
5358
+ if (!response.ok) {
5359
+ throw new Error(\`Upload failed: \${response.status} \${response.statusText}\`);
5360
+ }
5361
+
5362
+ return response.json();
5363
+ }
5364
+ `;
5365
+ }
5366
+ function generateUploadClientJSDoc(useESM) {
5367
+ return `/**
5368
+ * Public Upload Client
5369
+ * Generated by strapi2front
5370
+ *
5371
+ * Uploads files directly from the browser to Strapi using a restricted public token.
5372
+ * This token should ONLY have upload permissions (no delete, no update).
5373
+ *
5374
+ * Required environment variables:
5375
+ * PUBLIC_STRAPI_URL - Your Strapi CMS base URL
5376
+ * PUBLIC_STRAPI_UPLOAD_TOKEN - Restricted API token (upload-only)
5377
+ *
5378
+ * Create the token in: Strapi Admin > Settings > API Tokens
5379
+ * Set permissions: Upload > upload (only)
5380
+ */
5381
+
5382
+ /** @typedef {import('./utils').StrapiMedia} StrapiMedia */
5383
+ /** @typedef {import('./utils').StrapiFileInfo} StrapiFileInfo */
5384
+
5385
+ const STRAPI_URL = ${useESM ? "import.meta.env.PUBLIC_STRAPI_URL || ''" : "process.env.PUBLIC_STRAPI_URL || ''"};
5386
+ const UPLOAD_TOKEN = ${useESM ? "import.meta.env.PUBLIC_STRAPI_UPLOAD_TOKEN || ''" : "process.env.PUBLIC_STRAPI_UPLOAD_TOKEN || ''"};
5387
+
5388
+ /**
5389
+ * Upload a single file to Strapi from the browser
5390
+ * @param {File} file - The file to upload
5391
+ * @param {StrapiFileInfo} [fileInfo] - Optional file metadata
5392
+ * @returns {Promise<StrapiMedia>}
5393
+ */
5394
+ ${useESM ? "export " : ""}async function uploadFile(file, fileInfo) {
5395
+ const formData = new FormData();
5396
+ formData.append('files', file);
5397
+
5398
+ if (fileInfo) {
5399
+ formData.append('fileInfo', JSON.stringify(fileInfo));
5400
+ }
5401
+
5402
+ const response = await fetch(\`\${STRAPI_URL}/api/upload\`, {
5403
+ method: 'POST',
5404
+ headers: {
5405
+ Authorization: \`Bearer \${UPLOAD_TOKEN}\`,
5406
+ },
5407
+ body: formData,
5408
+ });
5409
+
5410
+ if (!response.ok) {
5411
+ throw new Error(\`Upload failed: \${response.status} \${response.statusText}\`);
5412
+ }
5413
+
5414
+ const data = await response.json();
5415
+ return Array.isArray(data) ? data[0] : data;
5416
+ }
5417
+
5418
+ /**
5419
+ * Upload multiple files to Strapi from the browser
5420
+ * @param {File[]} files - The files to upload
5421
+ * @param {StrapiFileInfo} [fileInfo] - Optional file metadata (applied to all)
5422
+ * @returns {Promise<StrapiMedia[]>}
5423
+ */
5424
+ ${useESM ? "export " : ""}async function uploadFiles(files, fileInfo) {
5425
+ const formData = new FormData();
5426
+ files.forEach((f) => formData.append('files', f));
5427
+
5428
+ if (fileInfo) {
5429
+ formData.append('fileInfo', JSON.stringify(fileInfo));
5430
+ }
5431
+
5432
+ const response = await fetch(\`\${STRAPI_URL}/api/upload\`, {
5433
+ method: 'POST',
5434
+ headers: {
5435
+ Authorization: \`Bearer \${UPLOAD_TOKEN}\`,
5436
+ },
5437
+ body: formData,
5438
+ });
5439
+
5440
+ if (!response.ok) {
5441
+ throw new Error(\`Upload failed: \${response.status} \${response.statusText}\`);
5442
+ }
5443
+
5444
+ return response.json();
5445
+ }
5446
+
5447
+ ${useESM ? "" : "module.exports = { uploadFile, uploadFiles };"}
5448
+ `;
5449
+ }
5450
+ function generateUploadActionTS() {
5451
+ return `/**
5452
+ * Upload Action
5453
+ * Generated by strapi2front
5454
+ *
5455
+ * Astro Action that receives files via FormData and uploads them to Strapi server-side.
5456
+ * Uses the private STRAPI_TOKEN \u2014 never exposed to the browser.
5457
+ *
5458
+ * Usage:
5459
+ * import { actions } from 'astro:actions';
5460
+ * const result = await actions.upload({ file: myFile, alternativeText: 'My image' });
5461
+ *
5462
+ * Register this action in src/actions/index.ts:
5463
+ * import { uploadAction, uploadMultipleAction } from '../strapi/shared/upload-action';
5464
+ * export const server = { upload: uploadAction, uploadMultiple: uploadMultipleAction };
5465
+ */
5466
+
5467
+ import { defineAction, ActionError } from 'astro:actions';
5468
+ import { z } from 'astro:schema';
5469
+ import { files } from './client';
5470
+
5471
+ /**
5472
+ * Upload a single file via Astro Action (server-side, secure)
5473
+ */
5474
+ export const uploadAction = defineAction({
5475
+ accept: 'form',
5476
+ input: z.object({
5477
+ file: z.instanceof(File),
5478
+ name: z.string().optional(),
5479
+ alternativeText: z.string().optional(),
5480
+ caption: z.string().optional(),
5481
+ }),
5482
+ handler: async (input) => {
5483
+ try {
5484
+ const { file, name, alternativeText, caption } = input;
5485
+
5486
+ const fileInfo: Record<string, string> = {};
5487
+ if (name) fileInfo.name = name;
5488
+ if (alternativeText) fileInfo.alternativeText = alternativeText;
5489
+ if (caption) fileInfo.caption = caption;
5490
+
5491
+ const result = await files.upload(file, {
5492
+ fileInfo: Object.keys(fileInfo).length > 0 ? fileInfo : undefined,
5493
+ });
5494
+
5495
+ return result;
5496
+ } catch (error) {
5497
+ throw new ActionError({
5498
+ code: 'INTERNAL_SERVER_ERROR',
5499
+ message: error instanceof Error ? error.message : 'Upload failed',
5500
+ });
5501
+ }
5502
+ },
5503
+ });
5504
+
5505
+ /**
5506
+ * Upload multiple files via Astro Action (server-side, secure)
5507
+ */
5508
+ export const uploadMultipleAction = defineAction({
5509
+ accept: 'form',
5510
+ input: z.object({
5511
+ files: z.array(z.instanceof(File)).min(1),
5512
+ alternativeText: z.string().optional(),
5513
+ caption: z.string().optional(),
5514
+ }),
5515
+ handler: async (input) => {
5516
+ try {
5517
+ const results = await Promise.all(
5518
+ input.files.map((file) =>
5519
+ files.upload(file, {
5520
+ fileInfo: {
5521
+ ...(input.alternativeText && { alternativeText: input.alternativeText }),
5522
+ ...(input.caption && { caption: input.caption }),
5523
+ },
5524
+ })
5525
+ )
5526
+ );
5527
+ return results;
5528
+ } catch (error) {
5529
+ throw new ActionError({
5530
+ code: 'INTERNAL_SERVER_ERROR',
5531
+ message: error instanceof Error ? error.message : 'Upload failed',
5532
+ });
5533
+ }
5534
+ },
5535
+ });
5536
+ `;
5537
+ }
4192
5538
 
4193
5539
  // src/frameworks/nextjs/actions.ts
4194
5540
  async function generateNextJsActions(_schema, _options) {
@@ -4222,7 +5568,176 @@ var frameworkSupport = {
4222
5568
  nextjs: { status: "coming-soon", minVersion: "14.0.0" },
4223
5569
  nuxt: { status: "coming-soon", minVersion: "3.0.0" }
4224
5570
  };
5571
+ async function generateZodSchemas(schema, options) {
5572
+ const { outputDir, byFeature = false, strapiVersion = "v5" } = options;
5573
+ const generatedFiles = [];
5574
+ await ensureDir(outputDir);
5575
+ if (byFeature) {
5576
+ for (const collection of schema.collections) {
5577
+ const dirPath = path9.join(outputDir, "collections", toKebabCase(collection.singularName));
5578
+ await ensureDir(dirPath);
5579
+ const filePath = path9.join(dirPath, "schemas.ts");
5580
+ const content = generateCollectionSchemas2(collection, schema.components, strapiVersion);
5581
+ await writeFile(filePath, await formatCode(content));
5582
+ generatedFiles.push({
5583
+ filePath,
5584
+ schemas: [
5585
+ `${toCamelCase(collection.singularName)}CreateSchema`,
5586
+ `${toCamelCase(collection.singularName)}UpdateSchema`
5587
+ ]
5588
+ });
5589
+ }
5590
+ for (const single of schema.singles) {
5591
+ const dirPath = path9.join(outputDir, "singles", toKebabCase(single.singularName));
5592
+ await ensureDir(dirPath);
5593
+ const filePath = path9.join(dirPath, "schemas.ts");
5594
+ const content = generateSingleSchemas2(single, schema.components, strapiVersion);
5595
+ await writeFile(filePath, await formatCode(content));
5596
+ generatedFiles.push({
5597
+ filePath,
5598
+ schemas: [`${toCamelCase(single.singularName)}UpdateSchema`]
5599
+ });
5600
+ }
5601
+ } else {
5602
+ const filePath = path9.join(outputDir, "schemas.ts");
5603
+ const content = generateAllSchemas(schema, strapiVersion);
5604
+ await writeFile(filePath, await formatCode(content));
5605
+ const allSchemas = [];
5606
+ for (const collection of schema.collections) {
5607
+ allSchemas.push(`${toCamelCase(collection.singularName)}CreateSchema`);
5608
+ allSchemas.push(`${toCamelCase(collection.singularName)}UpdateSchema`);
5609
+ }
5610
+ for (const single of schema.singles) {
5611
+ allSchemas.push(`${toCamelCase(single.singularName)}UpdateSchema`);
5612
+ }
5613
+ generatedFiles.push({ filePath, schemas: allSchemas });
5614
+ }
5615
+ return generatedFiles;
5616
+ }
5617
+ function generateAllSchemas(schema, strapiVersion = "v5") {
5618
+ const sections = [];
5619
+ sections.push(`/**
5620
+ * Zod Validation Schemas
5621
+ * Generated by strapi2front
5622
+ *
5623
+ * These schemas can be used for form validation, API input validation, etc.
5624
+ */
5625
+
5626
+ import { z } from 'zod';
5627
+ `);
5628
+ if (schema.collections.length > 0) {
5629
+ sections.push("// ============================================");
5630
+ sections.push("// Collection Schemas");
5631
+ sections.push("// ============================================\n");
5632
+ for (const collection of schema.collections) {
5633
+ sections.push(generateCollectionSchemaContent(collection, schema.components, strapiVersion));
5634
+ sections.push("");
5635
+ }
5636
+ }
5637
+ if (schema.singles.length > 0) {
5638
+ sections.push("// ============================================");
5639
+ sections.push("// Single Type Schemas");
5640
+ sections.push("// ============================================\n");
5641
+ for (const single of schema.singles) {
5642
+ sections.push(generateSingleSchemaContent(single, schema.components, strapiVersion));
5643
+ sections.push("");
5644
+ }
5645
+ }
5646
+ sections.push("// ============================================");
5647
+ sections.push("// Schema Exports");
5648
+ sections.push("// ============================================\n");
5649
+ const exports$1 = [];
5650
+ for (const collection of schema.collections) {
5651
+ const name = toCamelCase(collection.singularName);
5652
+ exports$1.push(` ${name}CreateSchema,`);
5653
+ exports$1.push(` ${name}UpdateSchema,`);
5654
+ }
5655
+ for (const single of schema.singles) {
5656
+ const name = toCamelCase(single.singularName);
5657
+ exports$1.push(` ${name}UpdateSchema,`);
5658
+ }
5659
+ sections.push(`export const schemas = {
5660
+ ${exports$1.join("\n")}
5661
+ };`);
5662
+ return sections.join("\n");
5663
+ }
5664
+ function generateCollectionSchemas2(collection, _components, strapiVersion = "v5") {
5665
+ const name = toCamelCase(collection.singularName);
5666
+ const pascalName = toPascalCase(collection.singularName);
5667
+ return `/**
5668
+ * ${collection.displayName} Zod Schemas
5669
+ * ${collection.description || ""}
5670
+ * Generated by strapi2front
5671
+ */
5672
+
5673
+ import { z } from 'zod';
5674
+
5675
+ ${generateCollectionSchemaContent(collection, _components, strapiVersion)}
5676
+
5677
+ // Type inference helpers
5678
+ export type ${pascalName}CreateInput = z.infer<typeof ${name}CreateSchema>;
5679
+ export type ${pascalName}UpdateInput = z.infer<typeof ${name}UpdateSchema>;
5680
+ `;
5681
+ }
5682
+ function generateSingleSchemas2(single, _components, strapiVersion = "v5") {
5683
+ const name = toCamelCase(single.singularName);
5684
+ const pascalName = toPascalCase(single.singularName);
5685
+ return `/**
5686
+ * ${single.displayName} Zod Schemas
5687
+ * ${single.description || ""}
5688
+ * Generated by strapi2front
5689
+ */
5690
+
5691
+ import { z } from 'zod';
5692
+
5693
+ ${generateSingleSchemaContent(single, _components, strapiVersion)}
5694
+
5695
+ // Type inference helpers
5696
+ export type ${pascalName}UpdateInput = z.infer<typeof ${name}UpdateSchema>;
5697
+ `;
5698
+ }
5699
+ function generateCollectionSchemaContent(collection, _components, strapiVersion = "v5") {
5700
+ const name = toCamelCase(collection.singularName);
5701
+ const sections = [];
5702
+ const createOptions = {
5703
+ isUpdate: false,
5704
+ strapiVersion
5705
+ };
5706
+ const updateOptions = {
5707
+ isUpdate: true,
5708
+ strapiVersion
5709
+ };
5710
+ const createResult = generateZodObjectSchema(collection.attributes, createOptions);
5711
+ sections.push(`/**
5712
+ * Schema for creating a new ${collection.displayName}
5713
+ */
5714
+ export const ${name}CreateSchema = ${createResult.schema};`);
5715
+ if (createResult.skippedFields.length > 0) {
5716
+ sections.push(`// Skipped fields: ${createResult.skippedFields.map((f) => f.name).join(", ")}`);
5717
+ }
5718
+ sections.push("");
5719
+ const updateResult = generateZodObjectSchema(collection.attributes, updateOptions);
5720
+ sections.push(`/**
5721
+ * Schema for updating a ${collection.displayName}
5722
+ * All fields are optional for partial updates
5723
+ */
5724
+ export const ${name}UpdateSchema = ${updateResult.schema};`);
5725
+ return sections.join("\n");
5726
+ }
5727
+ function generateSingleSchemaContent(single, _components, strapiVersion = "v5") {
5728
+ const name = toCamelCase(single.singularName);
5729
+ const updateOptions = {
5730
+ isUpdate: true,
5731
+ strapiVersion
5732
+ };
5733
+ const updateResult = generateZodObjectSchema(single.attributes, updateOptions);
5734
+ return `/**
5735
+ * Schema for updating ${single.displayName}
5736
+ * All fields are optional for partial updates
5737
+ */
5738
+ export const ${name}UpdateSchema = ${updateResult.schema};`;
5739
+ }
4225
5740
 
4226
- export { deleteFile, ensureDir, fileExists, formatCode, formatJson, frameworkSupport, generateActions, generateAstroActions, generateByFeature, generateClient, generateJSDocServices, generateJSDocTypes, generateLocales, generateNextJsActions, generateNuxtServerRoutes, generateServices, generateTypeScriptTypes, generateTypes, isAstroActionsSupported, isNextJsActionsSupported, isNuxtServerRoutesSupported, listFiles, pluralize, readFile, toCamelCase, toKebabCase, toPascalCase, writeFile };
5741
+ export { deleteFile, ensureDir, fileExists, formatCode, formatJson, frameworkSupport, generateActions, generateAstroActions, generateByFeature, generateClient, generateJSDocServices, generateJSDocTypes, generateLocales, generateNextJsActions, generateNuxtServerRoutes, generateServices, generateTypeScriptTypes, generateTypes, generateZodObjectSchema, generateZodSchemas, isAstroActionsSupported, isNextJsActionsSupported, isNuxtServerRoutesSupported, isSystemField, listFiles, mapAttributeToZodSchema, pluralize, readFile, toCamelCase, toKebabCase, toPascalCase, writeFile };
4227
5742
  //# sourceMappingURL=index.js.map
4228
5743
  //# sourceMappingURL=index.js.map