swallowkit 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +0 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/provision.d.ts +1 -0
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +144 -54
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +6 -1
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/core/operations/migration-generator.d.ts +1 -1
- package/dist/core/operations/migration-generator.d.ts.map +1 -1
- package/dist/core/operations/migration-generator.js +22 -0
- package/dist/core/operations/migration-generator.js.map +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +13 -0
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +26 -2
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +26 -2
- package/src/__tests__/agent-instructions.test.ts +22 -1
- package/src/__tests__/init-agent-instructions.test.ts +99 -0
- package/src/__tests__/plan-apply.test.ts +101 -0
- package/src/__tests__/provision-output.test.ts +313 -0
- package/src/__tests__/ui-generator.test.ts +21 -0
- package/src/cli/commands/init.ts +0 -3
- package/src/cli/commands/provision.ts +89 -3
- package/src/cli/commands/scaffold.ts +5 -1
- package/src/core/operations/migration-generator.ts +35 -1
- package/src/core/project/agent-templates/AGENTS.md +26 -0
- package/src/core/project/agent-templates/shared-models.instructions.md +7 -0
- package/src/core/project/workflows.ts +13 -0
- package/src/core/scaffold/ui-generator.ts +26 -2
|
@@ -1,7 +1,83 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import prompts from "prompts";
|
|
3
|
+
import { spawnSync } from "child_process";
|
|
3
4
|
import { ensureSwallowKitProject } from "../../core/config";
|
|
4
|
-
import { applyProvisionOperation, planProvisionOperation } from "../../core/operations/provision-operations";
|
|
5
|
+
import { applyProvisionOperation, planProvisionOperation, ProvisionPlanData } from "../../core/operations/provision-operations";
|
|
6
|
+
|
|
7
|
+
function readAzOutput(args: string[]): string | undefined {
|
|
8
|
+
try {
|
|
9
|
+
const result = spawnSync("az", args, {
|
|
10
|
+
shell: false,
|
|
11
|
+
encoding: "utf-8",
|
|
12
|
+
timeout: 60_000,
|
|
13
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
14
|
+
});
|
|
15
|
+
if (result.status === 0 && !result.error && !result.signal) {
|
|
16
|
+
const value = result.stdout?.trim();
|
|
17
|
+
if (value && value !== "null") return value;
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
// Callers report a safe fallback without displaying credential-bearing output.
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function manualAzCommand(args: string[]): string {
|
|
26
|
+
return `az ${args.map((arg) => /^[A-Za-z0-9._:/=-]+$/.test(arg) ? arg : JSON.stringify(arg)).join(" ")}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readCiCdSecret(args: string[], resourceKnown: boolean, xml = false): string {
|
|
30
|
+
const value = resourceKnown ? readAzOutput(args) : undefined;
|
|
31
|
+
const valid = value && (xml
|
|
32
|
+
? /^(?:<\?xml[^>]*\?>\s*)?<publishData(?:\s[^>]*)?>[\s\S]*<\/publishData>$/.test(value)
|
|
33
|
+
&& /<publishProfile\s[^>]+(?:\/>|>[\s\S]*<\/publishProfile>)/.test(value)
|
|
34
|
+
: /^\S+$/.test(value));
|
|
35
|
+
if (valid) return ` ${value}\n`;
|
|
36
|
+
return ` Could not retrieve automatically${resourceKnown ? "" : " (resource name missing)"}.\n Run manually: ${manualAzCommand(args)}\n`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Human CLI only: called after deployment, migration tags, and ledger writes succeed.
|
|
40
|
+
function printProvisionSetup(plan: ProvisionPlanData, subscription: string): void {
|
|
41
|
+
if (!["bootstrap", "reconcile", "migrate"].includes(plan.mode)) return;
|
|
42
|
+
const target = ["--resource-group", plan.resourceGroup, "--subscription", subscription];
|
|
43
|
+
const fields = ["staticWebAppName", "staticWebAppUrl", "functionsAppName", "functionsAppUrl", "cosmosDbAccountName", "cosmosDatabaseName"] as const;
|
|
44
|
+
const outputArgs = [
|
|
45
|
+
"deployment", "group", "show", "--name", "swallowkit-baseline", ...target,
|
|
46
|
+
"--query", `{${fields.map((field) => `${field}:properties.outputs.${field}.value`).join(",")}}`, "-o", "json",
|
|
47
|
+
];
|
|
48
|
+
let outputs: Record<string, unknown> = {};
|
|
49
|
+
const raw = readAzOutput(outputArgs);
|
|
50
|
+
try {
|
|
51
|
+
const parsed: unknown = raw ? JSON.parse(raw) : undefined;
|
|
52
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Invalid outputs.");
|
|
53
|
+
outputs = parsed as Record<string, unknown>;
|
|
54
|
+
if (!fields.every((field) => typeof outputs[field] === "string" && outputs[field])) {
|
|
55
|
+
throw new Error("Resource outputs missing.");
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
console.warn(`Provision succeeded, but resource information could not be read. Do not redeploy to retrieve it.\nRun manually: ${manualAzCommand(outputArgs)}`);
|
|
59
|
+
}
|
|
60
|
+
console.log("\nResource information:");
|
|
61
|
+
console.log(` resourceGroup: ${plan.resourceGroup}`);
|
|
62
|
+
console.log(` subscription: ${subscription}`);
|
|
63
|
+
for (const field of fields) {
|
|
64
|
+
if (typeof outputs[field] === "string" && outputs[field]) console.log(` ${field}: ${outputs[field]}`);
|
|
65
|
+
}
|
|
66
|
+
const knownName = (value: unknown): value is string => typeof value === "string" && /^[A-Za-z0-9-]+$/.test(value);
|
|
67
|
+
const swaKnown = knownName(outputs.staticWebAppName);
|
|
68
|
+
const functionsKnown = knownName(outputs.functionsAppName);
|
|
69
|
+
const swaName = swaKnown ? String(outputs.staticWebAppName) : "<swa-name>";
|
|
70
|
+
const functionsName = functionsKnown ? String(outputs.functionsAppName) : "<functions-app-name>";
|
|
71
|
+
console.log("\nCI/CD settings (save these as secrets in GitHub Actions or Azure Pipelines):\n");
|
|
72
|
+
console.log(" [AZURE_STATIC_WEB_APPS_API_TOKEN]");
|
|
73
|
+
console.log(readCiCdSecret(["staticwebapp", "secrets", "list", "--name", swaName, ...target, "--query", "properties.apiKey", "-o", "tsv"], swaKnown));
|
|
74
|
+
console.log(" [AZURE_FUNCTIONAPP_NAME]");
|
|
75
|
+
console.log(functionsKnown ? ` ${functionsName}\n` : " Resource name missing; inspect the deployment outputs using the command above.\n");
|
|
76
|
+
console.log(" [AZURE_FUNCTIONAPP_PUBLISH_PROFILE]");
|
|
77
|
+
console.log(readCiCdSecret(["webapp", "deployment", "list-publishing-profiles", "--name", functionsName, ...target, "--xml"], functionsKnown, true));
|
|
78
|
+
console.log(" These values are shown only in this terminal; SwallowKit does not store them. Treat them as secrets.");
|
|
79
|
+
console.log(" Next: configure your CI/CD pipeline and manually trigger the first deployment.");
|
|
80
|
+
}
|
|
5
81
|
|
|
6
82
|
const PRIMARY_REGIONS = [
|
|
7
83
|
{ title: "Japan East (japaneast)", value: "japaneast" },
|
|
@@ -64,7 +140,8 @@ async function resolveLocations(options: ProvisionCliOptions): Promise<{ locatio
|
|
|
64
140
|
return { location, swaLocation };
|
|
65
141
|
}
|
|
66
142
|
|
|
67
|
-
export
|
|
143
|
+
export function createProvisionCommand(): Command {
|
|
144
|
+
return new Command("provision")
|
|
68
145
|
.description("Bootstrap, adopt, migrate, or explicitly reconcile Azure resources")
|
|
69
146
|
.requiredOption("-g, --resource-group <name>", "Resource group name")
|
|
70
147
|
.option("--subscription <id>", "Azure subscription ID")
|
|
@@ -78,10 +155,14 @@ export const provisionCommand = new Command("provision")
|
|
|
78
155
|
.action(async (options: ProvisionCliOptions) => {
|
|
79
156
|
ensureSwallowKitProject("provision");
|
|
80
157
|
const locations = await resolveLocations(options);
|
|
158
|
+
const subscription = options.subscription ?? readAzOutput(["account", "show", "--query", "id", "-o", "tsv"]);
|
|
159
|
+
if (!subscription || !/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(subscription)) {
|
|
160
|
+
throw new Error("Could not determine the Azure subscription. Run az account show --query id -o tsv, then retry with --subscription <id>.");
|
|
161
|
+
}
|
|
81
162
|
const baseline = options.baseline === undefined ? undefined : Number(options.baseline);
|
|
82
163
|
const plan = await planProvisionOperation({
|
|
83
164
|
resourceGroup: options.resourceGroup,
|
|
84
|
-
subscription
|
|
165
|
+
subscription,
|
|
85
166
|
location: locations.location,
|
|
86
167
|
swaLocation: locations.swaLocation,
|
|
87
168
|
whatIf: options.whatIf,
|
|
@@ -121,4 +202,9 @@ export const provisionCommand = new Command("provision")
|
|
|
121
202
|
|
|
122
203
|
const result = await applyProvisionOperation({ planId: plan.planId, approve: true });
|
|
123
204
|
console.log(`Provision completed (${plan.mode}). ${result.executedCommands.length} command(s) executed.`);
|
|
205
|
+
for (const warning of result.warnings) console.warn(`Warning: ${warning}`);
|
|
206
|
+
printProvisionSetup(plan, subscription);
|
|
124
207
|
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export const provisionCommand = createProvisionCommand();
|
|
@@ -851,7 +851,11 @@ export async function generateCosmosContainer(modelInfo: any, session: FileOpera
|
|
|
851
851
|
return;
|
|
852
852
|
}
|
|
853
853
|
const migration = generateModelContainerMigration(modelInfo, session);
|
|
854
|
-
|
|
854
|
+
if (migration) {
|
|
855
|
+
console.log(`✅ Created migration ${migration.version}: ${migration.template}`);
|
|
856
|
+
} else {
|
|
857
|
+
console.log(`ℹ️ Cosmos DB container is already owned by the migration baseline. Skipping infrastructure migration.`);
|
|
858
|
+
}
|
|
855
859
|
}
|
|
856
860
|
|
|
857
861
|
/**
|
|
@@ -68,6 +68,37 @@ function appendMigration(manifest: MigrationManifest, migration: InfrastructureM
|
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function baselineOwnsModelContainer(
|
|
72
|
+
manifest: MigrationManifest,
|
|
73
|
+
model: { name: string; partitionKey: string },
|
|
74
|
+
session: FileOperationSession
|
|
75
|
+
): boolean {
|
|
76
|
+
const containerSlug = normalizeSlug(model.name);
|
|
77
|
+
const containerPath = `infra/containers/${containerSlug}-container.bicep`;
|
|
78
|
+
const mainPath = "infra/main.bicep";
|
|
79
|
+
const baselinePaths = new Set(manifest.baseline.files.map((file) => file.path.replace(/\\/g, "/")));
|
|
80
|
+
if (!baselinePaths.has(containerPath) || !baselinePaths.has(mainPath)) return false;
|
|
81
|
+
|
|
82
|
+
const mainBicep = session.readFile(path.join(session.rootDirectory, mainPath));
|
|
83
|
+
const wired = mainBicep.includes(`'containers/${containerSlug}-container.bicep'`) ||
|
|
84
|
+
mainBicep.includes(`"containers/${containerSlug}-container.bicep"`);
|
|
85
|
+
if (!wired) return false;
|
|
86
|
+
|
|
87
|
+
const containerBicep = session.readFile(path.join(session.rootDirectory, containerPath));
|
|
88
|
+
const baselinePartitionKey = containerBicep.match(
|
|
89
|
+
/param\s+partitionKeyPath\s+string\s*=\s*['"]([^'"]+)['"]/
|
|
90
|
+
)?.[1];
|
|
91
|
+
if (baselinePartitionKey && baselinePartitionKey !== model.partitionKey) {
|
|
92
|
+
throw new MachineCommandError(
|
|
93
|
+
"baseline-container-configuration-change",
|
|
94
|
+
`Model ${model.name} changes the partition key of a baseline-owned container. Create an explicit infrastructure migration instead of regenerating the container.`,
|
|
95
|
+
{ model: model.name, baselinePartitionKey, requestedPartitionKey: model.partitionKey, containerPath },
|
|
96
|
+
"blocked"
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
71
102
|
function initializeMigrations(projectRoot: string, legacy: boolean): MigrationManifest {
|
|
72
103
|
const manifest = createMigrationManifest(projectRoot, { legacy });
|
|
73
104
|
const session = new FileOperationSession("commit", projectRoot);
|
|
@@ -124,9 +155,12 @@ param location string
|
|
|
124
155
|
export function generateModelContainerMigration(
|
|
125
156
|
model: { name: string; partitionKey: string },
|
|
126
157
|
session: FileOperationSession = new FileOperationSession("commit")
|
|
127
|
-
): InfrastructureMigration {
|
|
158
|
+
): InfrastructureMigration | null {
|
|
128
159
|
const manifest = readManifestForSession(session);
|
|
129
160
|
const existing = manifest.migrations.find((migration) => migration.source === "model" && migration.sourceModel === model.name);
|
|
161
|
+
if (!existing && baselineOwnsModelContainer(manifest, model, session)) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
130
164
|
const slug = normalizeSlug(`create-${model.name}-container`);
|
|
131
165
|
const version = existing?.version ?? manifest.migrations.length + 1;
|
|
132
166
|
const directory = path.join("infra", "migrations", `${formatVersion(version)}-${slug}`);
|
|
@@ -254,6 +254,32 @@ Additional model-change facts:
|
|
|
254
254
|
|
|
255
255
|
## New Models / CRUD Features
|
|
256
256
|
|
|
257
|
+
For every new persistent model, follow the current SwallowKit model-creation
|
|
258
|
+
and infrastructure-generation workflow, **whether or not it uses custom APIs**.
|
|
259
|
+
Adding a hand-written schema alone is not completion.
|
|
260
|
+
|
|
261
|
+
1. Inspect current capabilities and entities. Use
|
|
262
|
+
`{{runCmd}} swallowkit create-model <name>` or the agent-facing equivalent
|
|
263
|
+
`{{runCmd}} swallowkit machine generate model <name> --overwrite never`.
|
|
264
|
+
This creates the schema, shared export, and model metadata; it does not
|
|
265
|
+
create the Cosmos DB container by itself.
|
|
266
|
+
2. Author domain fields, then plan/apply scaffold for that model using the
|
|
267
|
+
current options. In 1.x, scaffold adds new Cosmos DB containers as versioned
|
|
268
|
+
Bicep migrations under `infra/migrations/`, recorded in its `manifest.json`.
|
|
269
|
+
Existing baseline-owned containers remain wired in `infra/main.bicep`;
|
|
270
|
+
do not add duplicate containers or edit applied migrations.
|
|
271
|
+
3. Verify the correspondence between the model, container name/partition key,
|
|
272
|
+
Bicep wiring or migration manifest, and `.swallowkit/artifacts.json`
|
|
273
|
+
generation ledger. Use `machine inspect entities`, `machine inspect infra`,
|
|
274
|
+
`machine inspect drift`, and `machine verify project`; inspect the actual
|
|
275
|
+
files too. Local generation/verification does not mean Azure was deployed.
|
|
276
|
+
|
|
277
|
+
Custom APIs and non-persistent types remain ai-authored. Connector-backed
|
|
278
|
+
models follow their connector workflow rather than inventing Cosmos containers.
|
|
279
|
+
If generation is omitted, state the reason and unfinished work explicitly.
|
|
280
|
+
Never describe a project as deployable while required persistence infrastructure
|
|
281
|
+
is missing; do not hand-edit managed infrastructure or the generation ledger.
|
|
282
|
+
|
|
257
283
|
Use the `swallowkit-add-model` Agent Skill when available.
|
|
258
284
|
|
|
259
285
|
Otherwise follow the equivalent runbook under `.swallowkit/workflows/`.
|
|
@@ -52,6 +52,13 @@ After changing a model, do not manually patch generated artifacts.
|
|
|
52
52
|
|
|
53
53
|
## After changing a model
|
|
54
54
|
|
|
55
|
+
- For new persistent models, follow the root `AGENTS.md` model-creation and
|
|
56
|
+
scaffold infrastructure workflow even when all APIs are custom. A schema
|
|
57
|
+
alone is not completion: verify container Bicep wiring or migrations and
|
|
58
|
+
`.swallowkit/artifacts.json`. Non-persistent types and connector-backed
|
|
59
|
+
models do not require invented Cosmos containers. If generation is omitted,
|
|
60
|
+
report the reason and unfinished infrastructure; never claim deployability
|
|
61
|
+
while required persistence infrastructure is missing.
|
|
55
62
|
- The shared package is consumed via its built `dist/`. `verify project`
|
|
56
63
|
rebuilds it automatically, but if you typecheck manually, build the shared
|
|
57
64
|
package first — "no exported member" errors usually mean a stale `dist/`.
|
|
@@ -41,6 +41,12 @@ export function buildWorkflowDocs(runCmd: string): WorkflowDoc[] {
|
|
|
41
41
|
|
|
42
42
|
Goal: add a data model and derive CRUD/BFF/UI/infra deterministically.
|
|
43
43
|
|
|
44
|
+
For persistent models, this workflow is required even when all APIs are custom.
|
|
45
|
+
A hand-written schema alone is not completion. Non-persistent types remain
|
|
46
|
+
ai-authored; connector-backed models follow their connector workflow without
|
|
47
|
+
inventing Cosmos containers. If generation is omitted, report the reason and
|
|
48
|
+
unfinished work; never claim deployability with required infrastructure missing.
|
|
49
|
+
|
|
44
50
|
## Responsibility boundary
|
|
45
51
|
|
|
46
52
|
- **You author freely**: the model schema (\`shared/models/<name>.ts\`) and any business logic.
|
|
@@ -60,6 +66,13 @@ Goal: add a data model and derive CRUD/BFF/UI/infra deterministically.
|
|
|
60
66
|
5. Verify:
|
|
61
67
|
\`${runCmd} swallowkit machine verify project\`
|
|
62
68
|
- \`summary.done: false\` → \`${runCmd} swallowkit machine explain failure\` and repair, then re-verify.
|
|
69
|
+
- Check model/container name and partition key, versioned container
|
|
70
|
+
migrations in \`infra/migrations/manifest.json\` (or existing baseline-owned
|
|
71
|
+
container wiring in \`infra/main.bicep\`), and the
|
|
72
|
+
generation ledger \`.swallowkit/artifacts.json\`. Use \`machine inspect entities\`,
|
|
73
|
+
\`machine inspect infra\`, and \`machine inspect drift\` and inspect actual files.
|
|
74
|
+
Model creation alone does not generate containers; scaffold does.
|
|
75
|
+
Local verification is not proof of Azure deployment.
|
|
63
76
|
6. If \`dev-seeds/\` exists, update seed JSON for the new model. Seeds are only
|
|
64
77
|
applied by \`${runCmd} swallowkit dev --seed-env <environment>\` — editing the
|
|
65
78
|
JSON alone does nothing.
|
|
@@ -337,7 +337,7 @@ export function generateDetailPage(model: ModelInfo, sharedPackageName: string,
|
|
|
337
337
|
|
|
338
338
|
return `'use client';
|
|
339
339
|
|
|
340
|
-
import { useEffect, useState } from 'react';
|
|
340
|
+
import { Suspense, useEffect, useState } from 'react';
|
|
341
341
|
import { useParams, useRouter } from 'next/navigation';
|
|
342
342
|
import Link from 'next/link';
|
|
343
343
|
import { z } from 'zod/v4';
|
|
@@ -347,6 +347,18 @@ ${hasAuth ? `import { useAuth } from '${authContextImport}';` : ''}
|
|
|
347
347
|
type ${modelName} = z.infer<typeof ${localSchemaName}>;
|
|
348
348
|
|
|
349
349
|
export default function ${modelName}DetailPage() {
|
|
350
|
+
return (
|
|
351
|
+
<Suspense fallback={
|
|
352
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
353
|
+
<div className="text-lg text-gray-900 dark:text-gray-100">Loading...</div>
|
|
354
|
+
</div>
|
|
355
|
+
}>
|
|
356
|
+
<${modelName}DetailContent />
|
|
357
|
+
</Suspense>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function ${modelName}DetailContent() {
|
|
350
362
|
const params = useParams();
|
|
351
363
|
const router = useRouter();
|
|
352
364
|
const [${modelCamel}, set${modelName}] = useState<${modelName} | null>(null);
|
|
@@ -1031,7 +1043,7 @@ export function generateEditPage(model: ModelInfo, sharedPackageName: string, au
|
|
|
1031
1043
|
|
|
1032
1044
|
return `'use client';
|
|
1033
1045
|
|
|
1034
|
-
import { useEffect, useState } from 'react';
|
|
1046
|
+
import { Suspense, useEffect, useState } from 'react';
|
|
1035
1047
|
import { useParams, useRouter } from 'next/navigation';
|
|
1036
1048
|
import ${modelName}Form from '../../_components/${modelName}Form';
|
|
1037
1049
|
import { z } from 'zod/v4';
|
|
@@ -1041,6 +1053,18 @@ ${hasAuth ? `import { useAuth } from '${authContextImport}';` : ''}
|
|
|
1041
1053
|
type ${modelName} = z.infer<typeof ${localSchemaName}>;
|
|
1042
1054
|
|
|
1043
1055
|
export default function Edit${modelName}Page() {
|
|
1056
|
+
return (
|
|
1057
|
+
<Suspense fallback={
|
|
1058
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
1059
|
+
<div className="text-lg">Loading...</div>
|
|
1060
|
+
</div>
|
|
1061
|
+
}>
|
|
1062
|
+
<Edit${modelName}Content />
|
|
1063
|
+
</Suspense>
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function Edit${modelName}Content() {
|
|
1044
1068
|
const params = useParams();
|
|
1045
1069
|
${hasAuth ? ' const router = useRouter();' : ''}
|
|
1046
1070
|
const [${modelCamel}, set${modelName}] = useState<${modelName} | null>(null);
|