@betterstart/cli 0.1.67 → 0.1.69

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/cli.js CHANGED
@@ -1753,7 +1753,18 @@ function generateFormActions(schema, cwd, actionsDir, options) {
1753
1753
  const formName = schema.name;
1754
1754
  const tableName = `${toCamelCase(formName)}Submissions`;
1755
1755
  const pascal = toPascalCase(formName);
1756
+ const kebab = toKebabCase(formName);
1756
1757
  const fields = getAllFormSchemaFields(schema);
1758
+ const absActionsDir = path6.join(cwd, actionsDir);
1759
+ const dirPath = path6.join(absActionsDir, `${kebab}-form`);
1760
+ const oldFilePath = path6.join(absActionsDir, `${kebab}-form.ts`);
1761
+ if (!options.force && (fs6.existsSync(dirPath) || fs6.existsSync(oldFilePath))) {
1762
+ return { files: [] };
1763
+ }
1764
+ if (options.force) {
1765
+ if (fs6.existsSync(oldFilePath)) fs6.unlinkSync(oldFilePath);
1766
+ if (fs6.existsSync(dirPath)) fs6.rmSync(dirPath, { recursive: true });
1767
+ }
1757
1768
  const dataFields = fields.map((f) => {
1758
1769
  let tsType = "string";
1759
1770
  if (f.type === "number") tsType = "number";
@@ -1772,25 +1783,10 @@ function generateFormActions(schema, cwd, actionsDir, options) {
1772
1783
  return { success: false, error: '${f.label} is required' }
1773
1784
  }`
1774
1785
  ).join("\n ");
1775
- const kebab = toKebabCase(formName);
1776
- const filePath = path6.join(cwd, actionsDir, `${kebab}-form.ts`);
1777
- const dir = path6.dirname(filePath);
1778
- if (!fs6.existsSync(dir)) fs6.mkdirSync(dir, { recursive: true });
1779
- if (fs6.existsSync(filePath) && !options.force) {
1780
- return { files: [path6.relative(cwd, filePath)] };
1781
- }
1782
1786
  const mailchimpImport = schema.mailchimp ? `
1783
1787
  import { addToMailchimpAudience } from '@cms/utils/mailchimp'` : "";
1784
1788
  const mailchimpCall = generateMailchimpCode(schema, fields);
1785
- const content = `'use server'
1786
-
1787
- import { desc, eq, or } from 'drizzle-orm'
1788
- import db from '@cms/db'
1789
- import { ${tableName} } from '@cms/db/schema'
1790
- import { getFormSettings } from '@cms/actions/form-settings'
1791
- import { sendWebhook } from '@cms/utils/webhook'${mailchimpImport}
1792
-
1793
- export interface ${pascal}SubmissionData {
1789
+ const typesContent = `export interface ${pascal}SubmissionData {
1794
1790
  id: number
1795
1791
  ${dataFields}
1796
1792
  ipAddress: string | null
@@ -1822,6 +1818,13 @@ export interface Delete${pascal}SubmissionResult {
1822
1818
  error?: string
1823
1819
  count?: number
1824
1820
  }
1821
+ `;
1822
+ const getSubmissionsContent = `'use server'
1823
+
1824
+ import { desc } from 'drizzle-orm'
1825
+ import db from '@cms/db'
1826
+ import { ${tableName} } from '@cms/db/schema'
1827
+ import type { ${pascal}SubmissionsResponse, ${pascal}SubmissionData } from './types'
1825
1828
 
1826
1829
  export async function get${pascal}Submissions(): Promise<${pascal}SubmissionsResponse> {
1827
1830
  try {
@@ -1835,6 +1838,13 @@ export async function get${pascal}Submissions(): Promise<${pascal}SubmissionsRes
1835
1838
  throw new Error('Failed to fetch ${formName} submissions')
1836
1839
  }
1837
1840
  }
1841
+ `;
1842
+ const getSubmissionContent = `'use server'
1843
+
1844
+ import { eq } from 'drizzle-orm'
1845
+ import db from '@cms/db'
1846
+ import { ${tableName} } from '@cms/db/schema'
1847
+ import type { ${pascal}SubmissionData } from './types'
1838
1848
 
1839
1849
  export async function get${pascal}Submission(id: number): Promise<${pascal}SubmissionData | null> {
1840
1850
  try {
@@ -1849,6 +1859,14 @@ export async function get${pascal}Submission(id: number): Promise<${pascal}Submi
1849
1859
  throw new Error('Failed to fetch ${formName} submission')
1850
1860
  }
1851
1861
  }
1862
+ `;
1863
+ const createContent = `'use server'
1864
+
1865
+ import db from '@cms/db'
1866
+ import { ${tableName} } from '@cms/db/schema'
1867
+ import { getFormSettings } from '@cms/actions/form-settings'
1868
+ import { sendWebhook } from '@cms/utils/webhook'${mailchimpImport}
1869
+ import type { Create${pascal}SubmissionInput, Create${pascal}SubmissionResult, ${pascal}SubmissionData } from './types'
1852
1870
 
1853
1871
  export async function create${pascal}Submission(
1854
1872
  data: Create${pascal}SubmissionInput
@@ -1899,6 +1917,13 @@ export async function create${pascal}Submission(
1899
1917
  }
1900
1918
  }
1901
1919
  }
1920
+ `;
1921
+ const deleteContent = `'use server'
1922
+
1923
+ import { eq } from 'drizzle-orm'
1924
+ import db from '@cms/db'
1925
+ import { ${tableName} } from '@cms/db/schema'
1926
+ import type { Delete${pascal}SubmissionResult } from './types'
1902
1927
 
1903
1928
  export async function delete${pascal}Submission(id: number): Promise<Delete${pascal}SubmissionResult> {
1904
1929
  try {
@@ -1912,6 +1937,13 @@ export async function delete${pascal}Submission(id: number): Promise<Delete${pas
1912
1937
  }
1913
1938
  }
1914
1939
  }
1940
+ `;
1941
+ const deleteBulkContent = `'use server'
1942
+
1943
+ import { eq, or } from 'drizzle-orm'
1944
+ import db from '@cms/db'
1945
+ import { ${tableName} } from '@cms/db/schema'
1946
+ import type { Delete${pascal}SubmissionResult } from './types'
1915
1947
 
1916
1948
  export async function deleteBulk${pascal}Submissions(ids: number[]): Promise<Delete${pascal}SubmissionResult> {
1917
1949
  try {
@@ -1928,6 +1960,10 @@ export async function deleteBulk${pascal}Submissions(ids: number[]): Promise<Del
1928
1960
  }
1929
1961
  }
1930
1962
  }
1963
+ `;
1964
+ const exportCsvContent = `'use server'
1965
+
1966
+ import { get${pascal}Submissions } from './get-${kebab}-submissions'
1931
1967
 
1932
1968
  export async function export${pascal}SubmissionsCSV(): Promise<string> {
1933
1969
  const { submissions } = await get${pascal}Submissions()
@@ -1947,14 +1983,41 @@ export async function export${pascal}SubmissionsCSV(): Promise<string> {
1947
1983
  )
1948
1984
  return [headers, ...rows].join('\\n')
1949
1985
  }
1986
+ `;
1987
+ const exportJsonContent = `'use server'
1988
+
1989
+ import { get${pascal}Submissions } from './get-${kebab}-submissions'
1950
1990
 
1951
1991
  export async function export${pascal}SubmissionsJSON(): Promise<string> {
1952
1992
  const { submissions } = await get${pascal}Submissions()
1953
1993
  return JSON.stringify(submissions, null, 2)
1954
1994
  }
1955
1995
  `;
1956
- fs6.writeFileSync(filePath, content, "utf-8");
1957
- return { files: [path6.relative(cwd, filePath)] };
1996
+ const barrelContent = `export type { ${pascal}SubmissionData, ${pascal}SubmissionsResponse, Create${pascal}SubmissionInput, Create${pascal}SubmissionResult, Delete${pascal}SubmissionResult } from './types'
1997
+ export { get${pascal}Submissions } from './get-${kebab}-submissions'
1998
+ export { get${pascal}Submission } from './get-${kebab}-submission'
1999
+ export { create${pascal}Submission } from './create-${kebab}-submission'
2000
+ export { delete${pascal}Submission } from './delete-${kebab}-submission'
2001
+ export { deleteBulk${pascal}Submissions } from './delete-bulk-${kebab}-submissions'
2002
+ export { export${pascal}SubmissionsCSV } from './export-${kebab}-submissions-csv'
2003
+ export { export${pascal}SubmissionsJSON } from './export-${kebab}-submissions-json'
2004
+ `;
2005
+ const files = [
2006
+ { name: "types.ts", content: typesContent },
2007
+ { name: `get-${kebab}-submissions.ts`, content: getSubmissionsContent },
2008
+ { name: `get-${kebab}-submission.ts`, content: getSubmissionContent },
2009
+ { name: `create-${kebab}-submission.ts`, content: createContent },
2010
+ { name: `delete-${kebab}-submission.ts`, content: deleteContent },
2011
+ { name: `delete-bulk-${kebab}-submissions.ts`, content: deleteBulkContent },
2012
+ { name: `export-${kebab}-submissions-csv.ts`, content: exportCsvContent },
2013
+ { name: `export-${kebab}-submissions-json.ts`, content: exportJsonContent },
2014
+ { name: "index.ts", content: barrelContent }
2015
+ ];
2016
+ fs6.mkdirSync(dirPath, { recursive: true });
2017
+ for (const file of files) {
2018
+ fs6.writeFileSync(path6.join(dirPath, file.name), file.content, "utf-8");
2019
+ }
2020
+ return { files: files.map((f) => path6.relative(cwd, path6.join(dirPath, f.name))) };
1958
2021
  }
1959
2022
 
1960
2023
  // src/generators/form-pipeline/form-component-multistep.ts
@@ -3400,16 +3463,527 @@ ${blocks.join("\n\n")}
3400
3463
 
3401
3464
  return record
3402
3465
  }
3403
- `;
3466
+ `;
3467
+ }
3468
+
3469
+ // src/generators/actions/entity-file-contents.ts
3470
+ function genTypesContent(ctx) {
3471
+ const parts = [
3472
+ ctx.dataInterface,
3473
+ ctx.displayDataInterface || null,
3474
+ ctx.responseInterface,
3475
+ ctx.filtersInterface,
3476
+ ctx.createInterface,
3477
+ `export interface Create${ctx.Singular}Result {
3478
+ success: boolean
3479
+ error?: string
3480
+ ${ctx.camelSingular}?: ${ctx.Singular}Data
3481
+ }`,
3482
+ ctx.updateInterface,
3483
+ `export interface Update${ctx.Singular}Result {
3484
+ success: boolean
3485
+ error?: string
3486
+ ${ctx.camelSingular}?: ${ctx.Singular}Data
3487
+ }`,
3488
+ `export interface Delete${ctx.Singular}Result {
3489
+ success: boolean
3490
+ error?: string
3491
+ }`,
3492
+ `export const CACHE_TAG = '${ctx.cacheTag}'`
3493
+ ];
3494
+ return parts.filter(Boolean).join("\n\n") + "\n";
3495
+ }
3496
+ function genHelpersContent(ctx) {
3497
+ const hasSlugify = ctx.hasAutoSlug;
3498
+ const hasPopulate = ctx.hasListRels;
3499
+ if (!hasSlugify && !hasPopulate) return null;
3500
+ const lines = [];
3501
+ if (hasPopulate) {
3502
+ lines.push(`import db from '@cms/db'`);
3503
+ const tables = [...new Set(ctx.listRelTableImports)].sort();
3504
+ if (tables.length > 0) {
3505
+ lines.push(`import { ${tables.join(", ")} } from '@cms/db/schema'`);
3506
+ }
3507
+ lines.push(`import { inArray } from 'drizzle-orm'`);
3508
+ lines.push(`import type { ${ctx.Singular}Data } from './types'`);
3509
+ lines.push("");
3510
+ }
3511
+ if (hasSlugify) {
3512
+ lines.push(`export function slugify(text: string): string {`);
3513
+ lines.push(` return text.toLowerCase().trim().replace(/[^\\w\\s-]/g, '').replace(/\\s+/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, '')`);
3514
+ lines.push(`}`);
3515
+ lines.push("");
3516
+ }
3517
+ if (hasPopulate) {
3518
+ const exportedFn = ctx.populateListRelsFn.replace("async function", "export async function").trim();
3519
+ lines.push(exportedFn);
3520
+ lines.push("");
3521
+ }
3522
+ return lines.join("\n");
3523
+ }
3524
+ function genGetPluralContent(ctx) {
3525
+ const dbImports = [ctx.tableVar, ...ctx.relTableImports].sort();
3526
+ const drizzle = ["asc"];
3527
+ if (ctx.hasSearch || ctx.filterableFields.length > 1) drizzle.push("and");
3528
+ if (ctx.hasSearch) drizzle.push("ilike", "or");
3529
+ if (ctx.filterableFields.some(() => true)) drizzle.push("eq");
3530
+ if (ctx.hasRelationships) drizzle.push("sql");
3531
+ const sortedDrizzle = [...new Set(drizzle)].sort();
3532
+ const typeImports = [`${ctx.Plural}Response`, `Get${ctx.Plural}Filters`, `${ctx.Singular}Data`];
3533
+ const populateImport = ctx.hasListRels ? `
3534
+ import { populate${ctx.Singular}ListRelationships } from './helpers'` : "";
3535
+ return `'use server'
3536
+
3537
+ import db from '@cms/db'
3538
+ import { ${dbImports.join(", ")} } from '@cms/db/schema'
3539
+ import { ${sortedDrizzle.join(", ")} } from 'drizzle-orm'
3540
+ import type { ${typeImports.sort().join(", ")} } from './types'${populateImport}
3541
+
3542
+ export async function get${ctx.Plural}(filters?: Get${ctx.Plural}Filters): Promise<${ctx.Plural}Response> {
3543
+ try {
3544
+ const conditions = []
3545
+ ${ctx.searchBlock}${ctx.filterConditions}
3546
+
3547
+ const query = ${ctx.listSelectClause}
3548
+
3549
+ const orderedQuery = conditions.length > 0
3550
+ ? query.where(and(...conditions)).orderBy(asc(${ctx.tableVar}.sortOrder))
3551
+ : query.orderBy(asc(${ctx.tableVar}.sortOrder))
3552
+
3553
+ const results = filters?.limit && filters.limit > 0
3554
+ ? await orderedQuery.limit(filters.limit)
3555
+ : await orderedQuery${ctx.listResultMapping}
3556
+ } catch (error) {
3557
+ console.error('Error fetching ${ctx.plural}:', error)
3558
+ return { ${ctx.camelPlural}: [], total: 0 }
3559
+ }
3560
+ }
3561
+ `;
3562
+ }
3563
+ function genGetByIdContent(ctx) {
3564
+ const dbImports = [ctx.tableVar, ...ctx.relTableImports].sort();
3565
+ const drizzle = ["eq"];
3566
+ if (ctx.hasRelationships) drizzle.push("sql");
3567
+ const sortedDrizzle = [...new Set(drizzle)].sort();
3568
+ const populateImport = ctx.hasListRels ? `
3569
+ import { populate${ctx.Singular}ListRelationships } from './helpers'` : "";
3570
+ return `'use server'
3571
+
3572
+ import db from '@cms/db'
3573
+ import { ${dbImports.join(", ")} } from '@cms/db/schema'
3574
+ import { ${sortedDrizzle.join(", ")} } from 'drizzle-orm'
3575
+ import type { ${ctx.Singular}Data } from './types'${populateImport}
3576
+
3577
+ export async function get${ctx.Singular}ById(id: number): Promise<${ctx.Singular}Data | null> {
3578
+ try {
3579
+ const result = await ${ctx.selectClause}.where(eq(${ctx.tableVar}.id, id)).limit(1)
3580
+ if (result.length === 0) return null
3581
+ ${ctx.singleRowReturn}
3582
+ } catch (error) {
3583
+ console.error('Error fetching ${ctx.singular}:', error)
3584
+ return null
3585
+ }
3586
+ }
3587
+ `;
3588
+ }
3589
+ function genGetBySlugContent(ctx) {
3590
+ if (!ctx.hasSlug) return null;
3591
+ const returnType = ctx.hasHtmlOutput ? `${ctx.Singular}DisplayData` : `${ctx.Singular}Data`;
3592
+ const dbImports = [ctx.tableVar, ...ctx.relTableImports].sort();
3593
+ const drizzle = ["eq"];
3594
+ if (ctx.hasRelationships) drizzle.push("sql");
3595
+ const sortedDrizzle = [...new Set(drizzle)].sort();
3596
+ const typeImport = ctx.hasHtmlOutput ? `${ctx.Singular}DisplayData` : `${ctx.Singular}Data`;
3597
+ const extraTypes = ctx.hasListRels && ctx.hasHtmlOutput ? `, ${ctx.Singular}Data` : "";
3598
+ const populateImport = ctx.hasListRels ? `
3599
+ import { populate${ctx.Singular}ListRelationships } from './helpers'` : "";
3600
+ return `'use server'
3601
+
3602
+ import db from '@cms/db'
3603
+ import { ${dbImports.join(", ")} } from '@cms/db/schema'
3604
+ import { ${sortedDrizzle.join(", ")} } from 'drizzle-orm'
3605
+ import type { ${typeImport}${extraTypes} } from './types'${populateImport}
3606
+
3607
+ export async function get${ctx.Singular}BySlug(slug: string): Promise<${returnType} | null> {
3608
+ try {
3609
+ const result = await ${ctx.displaySelectClause}.where(eq(${ctx.tableVar}.slug, slug)).limit(1)
3610
+ if (result.length === 0) return null
3611
+ ${ctx.displaySingleRowReturn}
3612
+ } catch (error) {
3613
+ console.error('Error fetching ${ctx.singular} by slug:', error)
3614
+ return null
3615
+ }
3616
+ }
3617
+ `;
3618
+ }
3619
+ function genCreateContent(ctx) {
3620
+ const slugImport = ctx.hasAutoSlug ? `
3621
+ import { slugify } from './helpers'` : "";
3622
+ return `'use server'
3623
+
3624
+ import db from '@cms/db'
3625
+ import { ${ctx.tableVar} } from '@cms/db/schema'
3626
+ import { desc } from 'drizzle-orm'
3627
+ import { updateTag } from 'next/cache'
3628
+ import { CACHE_TAG } from './types'
3629
+ import type { Create${ctx.Singular}Input, Create${ctx.Singular}Result, ${ctx.Singular}Data } from './types'${slugImport}
3630
+
3631
+ export async function create${ctx.Singular}(input: Create${ctx.Singular}Input): Promise<Create${ctx.Singular}Result> {
3632
+ try {${ctx.autoSlugCreate}
3633
+ const maxSortOrderResult = await db
3634
+ .select({ maxOrder: ${ctx.tableVar}.sortOrder })
3635
+ .from(${ctx.tableVar})
3636
+ .orderBy(desc(${ctx.tableVar}.sortOrder))
3637
+ .limit(1)
3638
+ const nextSortOrder = (maxSortOrderResult[0]?.maxOrder ?? 0) + 1
3639
+ ${ctx.htmlCreateBlock}
3640
+ const result = await db.insert(${ctx.tableVar}).values({
3641
+ ${ctx.createMappings},${ctx.hasHtmlOutput ? `
3642
+ ${ctx.htmlCreateMappings},` : ""}${ctx.hasDraft ? `
3643
+ published: input.published ?? false,` : ""}
3644
+ sortOrder: nextSortOrder,
3645
+ createdAt: new Date().toISOString(),
3646
+ updatedAt: new Date().toISOString()
3647
+ }).returning()
3648
+
3649
+ updateTag(CACHE_TAG)
3650
+
3651
+ return {
3652
+ success: true,
3653
+ ${ctx.camelSingular}: result[0] as ${ctx.Singular}Data
3654
+ }
3655
+ } catch (error) {
3656
+ console.error('Error creating ${ctx.singular}:', error)
3657
+ throw new Error(error instanceof Error ? error.message : 'Failed to create ${ctx.singular}')
3658
+ }
3659
+ }
3660
+ `;
3661
+ }
3662
+ function genUpdateContent(ctx) {
3663
+ const slugImport = ctx.hasAutoSlug ? `
3664
+ import { slugify } from './helpers'` : "";
3665
+ const htmlUpdateBlock = ctx.hasHtmlOutput ? `
3666
+ const { renderMarkdownSync } = await import('@cms/lib/markdown/render')
3667
+ ` + ctx.htmlOutputFields.map((f) => ` if (processedData.${f.name} !== undefined) {
3668
+ processedData.${f.name}Html = renderMarkdownSync(String(processedData.${f.name} || ''))
3669
+ }`).join("\n") + "\n" : "";
3670
+ return `'use server'
3671
+
3672
+ import db from '@cms/db'
3673
+ import { ${ctx.tableVar} } from '@cms/db/schema'
3674
+ import { eq } from 'drizzle-orm'
3675
+ import { updateTag } from 'next/cache'
3676
+ import { CACHE_TAG } from './types'
3677
+ import type { Update${ctx.Singular}Input, Update${ctx.Singular}Result, ${ctx.Singular}Data } from './types'${slugImport}
3678
+
3679
+ export async function update${ctx.Singular}(input: Update${ctx.Singular}Input): Promise<Update${ctx.Singular}Result> {
3680
+ try {
3681
+ const { id, ...updateData } = input
3682
+ ${ctx.autoSlugUpdate}
3683
+ const fieldMeta = [
3684
+ ${ctx.fieldMeta}
3685
+ ]
3686
+
3687
+ const processedData: Record<string, unknown> = {}
3688
+ for (const [key, value] of Object.entries(updateData)) {
3689
+ if (key === 'published') { processedData[key] = value; continue }
3690
+ const field = fieldMeta.find(f => f.name === key)
3691
+ if (!field) continue
3692
+ if (field.type === 'list') {
3693
+ processedData[key] = value || []
3694
+ } else if (!field.required && ['date', 'timestamp', 'time', 'string', 'varchar', 'text', 'select'].includes(field.type)) {
3695
+ processedData[key] = value && value !== '' ? value : null
3696
+ } else {
3697
+ processedData[key] = value
3698
+ }
3699
+ }
3700
+ ${htmlUpdateBlock}
3701
+ const result = await db.update(${ctx.tableVar})
3702
+ .set({ ...processedData, updatedAt: new Date().toISOString() })
3703
+ .where(eq(${ctx.tableVar}.id, id))
3704
+ .returning()
3705
+
3706
+ if (result.length === 0) throw new Error('${ctx.Singular} not found')
3707
+
3708
+ updateTag(CACHE_TAG)
3709
+
3710
+ return {
3711
+ success: true,
3712
+ ${ctx.camelSingular}: result[0] as ${ctx.Singular}Data
3713
+ }
3714
+ } catch (error) {
3715
+ console.error('Error updating ${ctx.singular}:', error)
3716
+ throw new Error(error instanceof Error ? error.message : 'Failed to update ${ctx.singular}')
3717
+ }
3718
+ }
3719
+ `;
3720
+ }
3721
+ function genDeleteContent(ctx) {
3722
+ return `'use server'
3723
+
3724
+ import db from '@cms/db'
3725
+ import { ${ctx.tableVar} } from '@cms/db/schema'
3726
+ import { eq } from 'drizzle-orm'
3727
+ import { updateTag } from 'next/cache'
3728
+ import { CACHE_TAG } from './types'
3729
+ import type { Delete${ctx.Singular}Result } from './types'
3730
+
3731
+ export async function delete${ctx.Singular}(id: number): Promise<Delete${ctx.Singular}Result> {
3732
+ try {
3733
+ await db.delete(${ctx.tableVar}).where(eq(${ctx.tableVar}.id, id))
3734
+ updateTag(CACHE_TAG)
3735
+ return { success: true }
3736
+ } catch (error) {
3737
+ console.error('Error deleting ${ctx.singular}:', error)
3738
+ return { success: false, error: error instanceof Error ? error.message : 'Failed to delete ${ctx.singular}' }
3739
+ }
3740
+ }
3741
+ `;
3742
+ }
3743
+ function genDeleteBulkContent(ctx) {
3744
+ return `'use server'
3745
+
3746
+ import db from '@cms/db'
3747
+ import { ${ctx.tableVar} } from '@cms/db/schema'
3748
+ import { inArray } from 'drizzle-orm'
3749
+ import { updateTag } from 'next/cache'
3750
+ import { CACHE_TAG } from './types'
3751
+ import type { Delete${ctx.Singular}Result } from './types'
3752
+
3753
+ export async function deleteBulk${ctx.Plural}(ids: number[]): Promise<Delete${ctx.Singular}Result> {
3754
+ try {
3755
+ if (ids.length === 0) return { success: false, error: 'No items selected for deletion' }
3756
+ await db.delete(${ctx.tableVar}).where(inArray(${ctx.tableVar}.id, ids))
3757
+ updateTag(CACHE_TAG)
3758
+ return { success: true }
3759
+ } catch (error) {
3760
+ console.error('Error deleting ${ctx.plural}:', error)
3761
+ return { success: false, error: error instanceof Error ? error.message : 'Failed to delete ${ctx.plural}' }
3762
+ }
3763
+ }
3764
+ `;
3765
+ }
3766
+ function genSortOrderContent(ctx) {
3767
+ return `'use server'
3768
+
3769
+ import db from '@cms/db'
3770
+ import { ${ctx.tableVar} } from '@cms/db/schema'
3771
+ import { asc, desc, eq, gt, lt } from 'drizzle-orm'
3772
+ import { updateTag } from 'next/cache'
3773
+ import { CACHE_TAG } from './types'
3774
+
3775
+ export async function update${ctx.Singular}SortOrder(
3776
+ id: number,
3777
+ direction: 'up' | 'down'
3778
+ ): Promise<{ success: boolean; error?: string }> {
3779
+ try {
3780
+ const current = await db.select({ id: ${ctx.tableVar}.id, sortOrder: ${ctx.tableVar}.sortOrder }).from(${ctx.tableVar}).where(eq(${ctx.tableVar}.id, id)).limit(1)
3781
+ if (current.length === 0) return { success: false, error: '${ctx.Singular} not found' }
3782
+
3783
+ const currentSortOrder = current[0].sortOrder
3784
+ const adjacent = await db
3785
+ .select({ id: ${ctx.tableVar}.id, sortOrder: ${ctx.tableVar}.sortOrder })
3786
+ .from(${ctx.tableVar})
3787
+ .where(direction === 'up' ? lt(${ctx.tableVar}.sortOrder, currentSortOrder) : gt(${ctx.tableVar}.sortOrder, currentSortOrder))
3788
+ .orderBy(direction === 'up' ? desc(${ctx.tableVar}.sortOrder) : asc(${ctx.tableVar}.sortOrder))
3789
+ .limit(1)
3790
+
3791
+ if (adjacent.length === 0) return { success: true }
3792
+
3793
+ await db.update(${ctx.tableVar}).set({ sortOrder: adjacent[0].sortOrder }).where(eq(${ctx.tableVar}.id, id))
3794
+ await db.update(${ctx.tableVar}).set({ sortOrder: currentSortOrder }).where(eq(${ctx.tableVar}.id, adjacent[0].id))
3795
+ updateTag(CACHE_TAG)
3796
+ return { success: true }
3797
+ } catch (error) {
3798
+ console.error('Error updating sort order:', error)
3799
+ return { success: false, error: error instanceof Error ? error.message : 'Failed to update sort order' }
3800
+ }
3801
+ }
3802
+ `;
3803
+ }
3804
+ function genBulkSortOrderContent(ctx) {
3805
+ return `'use server'
3806
+
3807
+ import db from '@cms/db'
3808
+ import { ${ctx.tableVar} } from '@cms/db/schema'
3809
+ import { eq } from 'drizzle-orm'
3810
+ import { updateTag } from 'next/cache'
3811
+ import { CACHE_TAG } from './types'
3812
+
3813
+ export async function bulkUpdate${ctx.Plural}SortOrder(
3814
+ updates: Array<{ id: number; sortOrder: number }>
3815
+ ): Promise<{ success: boolean; error?: string }> {
3816
+ try {
3817
+ if (updates.length === 0) return { success: true }
3818
+ await Promise.all(updates.map((u) => db.update(${ctx.tableVar}).set({ sortOrder: u.sortOrder }).where(eq(${ctx.tableVar}.id, u.id))))
3819
+ updateTag(CACHE_TAG)
3820
+ return { success: true }
3821
+ } catch (error) {
3822
+ console.error('Error bulk updating sort order:', error)
3823
+ return { success: false, error: error instanceof Error ? error.message : 'Failed to bulk update sort order' }
3824
+ }
3825
+ }
3826
+ `;
3827
+ }
3828
+ function genM2MFiles(ctx) {
3829
+ const files = [];
3830
+ for (const f of ctx.m2mFields) {
3831
+ const rel = f.relationship || "";
3832
+ const RelPascal = toPascalCase(rel);
3833
+ const relSingular = singularize(rel);
3834
+ const junctionVar = toCamelCase(`${ctx.singular}${RelPascal}`);
3835
+ const entityIdCol = `${ctx.singular}Id`;
3836
+ const relIdCol = `${relSingular}Id`;
3837
+ const kebabRel = rel.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
3838
+ files.push({
3839
+ name: `get-${kebabRel}-for-${ctx.singular}.ts`,
3840
+ content: `'use server'
3841
+
3842
+ import db from '@cms/db'
3843
+ import { ${junctionVar} } from '@cms/db/schema'
3844
+ import { eq } from 'drizzle-orm'
3845
+
3846
+ export async function get${RelPascal}For${ctx.Singular}(${ctx.singular}Id: number): Promise<number[]> {
3847
+ try {
3848
+ const results = await db.select({ ${relIdCol}: ${junctionVar}.${relIdCol} }).from(${junctionVar}).where(eq(${junctionVar}.${entityIdCol}, ${ctx.singular}Id))
3849
+ return results.map(r => r.${relIdCol})
3850
+ } catch (error) {
3851
+ console.error('Error fetching ${rel} for ${ctx.singular}:', error)
3852
+ return []
3853
+ }
3854
+ }
3855
+ `
3856
+ });
3857
+ files.push({
3858
+ name: `set-${kebabRel}-for-${ctx.singular}.ts`,
3859
+ content: `'use server'
3860
+
3861
+ import db from '@cms/db'
3862
+ import { ${junctionVar} } from '@cms/db/schema'
3863
+ import { eq } from 'drizzle-orm'
3864
+ import { updateTag } from 'next/cache'
3865
+ import { CACHE_TAG } from './types'
3866
+
3867
+ export async function set${RelPascal}For${ctx.Singular}(${ctx.singular}Id: number, ${rel}Ids: number[]): Promise<{ success: boolean; error?: string }> {
3868
+ try {
3869
+ await db.delete(${junctionVar}).where(eq(${junctionVar}.${entityIdCol}, ${ctx.singular}Id))
3870
+ if (${rel}Ids.length > 0) {
3871
+ await db.insert(${junctionVar}).values(${rel}Ids.map(${relSingular}Id => ({ ${entityIdCol}: ${ctx.singular}Id, ${relIdCol}: ${relSingular}Id })))
3872
+ }
3873
+ updateTag(CACHE_TAG)
3874
+ return { success: true }
3875
+ } catch (error) {
3876
+ console.error('Error setting ${rel} for ${ctx.singular}:', error)
3877
+ return { success: false, error: error instanceof Error ? error.message : 'Failed to set ${rel}' }
3878
+ }
3879
+ }
3880
+ `
3881
+ });
3882
+ }
3883
+ return files;
3884
+ }
3885
+ function genDistinctFiles(ctx) {
3886
+ if (!ctx.hasFilters) return [];
3887
+ return ctx.filters.map((filter) => {
3888
+ const fieldPascal = toPascalCase(filter.field);
3889
+ const kebabField = filter.field.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
3890
+ const kebabPlural = ctx.plural.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
3891
+ return {
3892
+ name: `get-distinct-${kebabPlural}-${kebabField}.ts`,
3893
+ content: `'use server'
3894
+
3895
+ import db from '@cms/db'
3896
+ import { ${ctx.tableVar} } from '@cms/db/schema'
3897
+ import { asc, isNotNull } from 'drizzle-orm'
3898
+
3899
+ export async function getDistinct${ctx.Plural}${fieldPascal}(): Promise<string[]> {
3900
+ try {
3901
+ const results = await db
3902
+ .selectDistinct({ value: ${ctx.tableVar}.${filter.field} })
3903
+ .from(${ctx.tableVar})
3904
+ .where(isNotNull(${ctx.tableVar}.${filter.field}))
3905
+ .orderBy(asc(${ctx.tableVar}.${filter.field}))
3906
+ return results.map((r) => String(r.value)).filter(Boolean)
3907
+ } catch (error) {
3908
+ console.error('Error fetching distinct ${ctx.plural} ${filter.field}:', error)
3909
+ return []
3910
+ }
3911
+ }
3912
+ `
3913
+ };
3914
+ });
3915
+ }
3916
+ function genBarrelContent(files, ctx) {
3917
+ const lines = [];
3918
+ const typeNames = [
3919
+ `${ctx.Singular}Data`,
3920
+ ctx.hasHtmlOutput ? `${ctx.Singular}DisplayData` : null,
3921
+ `${ctx.Plural}Response`,
3922
+ `Get${ctx.Plural}Filters`,
3923
+ `Create${ctx.Singular}Input`,
3924
+ `Create${ctx.Singular}Result`,
3925
+ `Update${ctx.Singular}Input`,
3926
+ `Update${ctx.Singular}Result`,
3927
+ `Delete${ctx.Singular}Result`
3928
+ ].filter(Boolean);
3929
+ lines.push(`export type { ${typeNames.join(", ")} } from './types'`);
3930
+ lines.push("");
3931
+ for (const file of files) {
3932
+ if (file.name === "types.ts" || file.name === "helpers.ts") continue;
3933
+ const exportMatches = file.content.matchAll(/export async function (\w+)/g);
3934
+ const fnNames = [...exportMatches].map((m) => m[1]);
3935
+ if (fnNames.length > 0) {
3936
+ const modulePath = "./" + file.name.replace(/\.ts$/, "");
3937
+ lines.push(`export { ${fnNames.join(", ")} } from '${modulePath}'`);
3938
+ }
3939
+ }
3940
+ lines.push("");
3941
+ return lines.join("\n");
3404
3942
  }
3405
3943
 
3406
3944
  // src/generators/actions/entity-actions.ts
3407
3945
  function generateActions(schema, cwd, actionsDir, options = {}) {
3408
3946
  const absActionsDir = path12.join(cwd, actionsDir);
3409
- const filePath = path12.join(absActionsDir, `${schema.name}.ts`);
3410
- if (fs12.existsSync(filePath) && !options.force) {
3947
+ const dirPath = path12.join(absActionsDir, schema.name);
3948
+ const oldFilePath = path12.join(absActionsDir, `${schema.name}.ts`);
3949
+ if (!options.force && (fs12.existsSync(dirPath) || fs12.existsSync(oldFilePath))) {
3411
3950
  return { files: [] };
3412
3951
  }
3952
+ if (options.force) {
3953
+ if (fs12.existsSync(oldFilePath)) fs12.unlinkSync(oldFilePath);
3954
+ if (fs12.existsSync(dirPath)) fs12.rmSync(dirPath, { recursive: true });
3955
+ }
3956
+ const ctx = buildEntityContext(schema);
3957
+ const kebabSingular = toKebabCase(ctx.singular);
3958
+ const kebabPlural = toKebabCase(ctx.plural);
3959
+ const files = [];
3960
+ files.push({ name: "types.ts", content: genTypesContent(ctx) });
3961
+ const helpersContent = genHelpersContent(ctx);
3962
+ if (helpersContent) {
3963
+ files.push({ name: "helpers.ts", content: helpersContent });
3964
+ }
3965
+ files.push({ name: `get-${kebabPlural}.ts`, content: genGetPluralContent(ctx) });
3966
+ files.push({ name: `get-${kebabSingular}-by-id.ts`, content: genGetByIdContent(ctx) });
3967
+ const bySlugContent = genGetBySlugContent(ctx);
3968
+ if (bySlugContent) {
3969
+ files.push({ name: `get-${kebabSingular}-by-slug.ts`, content: bySlugContent });
3970
+ }
3971
+ files.push({ name: `create-${kebabSingular}.ts`, content: genCreateContent(ctx) });
3972
+ files.push({ name: `update-${kebabSingular}.ts`, content: genUpdateContent(ctx) });
3973
+ files.push({ name: `delete-${kebabSingular}.ts`, content: genDeleteContent(ctx) });
3974
+ files.push({ name: `delete-bulk-${kebabPlural}.ts`, content: genDeleteBulkContent(ctx) });
3975
+ files.push({ name: `update-${kebabSingular}-sort-order.ts`, content: genSortOrderContent(ctx) });
3976
+ files.push({ name: `bulk-update-${kebabPlural}-sort-order.ts`, content: genBulkSortOrderContent(ctx) });
3977
+ files.push(...genM2MFiles(ctx));
3978
+ files.push(...genDistinctFiles(ctx));
3979
+ files.push({ name: "index.ts", content: genBarrelContent(files, ctx) });
3980
+ fs12.mkdirSync(dirPath, { recursive: true });
3981
+ for (const file of files) {
3982
+ fs12.writeFileSync(path12.join(dirPath, file.name), file.content, "utf-8");
3983
+ }
3984
+ return { files: files.map((f) => path12.join(actionsDir, schema.name, f.name)) };
3985
+ }
3986
+ function buildEntityContext(schema) {
3413
3987
  const singular = singularize(schema.name);
3414
3988
  const plural = pluralize(schema.name);
3415
3989
  const Singular = toPascalCase(singular);
@@ -3452,6 +4026,7 @@ function generateActions(schema, cwd, actionsDir, options = {}) {
3452
4026
  const hasSearch = (schema.search?.fields || []).length > 0;
3453
4027
  const searchFields = schema.search?.fields || [];
3454
4028
  const hasFilters = (schema.filters || []).length > 0;
4029
+ const hasAutoSlug = schema.autoSlugify?.enabled === true;
3455
4030
  const allDbFields = [...regularDbFields];
3456
4031
  if (hasDraft && !hasPublished) {
3457
4032
  allDbFields.push({ name: "published", type: "boolean", required: true });
@@ -3473,23 +4048,8 @@ function generateActions(schema, cwd, actionsDir, options = {}) {
3473
4048
  const filterableFields = allDbFields.filter(
3474
4049
  (f) => !f.primaryKey && f.name !== "createdAt" && f.name !== "updatedAt" && f.name !== "sortOrder" && ["string", "varchar", "text", "boolean", "number", "decimal"].includes(f.type)
3475
4050
  );
3476
- const drizzleImports = ["asc", "desc", "eq", "gt", "inArray", "lt"];
3477
- if (hasSearch || filterableFields.length > 1) drizzleImports.push("and");
3478
- if (hasSearch) drizzleImports.push("ilike", "or");
3479
- if (hasRelationships) drizzleImports.push("sql");
3480
- if (hasFilters) drizzleImports.push("isNotNull");
3481
- const sortedDrizzleImports = [...new Set(drizzleImports)].sort();
3482
- const dbImports = /* @__PURE__ */ new Set([tableVar]);
3483
- for (const f of relationshipFields) dbImports.add(toCamelCase(f.relationship));
3484
- for (const f of m2mFields) {
3485
- const junctionVar = toCamelCase(`${singular}${toPascalCase(f.relationship || "")}`);
3486
- dbImports.add(junctionVar);
3487
- dbImports.add(toCamelCase(f.relationship || ""));
3488
- }
3489
- for (const q of allListRelQueries) {
3490
- dbImports.add(q.relTable);
3491
- }
3492
- const sortedDbImports = [...dbImports].sort();
4051
+ const relTableImports = relationshipFields.map((f) => toCamelCase(f.relationship));
4052
+ const listRelTableImports = allListRelQueries.map((q) => q.relTable);
3493
4053
  const dataFields = allDbFields.map(
3494
4054
  (f) => ` ${quotePropertyName(f.name)}: ${getFieldType(f, "output")}${f.required ? "" : " | null"}`
3495
4055
  ).join("\n");
@@ -3501,9 +4061,10 @@ ${htmlFieldTypes}` : ""}${m2mFieldTypes ? `
3501
4061
  ${m2mFieldTypes}` : ""}
3502
4062
  }`;
3503
4063
  const displayDbFields = allDbFields.filter((f) => !htmlOutputFields.some((h) => h.name === f.name));
3504
- const displayDataFields = displayDbFields.map((f) => ` ${quotePropertyName(f.name)}: ${getFieldType(f, "output")}${f.required ? "" : " | null"}`).join("\n");
3505
- const displayDataInterface = hasHtmlOutput ? `
3506
- export interface ${Singular}DisplayData {
4064
+ const displayDataFields = displayDbFields.map(
4065
+ (f) => ` ${quotePropertyName(f.name)}: ${getFieldType(f, "output")}${f.required ? "" : " | null"}`
4066
+ ).join("\n");
4067
+ const displayDataInterface = hasHtmlOutput ? `export interface ${Singular}DisplayData {
3507
4068
  ${displayDataFields}${htmlFieldTypes ? `
3508
4069
  ${htmlFieldTypes}` : ""}${m2mFieldTypes ? `
3509
4070
  ${m2mFieldTypes}` : ""}
@@ -3551,14 +4112,7 @@ ${searchFields.map((f) => ` ilike(${tableVar}.${f}, searchTerm)`).join(
3551
4112
  )
3552
4113
  }
3553
4114
  ` : "";
3554
- const resultMapping = hasRelationships ? buildResultMapping(
3555
- allDbFields,
3556
- relationshipFields,
3557
- Plural,
3558
- camelPlural,
3559
- Singular,
3560
- hasListRels
3561
- ) : hasListRels ? `
4115
+ const resultMapping = hasRelationships ? buildResultMapping(allDbFields, relationshipFields, Plural, camelPlural, Singular, hasListRels) : hasListRels ? `
3562
4116
 
3563
4117
  const populated${Plural} = await Promise.all(
3564
4118
  results.map(record => populate${Singular}ListRelationships(record as ${Singular}Data))
@@ -3572,72 +4126,26 @@ ${searchFields.map((f) => ` ilike(${tableVar}.${f}, searchTerm)`).join(
3572
4126
  ${camelPlural}: results as ${Singular}Data[],
3573
4127
  total: results.length
3574
4128
  }`;
3575
- const listResultMapping = hasHtmlOutput && hasRelationships ? buildResultMapping(listDbFields, relationshipFields, Plural, camelPlural, Singular, hasListRels) : resultMapping;
4129
+ const listResultMapping = hasHtmlOutput && hasRelationships ? buildResultMapping(
4130
+ listDbFields,
4131
+ relationshipFields,
4132
+ Plural,
4133
+ camelPlural,
4134
+ Singular,
4135
+ hasListRels
4136
+ ) : resultMapping;
3576
4137
  const fieldMeta = createFields.map((f) => `{ name: '${f.name}', type: '${f.type}', required: ${f.required ?? false} }`).join(",\n ");
3577
4138
  const createMappings = createFields.map((f) => ` ${f.name}: ${generateFieldMapping(f)}`).join(",\n");
3578
4139
  const htmlCreateBlock = hasHtmlOutput ? `
3579
4140
  const { renderMarkdownSync } = await import('@cms/lib/markdown/render')
3580
4141
  ` + htmlOutputFields.map((f) => ` const ${f.name}Html = renderMarkdownSync(input.${f.name} || '')`).join("\n") + "\n" : "";
3581
4142
  const htmlCreateMappings = htmlOutputFields.map((f) => ` ${f.name}Html`).join(",\n");
3582
- const distinctFns = hasFilters ? (schema.filters || []).map(
3583
- (filter) => `
3584
- export async function getDistinct${Plural}${toPascalCase(filter.field)}(): Promise<string[]> {
3585
- try {
3586
- const results = await db
3587
- .selectDistinct({ value: ${tableVar}.${filter.field} })
3588
- .from(${tableVar})
3589
- .where(isNotNull(${tableVar}.${filter.field}))
3590
- .orderBy(asc(${tableVar}.${filter.field}))
3591
- return results.map((r) => String(r.value)).filter(Boolean)
3592
- } catch (error) {
3593
- console.error('Error fetching distinct ${plural} ${filter.field}:', error)
3594
- return []
3595
- }
3596
- }`
3597
- ).join("\n") : "";
3598
- const m2mHelpers = hasM2M ? m2mFields.map((f) => {
3599
- const rel = f.relationship || "";
3600
- const RelPascal = toPascalCase(rel);
3601
- const relSingular = singularize(rel);
3602
- const junctionVar = toCamelCase(`${singular}${RelPascal}`);
3603
- const entityIdCol = `${singular}Id`;
3604
- const relIdCol = `${relSingular}Id`;
3605
- return `
3606
- export async function get${RelPascal}For${Singular}(${singular}Id: number): Promise<number[]> {
3607
- try {
3608
- const results = await db.select({ ${relIdCol}: ${junctionVar}.${relIdCol} }).from(${junctionVar}).where(eq(${junctionVar}.${entityIdCol}, ${singular}Id))
3609
- return results.map(r => r.${relIdCol})
3610
- } catch (error) {
3611
- console.error('Error fetching ${rel} for ${singular}:', error)
3612
- return []
3613
- }
3614
- }
3615
-
3616
- export async function set${RelPascal}For${Singular}(${singular}Id: number, ${rel}Ids: number[]): Promise<{ success: boolean; error?: string }> {
3617
- try {
3618
- await db.delete(${junctionVar}).where(eq(${junctionVar}.${entityIdCol}, ${singular}Id))
3619
- if (${rel}Ids.length > 0) {
3620
- await db.insert(${junctionVar}).values(${rel}Ids.map(${relSingular}Id => ({ ${entityIdCol}: ${singular}Id, ${relIdCol}: ${relSingular}Id })))
3621
- }
3622
- updateTag(CACHE_TAG)
3623
- return { success: true }
3624
- } catch (error) {
3625
- console.error('Error setting ${rel} for ${singular}:', error)
3626
- return { success: false, error: error instanceof Error ? error.message : 'Failed to set ${rel}' }
3627
- }
3628
- }`;
3629
- }).join("\n") : "";
3630
- const slugifyHelper = schema.autoSlugify?.enabled ? `
3631
- function slugify(text: string): string {
3632
- return text.toLowerCase().trim().replace(/[^\\w\\s-]/g, '').replace(/\\s+/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, '')
3633
- }
3634
- ` : "";
3635
- const autoSlugCreate = schema.autoSlugify?.enabled ? `
4143
+ const autoSlugCreate = hasAutoSlug ? `
3636
4144
  if ((!input.${schema.autoSlugify.targetField} || input.${schema.autoSlugify.targetField} === '') && input.${schema.autoSlugify.sourceField}) {
3637
4145
  input.${schema.autoSlugify.targetField} = slugify(input.${schema.autoSlugify.sourceField})
3638
4146
  }
3639
4147
  ` : "";
3640
- const autoSlugUpdate = schema.autoSlugify?.enabled ? `
4148
+ const autoSlugUpdate = hasAutoSlug ? `
3641
4149
  if (updateData.${schema.autoSlugify.sourceField}) {
3642
4150
  if (updateData.${schema.autoSlugify.targetField} === '' || updateData.${schema.autoSlugify.targetField} === undefined) {
3643
4151
  updateData.${schema.autoSlugify.targetField} = slugify(updateData.${schema.autoSlugify.sourceField})
@@ -3674,230 +4182,54 @@ function slugify(text: string): string {
3674
4182
  }
3675
4183
  return `return result[0] as ${displayType}`;
3676
4184
  })() : singleRowReturn;
3677
- const content = `'use server'
3678
-
3679
- import db from '@cms/db'
3680
- import { ${sortedDbImports.join(", ")} } from '@cms/db/schema'
3681
- import { ${sortedDrizzleImports.join(", ")} } from 'drizzle-orm'
3682
- import { updateTag } from 'next/cache'
3683
-
3684
- const CACHE_TAG = '${cacheTag}'
3685
-
3686
- ${dataInterface}${displayDataInterface}
3687
-
3688
- ${responseInterface}
3689
-
3690
- ${filtersInterface}
3691
-
3692
- ${createInterface}
3693
-
3694
- export interface Create${Singular}Result {
3695
- success: boolean
3696
- error?: string
3697
- ${camelSingular}?: ${Singular}Data
3698
- }
3699
-
3700
- ${updateInterface}
3701
-
3702
- export interface Update${Singular}Result {
3703
- success: boolean
3704
- error?: string
3705
- ${camelSingular}?: ${Singular}Data
3706
- }
3707
-
3708
- export interface Delete${Singular}Result {
3709
- success: boolean
3710
- error?: string
3711
- }
3712
- ${slugifyHelper}${populateListRelsFn}
3713
- export async function get${Plural}(filters?: Get${Plural}Filters): Promise<${Plural}Response> {
3714
- try {
3715
- const conditions = []
3716
- ${searchBlock}${filterConditions}
3717
-
3718
- const query = ${listSelectClause}
3719
-
3720
- const orderedQuery = conditions.length > 0
3721
- ? query.where(and(...conditions)).orderBy(asc(${tableVar}.sortOrder))
3722
- : query.orderBy(asc(${tableVar}.sortOrder))
3723
-
3724
- const results = filters?.limit && filters.limit > 0
3725
- ? await orderedQuery.limit(filters.limit)
3726
- : await orderedQuery${listResultMapping}
3727
- } catch (error) {
3728
- console.error('Error fetching ${plural}:', error)
3729
- return { ${camelPlural}: [], total: 0 }
3730
- }
3731
- }
3732
- ${distinctFns}
3733
-
3734
- export async function get${Singular}ById(id: number): Promise<${Singular}Data | null> {
3735
- try {
3736
- const result = await ${selectClause}.where(eq(${tableVar}.id, id)).limit(1)
3737
- if (result.length === 0) return null
3738
- ${singleRowReturn}
3739
- } catch (error) {
3740
- console.error('Error fetching ${singular}:', error)
3741
- return null
3742
- }
3743
- }${hasSlug ? `
3744
-
3745
- export async function get${Singular}BySlug(slug: string): Promise<${hasHtmlOutput ? `${Singular}DisplayData` : `${Singular}Data`} | null> {
3746
- try {
3747
- const result = await ${displaySelectClause}.where(eq(${tableVar}.slug, slug)).limit(1)
3748
- if (result.length === 0) return null
3749
- ${displaySingleRowReturn}
3750
- } catch (error) {
3751
- console.error('Error fetching ${singular} by slug:', error)
3752
- return null
3753
- }
3754
- }` : ""}
3755
-
3756
- export async function create${Singular}(input: Create${Singular}Input): Promise<Create${Singular}Result> {
3757
- try {${autoSlugCreate}
3758
- const maxSortOrderResult = await db
3759
- .select({ maxOrder: ${tableVar}.sortOrder })
3760
- .from(${tableVar})
3761
- .orderBy(desc(${tableVar}.sortOrder))
3762
- .limit(1)
3763
- const nextSortOrder = (maxSortOrderResult[0]?.maxOrder ?? 0) + 1
3764
- ${htmlCreateBlock}
3765
- const result = await db.insert(${tableVar}).values({
3766
- ${createMappings},${hasHtmlOutput ? `
3767
- ${htmlCreateMappings},` : ""}${hasDraft ? `
3768
- published: input.published ?? false,` : ""}
3769
- sortOrder: nextSortOrder,
3770
- createdAt: new Date().toISOString(),
3771
- updatedAt: new Date().toISOString()
3772
- }).returning()
3773
-
3774
- updateTag(CACHE_TAG)
3775
-
3776
- return {
3777
- success: true,
3778
- ${camelSingular}: result[0] as ${Singular}Data
3779
- }
3780
- } catch (error) {
3781
- console.error('Error creating ${singular}:', error)
3782
- throw new Error(error instanceof Error ? error.message : 'Failed to create ${singular}')
3783
- }
3784
- }
3785
-
3786
- export async function update${Singular}(input: Update${Singular}Input): Promise<Update${Singular}Result> {
3787
- try {
3788
- const { id, ...updateData } = input
3789
- ${autoSlugUpdate}
3790
- const fieldMeta = [
3791
- ${fieldMeta}
3792
- ]
3793
-
3794
- const processedData: Record<string, unknown> = {}
3795
- for (const [key, value] of Object.entries(updateData)) {
3796
- if (key === 'published') { processedData[key] = value; continue }
3797
- const field = fieldMeta.find(f => f.name === key)
3798
- if (!field) continue
3799
- if (field.type === 'list') {
3800
- processedData[key] = value || []
3801
- } else if (!field.required && ['date', 'timestamp', 'time', 'string', 'varchar', 'text', 'select'].includes(field.type)) {
3802
- processedData[key] = value && value !== '' ? value : null
3803
- } else {
3804
- processedData[key] = value
3805
- }
3806
- }
3807
- ${hasHtmlOutput ? `
3808
- const { renderMarkdownSync } = await import('@cms/lib/markdown/render')
3809
- ` + htmlOutputFields.map((f) => ` if (processedData.${f.name} !== undefined) {
3810
- processedData.${f.name}Html = renderMarkdownSync(String(processedData.${f.name} || ''))
3811
- }`).join("\n") + "\n" : ""}
3812
- const result = await db.update(${tableVar})
3813
- .set({ ...processedData, updatedAt: new Date().toISOString() })
3814
- .where(eq(${tableVar}.id, id))
3815
- .returning()
3816
-
3817
- if (result.length === 0) throw new Error('${Singular} not found')
3818
-
3819
- updateTag(CACHE_TAG)
3820
-
3821
- return {
3822
- success: true,
3823
- ${camelSingular}: result[0] as ${Singular}Data
3824
- }
3825
- } catch (error) {
3826
- console.error('Error updating ${singular}:', error)
3827
- throw new Error(error instanceof Error ? error.message : 'Failed to update ${singular}')
3828
- }
3829
- }
3830
-
3831
- export async function delete${Singular}(id: number): Promise<Delete${Singular}Result> {
3832
- try {
3833
- await db.delete(${tableVar}).where(eq(${tableVar}.id, id))
3834
- updateTag(CACHE_TAG)
3835
- return { success: true }
3836
- } catch (error) {
3837
- console.error('Error deleting ${singular}:', error)
3838
- return { success: false, error: error instanceof Error ? error.message : 'Failed to delete ${singular}' }
3839
- }
3840
- }
3841
-
3842
- export async function deleteBulk${Plural}(ids: number[]): Promise<Delete${Singular}Result> {
3843
- try {
3844
- if (ids.length === 0) return { success: false, error: 'No items selected for deletion' }
3845
- await db.delete(${tableVar}).where(inArray(${tableVar}.id, ids))
3846
- updateTag(CACHE_TAG)
3847
- return { success: true }
3848
- } catch (error) {
3849
- console.error('Error deleting ${plural}:', error)
3850
- return { success: false, error: error instanceof Error ? error.message : 'Failed to delete ${plural}' }
3851
- }
3852
- }
3853
-
3854
- export async function update${Singular}SortOrder(
3855
- id: number,
3856
- direction: 'up' | 'down'
3857
- ): Promise<{ success: boolean; error?: string }> {
3858
- try {
3859
- const current = await db.select({ id: ${tableVar}.id, sortOrder: ${tableVar}.sortOrder }).from(${tableVar}).where(eq(${tableVar}.id, id)).limit(1)
3860
- if (current.length === 0) return { success: false, error: '${Singular} not found' }
3861
-
3862
- const currentSortOrder = current[0].sortOrder
3863
- const adjacent = await db
3864
- .select({ id: ${tableVar}.id, sortOrder: ${tableVar}.sortOrder })
3865
- .from(${tableVar})
3866
- .where(direction === 'up' ? lt(${tableVar}.sortOrder, currentSortOrder) : gt(${tableVar}.sortOrder, currentSortOrder))
3867
- .orderBy(direction === 'up' ? desc(${tableVar}.sortOrder) : asc(${tableVar}.sortOrder))
3868
- .limit(1)
3869
-
3870
- if (adjacent.length === 0) return { success: true }
3871
-
3872
- await db.update(${tableVar}).set({ sortOrder: adjacent[0].sortOrder }).where(eq(${tableVar}.id, id))
3873
- await db.update(${tableVar}).set({ sortOrder: currentSortOrder }).where(eq(${tableVar}.id, adjacent[0].id))
3874
- updateTag(CACHE_TAG)
3875
- return { success: true }
3876
- } catch (error) {
3877
- console.error('Error updating sort order:', error)
3878
- return { success: false, error: error instanceof Error ? error.message : 'Failed to update sort order' }
3879
- }
3880
- }
3881
-
3882
- export async function bulkUpdate${Plural}SortOrder(
3883
- updates: Array<{ id: number; sortOrder: number }>
3884
- ): Promise<{ success: boolean; error?: string }> {
3885
- try {
3886
- if (updates.length === 0) return { success: true }
3887
- await Promise.all(updates.map((u) => db.update(${tableVar}).set({ sortOrder: u.sortOrder }).where(eq(${tableVar}.id, u.id))))
3888
- updateTag(CACHE_TAG)
3889
- return { success: true }
3890
- } catch (error) {
3891
- console.error('Error bulk updating sort order:', error)
3892
- return { success: false, error: error instanceof Error ? error.message : 'Failed to bulk update sort order' }
3893
- }
3894
- }${m2mHelpers}
3895
- `;
3896
- if (!fs12.existsSync(absActionsDir)) {
3897
- fs12.mkdirSync(absActionsDir, { recursive: true });
3898
- }
3899
- fs12.writeFileSync(filePath, content, "utf-8");
3900
- return { files: [path12.join(actionsDir, `${schema.name}.ts`)] };
4185
+ return {
4186
+ singular,
4187
+ plural,
4188
+ Singular,
4189
+ Plural,
4190
+ tableVar,
4191
+ camelSingular,
4192
+ camelPlural,
4193
+ cacheTag,
4194
+ hasRelationships,
4195
+ hasM2M,
4196
+ hasListRels,
4197
+ hasHtmlOutput,
4198
+ hasSlug,
4199
+ hasDraft,
4200
+ hasSearch,
4201
+ hasFilters,
4202
+ hasAutoSlug,
4203
+ searchFields,
4204
+ htmlOutputFields,
4205
+ m2mFields,
4206
+ filterableFields,
4207
+ relTableImports,
4208
+ listRelTableImports,
4209
+ dataInterface,
4210
+ displayDataInterface,
4211
+ responseInterface,
4212
+ filtersInterface,
4213
+ createInterface,
4214
+ updateInterface,
4215
+ selectClause,
4216
+ listSelectClause,
4217
+ displaySelectClause,
4218
+ filterConditions,
4219
+ searchBlock,
4220
+ resultMapping,
4221
+ listResultMapping,
4222
+ singleRowReturn,
4223
+ displaySingleRowReturn,
4224
+ fieldMeta,
4225
+ createMappings,
4226
+ htmlCreateBlock,
4227
+ htmlCreateMappings,
4228
+ autoSlugCreate,
4229
+ autoSlugUpdate,
4230
+ populateListRelsFn,
4231
+ filters: schema.filters || []
4232
+ };
3901
4233
  }
3902
4234
 
3903
4235
  // src/generators/actions/single-actions.ts
@@ -3905,21 +4237,27 @@ import fs13 from "fs";
3905
4237
  import path13 from "path";
3906
4238
  function generateSingleActions(schema, cwd, actionsDir, options = {}) {
3907
4239
  const absActionsDir = path13.join(cwd, actionsDir);
3908
- const filePath = path13.join(absActionsDir, `${schema.name}.ts`);
3909
- if (fs13.existsSync(filePath) && !options.force) {
4240
+ const dirPath = path13.join(absActionsDir, schema.name);
4241
+ const oldFilePath = path13.join(absActionsDir, `${schema.name}.ts`);
4242
+ if (!options.force && (fs13.existsSync(dirPath) || fs13.existsSync(oldFilePath))) {
3910
4243
  return { files: [] };
3911
4244
  }
4245
+ if (options.force) {
4246
+ if (fs13.existsSync(oldFilePath)) fs13.unlinkSync(oldFilePath);
4247
+ if (fs13.existsSync(dirPath)) fs13.rmSync(dirPath, { recursive: true });
4248
+ }
3912
4249
  const singular = singularize(schema.name);
3913
4250
  const Singular = toPascalCase(singular);
3914
4251
  const tableVar = toCamelCase(schema.name);
3915
4252
  const camelSingular = toCamelCase(singular);
4253
+ const kebabSingular = toKebabCase(singular);
3916
4254
  const dbFields = flattenFields(schema.fields).filter(
3917
4255
  (f) => !(f.type === "relationship" && f.multiple === true)
3918
4256
  );
3919
- const singleHtmlOutputFields = dbFields.filter(
4257
+ const htmlOutputFields = dbFields.filter(
3920
4258
  (f) => (f.type === "richtext" || f.type === "markdown") && f.output === "html"
3921
4259
  );
3922
- const singleHasHtmlOutput = singleHtmlOutputFields.length > 0;
4260
+ const hasHtmlOutput = htmlOutputFields.length > 0;
3923
4261
  const allDbFields = [...dbFields];
3924
4262
  if (!dbFields.some((f) => f.name === "createdAt")) {
3925
4263
  allDbFields.push({ name: "createdAt", type: "timestamp", required: true });
@@ -3933,39 +4271,34 @@ function generateSingleActions(schema, cwd, actionsDir, options = {}) {
3933
4271
  const dataFields = allDbFields.map(
3934
4272
  (f) => ` ${quotePropertyName(f.name)}: ${getFieldType(f, "output")}${f.required ? "" : " | null"}`
3935
4273
  ).join("\n");
3936
- const singleHtmlFieldTypes = singleHtmlOutputFields.map((f) => ` ${f.name}Html: string`).join("\n");
3937
- const dataInterface = `export interface ${Singular}Data {
3938
- ${dataFields}${singleHtmlFieldTypes ? `
3939
- ${singleHtmlFieldTypes}` : ""}
3940
- }`;
4274
+ const htmlFieldTypes = htmlOutputFields.map((f) => ` ${f.name}Html: string`).join("\n");
3941
4275
  const upsertInterfaceFields = upsertFields.map(
3942
4276
  (f) => ` ${quotePropertyName(f.name)}${f.required ? "" : "?"}: ${getFieldType(f, "input")}`
3943
4277
  ).join("\n");
3944
- const upsertInterface = `export interface Upsert${Singular}Input {
3945
- ${upsertInterfaceFields}
3946
- }`;
3947
- const upsertMappings = upsertFields.map((f) => ` ${f.name}: ${generateFieldMapping(f)}`).join(",\n");
3948
4278
  const fieldMeta = upsertFields.map((f) => `{ name: '${f.name}', type: '${f.type}', required: ${f.required ?? false} }`).join(",\n ");
4279
+ const upsertMappings = upsertFields.map((f) => ` ${f.name}: ${generateFieldMapping(f)}`).join(",\n");
3949
4280
  const cacheTag = `${schema.name}:all`;
3950
- const singleHtmlUpsertMappings = singleHtmlOutputFields.map((f) => ` ${f.name}Html`).join(",\n");
3951
- const content = `'use server'
4281
+ const typesContent = [
4282
+ `export interface ${Singular}Data {
4283
+ ${dataFields}${htmlFieldTypes ? `
4284
+ ${htmlFieldTypes}` : ""}
4285
+ }`,
4286
+ `export interface Upsert${Singular}Input {
4287
+ ${upsertInterfaceFields}
4288
+ }`,
4289
+ `export interface Upsert${Singular}Result {
4290
+ success: boolean
4291
+ error?: string
4292
+ ${camelSingular}?: ${Singular}Data
4293
+ }`,
4294
+ `export const CACHE_TAG = '${cacheTag}'`
4295
+ ].join("\n\n") + "\n";
4296
+ const getContent = `'use server'
3952
4297
 
3953
4298
  import db from '@cms/db'
3954
4299
  import { ${tableVar} } from '@cms/db/schema'
3955
4300
  import { eq } from 'drizzle-orm'
3956
- import { updateTag } from 'next/cache'
3957
-
3958
- const CACHE_TAG = '${cacheTag}'
3959
-
3960
- ${dataInterface}
3961
-
3962
- ${upsertInterface}
3963
-
3964
- export interface Upsert${Singular}Result {
3965
- success: boolean
3966
- error?: string
3967
- ${camelSingular}?: ${Singular}Data
3968
- }
4301
+ import type { ${Singular}Data } from './types'
3969
4302
 
3970
4303
  export async function get${Singular}(): Promise<${Singular}Data | null> {
3971
4304
  try {
@@ -3977,6 +4310,20 @@ export async function get${Singular}(): Promise<${Singular}Data | null> {
3977
4310
  return null
3978
4311
  }
3979
4312
  }
4313
+ `;
4314
+ const htmlUpsertBlock = hasHtmlOutput ? `
4315
+ const { renderMarkdownSync } = await import('@cms/lib/markdown/render')
4316
+ ` + htmlOutputFields.map((f) => ` if (processedData.${f.name} !== undefined) {
4317
+ processedData.${f.name}Html = renderMarkdownSync(String(processedData.${f.name} || ''))
4318
+ }`).join("\n") + "\n" + htmlOutputFields.map((f) => ` const ${f.name}Html = renderMarkdownSync(input.${f.name} || '')`).join("\n") + "\n" : "";
4319
+ const htmlUpsertMappings = hasHtmlOutput ? "\n" + htmlOutputFields.map((f) => ` ${f.name}Html`).join(",\n") + "," : "";
4320
+ const upsertContent = `'use server'
4321
+
4322
+ import db from '@cms/db'
4323
+ import { ${tableVar} } from '@cms/db/schema'
4324
+ import { updateTag } from 'next/cache'
4325
+ import { CACHE_TAG } from './types'
4326
+ import type { Upsert${Singular}Input, Upsert${Singular}Result, ${Singular}Data } from './types'
3980
4327
 
3981
4328
  export async function upsert${Singular}(input: Upsert${Singular}Input): Promise<Upsert${Singular}Result> {
3982
4329
  try {
@@ -3996,18 +4343,14 @@ export async function upsert${Singular}(input: Upsert${Singular}Input): Promise<
3996
4343
  processedData[key] = value
3997
4344
  }
3998
4345
  }
3999
- ${singleHasHtmlOutput ? `
4000
- const { renderMarkdownSync } = await import('@cms/lib/markdown/render')
4001
- ` + singleHtmlOutputFields.map((f) => ` if (processedData.${f.name} !== undefined) {
4002
- processedData.${f.name}Html = renderMarkdownSync(String(processedData.${f.name} || ''))
4003
- }`).join("\n") + "\n" + singleHtmlOutputFields.map((f) => ` const ${f.name}Html = renderMarkdownSync(input.${f.name} || '')`).join("\n") + "\n" : ""}
4346
+ ${htmlUpsertBlock}
4004
4347
  const now = new Date().toISOString()
4005
4348
 
4006
4349
  const result = await db
4007
4350
  .insert(${tableVar})
4008
4351
  .values({
4009
4352
  id: 1,
4010
- ${upsertMappings},${singleHasHtmlOutput ? "\n" + singleHtmlOutputFields.map((f) => ` ${f.name}Html`).join(",\n") + "," : ""}
4353
+ ${upsertMappings},${htmlUpsertMappings}
4011
4354
  createdAt: now,
4012
4355
  updatedAt: now
4013
4356
  })
@@ -4029,11 +4372,21 @@ ${upsertMappings},${singleHasHtmlOutput ? "\n" + singleHtmlOutputFields.map((f)
4029
4372
  }
4030
4373
  }
4031
4374
  `;
4032
- if (!fs13.existsSync(absActionsDir)) {
4033
- fs13.mkdirSync(absActionsDir, { recursive: true });
4375
+ const barrelContent = `export type { ${Singular}Data, Upsert${Singular}Input, Upsert${Singular}Result } from './types'
4376
+ export { get${Singular} } from './get-${kebabSingular}'
4377
+ export { upsert${Singular} } from './upsert-${kebabSingular}'
4378
+ `;
4379
+ const files = [
4380
+ { name: "types.ts", content: typesContent },
4381
+ { name: `get-${kebabSingular}.ts`, content: getContent },
4382
+ { name: `upsert-${kebabSingular}.ts`, content: upsertContent },
4383
+ { name: "index.ts", content: barrelContent }
4384
+ ];
4385
+ fs13.mkdirSync(dirPath, { recursive: true });
4386
+ for (const file of files) {
4387
+ fs13.writeFileSync(path13.join(dirPath, file.name), file.content, "utf-8");
4034
4388
  }
4035
- fs13.writeFileSync(filePath, content, "utf-8");
4036
- return { files: [path13.join(actionsDir, `${schema.name}.ts`)] };
4389
+ return { files: files.map((f) => path13.join(actionsDir, schema.name, f.name)) };
4037
4390
  }
4038
4391
 
4039
4392
  // src/generators/cache.ts
@@ -4106,7 +4459,7 @@ ${entries.join(",\n")}
4106
4459
  }
4107
4460
  function generateCachedQueries(configs) {
4108
4461
  const actionImports = [];
4109
- const typeImports = [];
4462
+ const typeImportsBySchema = /* @__PURE__ */ new Map();
4110
4463
  const fns = [];
4111
4464
  for (const c of configs) {
4112
4465
  if (c.isSingle) {
@@ -4120,7 +4473,9 @@ export const getCached${c.pascalSingular} = async () => {
4120
4473
  continue;
4121
4474
  }
4122
4475
  actionImports.push(`get${c.pascalPlural}`);
4123
- typeImports.push(`Get${c.pascalPlural}Filters`);
4476
+ const types = typeImportsBySchema.get(c.name) || [];
4477
+ types.push(`Get${c.pascalPlural}Filters`);
4478
+ typeImportsBySchema.set(c.name, types);
4124
4479
  if (c.hasSlug) actionImports.push(`get${c.pascalSingular}BySlug`);
4125
4480
  for (const n of c.nestedLookups) actionImports.push(`get${n.pascalName}BySlug`);
4126
4481
  fns.push(`
@@ -4149,11 +4504,7 @@ export const getCached${n.pascalName}BySlug = async (${c.singularName}Slug: stri
4149
4504
  }
4150
4505
  }
4151
4506
  actionImports.sort();
4152
- typeImports.sort();
4153
- const typeImportStr = typeImports.length > 0 ? `${typeImports.map((t) => {
4154
- const schemaName = t.replace(/^Get/, "").replace(/Filters$/, "").toLowerCase();
4155
- return `import type { ${t} } from '@cms/actions/${schemaName}'`;
4156
- }).join("\n")}
4507
+ const typeImportStr = typeImportsBySchema.size > 0 ? `${Array.from(typeImportsBySchema.entries()).map(([schemaName, types]) => `import type { ${types.sort().join(", ")} } from '@cms/actions/${schemaName}'`).join("\n")}
4157
4508
  ` : "";
4158
4509
  const actionsBySchema = /* @__PURE__ */ new Map();
4159
4510
  for (const c of configs) {
@@ -12200,6 +12551,7 @@ var CORE_DEPS = [
12200
12551
  "input-otp",
12201
12552
  "react-resizable-panels",
12202
12553
  "recharts",
12554
+ "shadcn",
12203
12555
  "tw-animate-css",
12204
12556
  "usehooks-ts",
12205
12557
  "vaul"
@@ -14942,8 +15294,15 @@ var removeCommand = new Command4("remove").alias("rm").description("Remove all g
14942
15294
  isDir: true
14943
15295
  });
14944
15296
  }
15297
+ const actionsDir = path46.join(cwd, cmsDir, "lib", "actions", kebabName);
14945
15298
  const actionsFile = path46.join(cwd, cmsDir, "lib", "actions", `${kebabName}.ts`);
14946
- if (fs41.existsSync(actionsFile)) {
15299
+ if (fs41.existsSync(actionsDir)) {
15300
+ targets.push({
15301
+ path: actionsDir,
15302
+ label: `${path46.join(cmsDir, "lib", "actions", kebabName)}/`,
15303
+ isDir: true
15304
+ });
15305
+ } else if (fs41.existsSync(actionsFile)) {
14947
15306
  targets.push({
14948
15307
  path: actionsFile,
14949
15308
  label: path46.join(cmsDir, "lib", "actions", `${kebabName}.ts`),