swallowkit 1.0.0-beta.7 → 1.0.0-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -0
- package/dist/cli/commands/dev-seeds.d.ts +35 -0
- package/dist/cli/commands/dev-seeds.d.ts.map +1 -0
- package/dist/cli/commands/dev-seeds.js +292 -0
- package/dist/cli/commands/dev-seeds.js.map +1 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +57 -53
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +27 -20
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/dev-seeds.test.ts +112 -0
- package/src/__tests__/init.test.ts +21 -0
- package/src/cli/commands/dev-seeds.ts +358 -0
- package/src/cli/commands/dev.ts +68 -57
- package/src/cli/commands/index.ts +1 -0
- package/src/cli/commands/init.ts +27 -20
- package/src/cli/index.ts +3 -2
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { CosmosClient, Database, PartitionKeyKind } from "@azure/cosmos";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { ensureSwallowKitProject } from "../../core/config";
|
|
6
|
+
import { FieldInfo, ModelInfo, getAllModels, toKebabCase } from "../../core/scaffold/model-parser";
|
|
7
|
+
|
|
8
|
+
export type SeedDocument = Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
interface GenerateDevSeedTemplatesOptions {
|
|
11
|
+
environment: string;
|
|
12
|
+
modelsDir?: string;
|
|
13
|
+
seedsDir?: string;
|
|
14
|
+
force?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ApplyDevSeedEnvironmentOptions {
|
|
18
|
+
client: CosmosClient;
|
|
19
|
+
databaseName: string;
|
|
20
|
+
environment: string;
|
|
21
|
+
models: ModelInfo[];
|
|
22
|
+
seedsDir?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface LoadedSeedFile {
|
|
26
|
+
model: ModelInfo;
|
|
27
|
+
containerName: string;
|
|
28
|
+
documents: SeedDocument[];
|
|
29
|
+
filePath: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getContainerNameForModel(model: Pick<ModelInfo, "name">): string {
|
|
33
|
+
return `${model.name}s`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getSeedEnvironmentDir(environment: string, seedsDir: string = "dev-seeds"): string {
|
|
37
|
+
return path.join(process.cwd(), seedsDir, environment);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function normalizeSeedIdentifier(value: string): string {
|
|
41
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function parseSeedDocuments(content: string, filePath: string): SeedDocument[] {
|
|
45
|
+
const parsed: unknown = JSON.parse(content);
|
|
46
|
+
|
|
47
|
+
if (Array.isArray(parsed)) {
|
|
48
|
+
return parsed.map((item, index) => {
|
|
49
|
+
if (!isSeedDocument(item)) {
|
|
50
|
+
throw new Error(`${filePath} must contain only JSON objects. Invalid item at index ${index}.`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return item;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (isSeedDocument(parsed)) {
|
|
58
|
+
return [parsed];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
throw new Error(`${filePath} must contain a JSON object or an array of JSON objects.`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function loadProjectModels(modelsDir: string = "shared/models"): Promise<ModelInfo[]> {
|
|
65
|
+
return getAllModels(modelsDir);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildSeedTemplateDocument(
|
|
69
|
+
model: ModelInfo,
|
|
70
|
+
allModels: ModelInfo[] = [model],
|
|
71
|
+
seenModels: Set<string> = new Set()
|
|
72
|
+
): SeedDocument {
|
|
73
|
+
const modelLookup = new Map(allModels.map((candidate) => [candidate.name, candidate]));
|
|
74
|
+
return buildSeedTemplateDocumentInternal(model, modelLookup, seenModels);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function loadDevSeedFiles(
|
|
78
|
+
environment: string,
|
|
79
|
+
models: ModelInfo[],
|
|
80
|
+
seedsDir: string = "dev-seeds"
|
|
81
|
+
): Promise<LoadedSeedFile[]> {
|
|
82
|
+
const environmentDir = getSeedEnvironmentDir(environment, seedsDir);
|
|
83
|
+
if (!fs.existsSync(environmentDir)) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const modelAliases = buildModelAliasMap(models);
|
|
88
|
+
const seedFiles = fs.readdirSync(environmentDir).filter((entry) => entry.endsWith(".json"));
|
|
89
|
+
const loaded: LoadedSeedFile[] = [];
|
|
90
|
+
|
|
91
|
+
for (const fileName of seedFiles) {
|
|
92
|
+
const filePath = path.join(environmentDir, fileName);
|
|
93
|
+
const fileStem = path.basename(fileName, ".json");
|
|
94
|
+
const model = modelAliases.get(normalizeSeedIdentifier(fileStem));
|
|
95
|
+
|
|
96
|
+
if (!model) {
|
|
97
|
+
console.warn(`⚠️ Skipping seed file without matching schema: ${filePath}`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const documents = parseSeedDocuments(fs.readFileSync(filePath, "utf-8"), filePath);
|
|
102
|
+
validateSeedDocuments(documents, filePath);
|
|
103
|
+
|
|
104
|
+
loaded.push({
|
|
105
|
+
model,
|
|
106
|
+
containerName: getContainerNameForModel(model),
|
|
107
|
+
documents,
|
|
108
|
+
filePath,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return loaded;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function applyDevSeedEnvironment({
|
|
116
|
+
client,
|
|
117
|
+
databaseName,
|
|
118
|
+
environment,
|
|
119
|
+
models,
|
|
120
|
+
seedsDir = "dev-seeds",
|
|
121
|
+
}: ApplyDevSeedEnvironmentOptions): Promise<boolean> {
|
|
122
|
+
const environmentDir = getSeedEnvironmentDir(environment, seedsDir);
|
|
123
|
+
if (!fs.existsSync(environmentDir)) {
|
|
124
|
+
console.log(`ℹ️ Seed environment "${environment}" not found at ${environmentDir}. Keeping existing Cosmos DB data.`);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const seedFiles = await loadDevSeedFiles(environment, models, seedsDir);
|
|
129
|
+
if (seedFiles.length === 0) {
|
|
130
|
+
console.log(`ℹ️ No seed files found for environment "${environment}". Keeping existing Cosmos DB data.`);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const database = client.database(databaseName);
|
|
135
|
+
console.log(`🧪 Applying Cosmos DB seed data for environment "${environment}"...`);
|
|
136
|
+
|
|
137
|
+
for (const seedFile of seedFiles) {
|
|
138
|
+
await recreateContainer(database, seedFile.containerName);
|
|
139
|
+
const container = database.container(seedFile.containerName);
|
|
140
|
+
|
|
141
|
+
for (const document of seedFile.documents) {
|
|
142
|
+
await container.items.create(document);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log(
|
|
146
|
+
`✅ Seeded "${seedFile.containerName}" with ${seedFile.documents.length} item(s) from ${path.basename(seedFile.filePath)}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log("✅ Cosmos DB seed replacement complete\n");
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function generateDevSeedTemplates({
|
|
155
|
+
environment,
|
|
156
|
+
modelsDir = "shared/models",
|
|
157
|
+
seedsDir = "dev-seeds",
|
|
158
|
+
force = false,
|
|
159
|
+
}: GenerateDevSeedTemplatesOptions): Promise<void> {
|
|
160
|
+
ensureSwallowKitProject("create-dev-seeds");
|
|
161
|
+
|
|
162
|
+
console.log(`🧪 Generating dev seed templates for environment "${environment}"...\n`);
|
|
163
|
+
const models = await loadProjectModels(modelsDir);
|
|
164
|
+
|
|
165
|
+
if (models.length === 0) {
|
|
166
|
+
console.log("⚠️ No schemas found under shared/models. Nothing was generated.");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const environmentDir = getSeedEnvironmentDir(environment, seedsDir);
|
|
171
|
+
fs.mkdirSync(environmentDir, { recursive: true });
|
|
172
|
+
|
|
173
|
+
const writtenFiles: string[] = [];
|
|
174
|
+
const skippedFiles: string[] = [];
|
|
175
|
+
|
|
176
|
+
for (const model of models) {
|
|
177
|
+
const filePath = path.join(environmentDir, `${toKebabCase(model.name)}.json`);
|
|
178
|
+
if (!force && fs.existsSync(filePath)) {
|
|
179
|
+
skippedFiles.push(filePath);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const content = JSON.stringify([buildSeedTemplateDocument(model, models)], null, 2) + "\n";
|
|
184
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
185
|
+
writtenFiles.push(filePath);
|
|
186
|
+
console.log(`✅ Created: ${filePath}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (skippedFiles.length > 0) {
|
|
190
|
+
console.log("");
|
|
191
|
+
console.log(`⏭️ Skipped ${skippedFiles.length} existing file(s). Use --force to overwrite them.`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
console.log("");
|
|
195
|
+
console.log("📝 Next steps:");
|
|
196
|
+
console.log(` 1. Edit JSON files under ${environmentDir}`);
|
|
197
|
+
console.log(` 2. Run 'swallowkit dev --seed-env ${environment}' to replace emulator data before startup`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const devSeedsCommand = new Command()
|
|
201
|
+
.name("create-dev-seeds")
|
|
202
|
+
.description("Generate dev seed JSON templates under dev-seeds/<environment> from shared/models schemas")
|
|
203
|
+
.argument("<environment>", "Seed environment name")
|
|
204
|
+
.option("--models-dir <dir>", "Models directory", "shared/models")
|
|
205
|
+
.option("--seeds-dir <dir>", "Dev seeds directory", "dev-seeds")
|
|
206
|
+
.option("--force", "Overwrite existing seed JSON files", false)
|
|
207
|
+
.action(async (environment: string, options: { modelsDir?: string; seedsDir?: string; force?: boolean }) => {
|
|
208
|
+
await generateDevSeedTemplates({
|
|
209
|
+
environment,
|
|
210
|
+
modelsDir: options.modelsDir,
|
|
211
|
+
seedsDir: options.seedsDir,
|
|
212
|
+
force: options.force,
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
function buildSeedTemplateDocumentInternal(
|
|
217
|
+
model: ModelInfo,
|
|
218
|
+
modelLookup: Map<string, ModelInfo>,
|
|
219
|
+
seenModels: Set<string>
|
|
220
|
+
): SeedDocument {
|
|
221
|
+
const nextSeen = new Set(seenModels);
|
|
222
|
+
nextSeen.add(model.name);
|
|
223
|
+
|
|
224
|
+
const document: SeedDocument = {};
|
|
225
|
+
|
|
226
|
+
for (const field of model.fields) {
|
|
227
|
+
document[field.name] = buildTemplateValueForField(model, field, modelLookup, nextSeen);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return document;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function buildTemplateValueForField(
|
|
234
|
+
model: ModelInfo,
|
|
235
|
+
field: FieldInfo,
|
|
236
|
+
modelLookup: Map<string, ModelInfo>,
|
|
237
|
+
seenModels: Set<string>
|
|
238
|
+
): unknown {
|
|
239
|
+
if (field.isNestedSchema && field.nestedModelName) {
|
|
240
|
+
const nestedModel = modelLookup.get(field.nestedModelName);
|
|
241
|
+
if (nestedModel) {
|
|
242
|
+
if (seenModels.has(nestedModel.name)) {
|
|
243
|
+
const fallback = { id: `${toKebabCase(nestedModel.name)}-001` };
|
|
244
|
+
return field.isArray ? [fallback] : fallback;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const nestedDocument = buildSeedTemplateDocumentInternal(nestedModel, modelLookup, seenModels);
|
|
248
|
+
return field.isArray ? [nestedDocument] : nestedDocument;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (field.isArray) {
|
|
253
|
+
return [buildScalarTemplateValue(model, field)];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return buildScalarTemplateValue(model, field);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function buildScalarTemplateValue(model: ModelInfo, field: FieldInfo): unknown {
|
|
260
|
+
if (field.name === "id") {
|
|
261
|
+
return `${toKebabCase(model.name)}-001`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (field.enumValues && field.enumValues.length > 0) {
|
|
265
|
+
return field.enumValues[0];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (field.type === "number") {
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (field.type === "boolean") {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (field.type === "date" || field.name.endsWith("At")) {
|
|
277
|
+
return "2026-01-01T00:00:00.000Z";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (field.type === "object") {
|
|
281
|
+
return {};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return `${toKebabCase(model.name)}-${toKebabCase(field.name)}-sample`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function buildModelAliasMap(models: ModelInfo[]): Map<string, ModelInfo> {
|
|
288
|
+
const aliases = new Map<string, ModelInfo>();
|
|
289
|
+
|
|
290
|
+
for (const model of models) {
|
|
291
|
+
const candidates = [
|
|
292
|
+
model.name,
|
|
293
|
+
model.schemaName,
|
|
294
|
+
toKebabCase(model.name),
|
|
295
|
+
path.basename(model.filePath, path.extname(model.filePath)),
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
for (const candidate of candidates) {
|
|
299
|
+
const normalized = normalizeSeedIdentifier(candidate);
|
|
300
|
+
if (!aliases.has(normalized)) {
|
|
301
|
+
aliases.set(normalized, model);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return aliases;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function isSeedDocument(value: unknown): value is SeedDocument {
|
|
310
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function validateSeedDocuments(documents: SeedDocument[], filePath: string): void {
|
|
314
|
+
const ids = new Set<string>();
|
|
315
|
+
|
|
316
|
+
documents.forEach((document, index) => {
|
|
317
|
+
if (typeof document.id !== "string" || document.id.trim().length === 0) {
|
|
318
|
+
throw new Error(`${filePath} item at index ${index} must contain a non-empty string id.`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (ids.has(document.id)) {
|
|
322
|
+
throw new Error(`${filePath} contains duplicate id "${document.id}".`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
ids.add(document.id);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function recreateContainer(database: Database, containerName: string): Promise<void> {
|
|
330
|
+
try {
|
|
331
|
+
await database.container(containerName).delete();
|
|
332
|
+
} catch (error: any) {
|
|
333
|
+
if (error?.code !== 404) {
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
await database.containers.createIfNotExists({
|
|
340
|
+
id: containerName,
|
|
341
|
+
partitionKey: {
|
|
342
|
+
paths: ["/id"],
|
|
343
|
+
kind: PartitionKeyKind.Hash,
|
|
344
|
+
version: 2,
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
} catch (error: unknown) {
|
|
348
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
349
|
+
console.log(`⚠️ Failed to recreate "${containerName}" with full partition key definition: ${message}`);
|
|
350
|
+
console.log("🔄 Retrying with simple partition key...");
|
|
351
|
+
await database.containers.createIfNotExists({
|
|
352
|
+
id: containerName,
|
|
353
|
+
partitionKey: {
|
|
354
|
+
paths: ["/id"],
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -5,6 +5,8 @@ import * as fs from 'fs';
|
|
|
5
5
|
import * as os from 'os';
|
|
6
6
|
import { CosmosClient, PartitionKeyKind } from '@azure/cosmos';
|
|
7
7
|
import { ensureSwallowKitProject, getBackendLanguage } from '../../core/config';
|
|
8
|
+
import { ModelInfo } from '../../core/scaffold/model-parser';
|
|
9
|
+
import { applyDevSeedEnvironment, getContainerNameForModel, loadProjectModels } from './dev-seeds';
|
|
8
10
|
import { BackendLanguage } from '../../types';
|
|
9
11
|
import { detectFromProject, getCommands } from '../../utils/package-manager';
|
|
10
12
|
|
|
@@ -15,6 +17,7 @@ interface DevOptions {
|
|
|
15
17
|
open?: boolean;
|
|
16
18
|
verbose?: boolean;
|
|
17
19
|
noFunctions?: boolean;
|
|
20
|
+
seedEnv?: string;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
export function buildFunctionsStartArgs(functionsPort: string): string[] {
|
|
@@ -319,7 +322,8 @@ export const devCommand = new Command()
|
|
|
319
322
|
.option('--open', 'Open browser automatically', false)
|
|
320
323
|
.option('--verbose', 'Show verbose logs', false)
|
|
321
324
|
.option('--no-functions', 'Skip Azure Functions startup', false)
|
|
322
|
-
.
|
|
325
|
+
.option('--seed-env <environment>', 'Replace Cosmos DB Emulator data from dev-seeds/<environment> before startup')
|
|
326
|
+
.action(async (options: DevOptions & { functionsPort?: string; noFunctions?: boolean; seedEnv?: string }) => {
|
|
323
327
|
// SwallowKit プロジェクトディレクトリかどうかを検証
|
|
324
328
|
ensureSwallowKitProject("dev");
|
|
325
329
|
|
|
@@ -331,7 +335,14 @@ export const devCommand = new Command()
|
|
|
331
335
|
await startDevEnvironment(options);
|
|
332
336
|
});
|
|
333
337
|
|
|
334
|
-
|
|
338
|
+
interface CosmosInitializationResult {
|
|
339
|
+
endpoint: string;
|
|
340
|
+
key: string;
|
|
341
|
+
databaseName: string;
|
|
342
|
+
models: ModelInfo[];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function initializeCosmosDB(databaseName: string): Promise<CosmosInitializationResult | null> {
|
|
335
346
|
try {
|
|
336
347
|
// Read local.settings.json from functions directory
|
|
337
348
|
const functionsDir = path.join(process.cwd(), 'functions');
|
|
@@ -339,7 +350,7 @@ async function initializeCosmosDB(databaseName: string): Promise<void> {
|
|
|
339
350
|
|
|
340
351
|
if (!fs.existsSync(localSettingsPath)) {
|
|
341
352
|
console.log('⚠️ local.settings.json not found. Skipping Cosmos DB initialization.');
|
|
342
|
-
return;
|
|
353
|
+
return null;
|
|
343
354
|
}
|
|
344
355
|
|
|
345
356
|
const localSettings = JSON.parse(fs.readFileSync(localSettingsPath, 'utf-8'));
|
|
@@ -348,7 +359,7 @@ async function initializeCosmosDB(databaseName: string): Promise<void> {
|
|
|
348
359
|
|
|
349
360
|
if (!connectionString) {
|
|
350
361
|
console.log('⚠️ CosmosDBConnection not found in local.settings.json. Skipping Cosmos DB initialization.');
|
|
351
|
-
return;
|
|
362
|
+
return null;
|
|
352
363
|
}
|
|
353
364
|
|
|
354
365
|
console.log('🗄️ Initializing Cosmos DB...');
|
|
@@ -359,7 +370,7 @@ async function initializeCosmosDB(databaseName: string): Promise<void> {
|
|
|
359
370
|
|
|
360
371
|
if (!endpointMatch || !keyMatch) {
|
|
361
372
|
console.log('⚠️ Invalid CosmosDB connection string format.');
|
|
362
|
-
return;
|
|
373
|
+
return null;
|
|
363
374
|
}
|
|
364
375
|
|
|
365
376
|
const endpoint = endpointMatch[1];
|
|
@@ -373,72 +384,61 @@ async function initializeCosmosDB(databaseName: string): Promise<void> {
|
|
|
373
384
|
const { database } = await client.databases.createIfNotExists({ id: dbName });
|
|
374
385
|
console.log(`✅ Database "${dbName}" ready`);
|
|
375
386
|
|
|
376
|
-
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
const scaffoldConfigContent = fs.readFileSync(scaffoldConfigPath, 'utf-8');
|
|
387
|
+
const models = await loadProjectModels();
|
|
388
|
+
for (const model of models) {
|
|
389
|
+
const containerName = getContainerNameForModel(model);
|
|
380
390
|
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
// Try creating container with full partition key definition first
|
|
393
|
-
let containerCreated = false;
|
|
394
|
-
|
|
395
|
-
try {
|
|
396
|
-
console.log(`🔧 Creating container "${containerName}" with partition key /id...`);
|
|
397
|
-
const containerResponse = await database.containers.createIfNotExists({
|
|
398
|
-
id: containerName,
|
|
399
|
-
partitionKey: {
|
|
400
|
-
paths: ['/id'],
|
|
401
|
-
kind: PartitionKeyKind.Hash,
|
|
402
|
-
version: 2
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
|
-
console.log(`✅ Container "${containerName}" ready (status: ${containerResponse.statusCode})`);
|
|
406
|
-
containerCreated = true;
|
|
407
|
-
} catch (error: any) {
|
|
408
|
-
console.log(`⚠️ Failed with full partition key definition: ${error.message}`);
|
|
409
|
-
console.log(`🔄 Retrying with simple partition key...`);
|
|
391
|
+
// Try creating container with full partition key definition first
|
|
392
|
+
let containerCreated = false;
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
console.log(`🔧 Creating container "${containerName}" with partition key /id...`);
|
|
396
|
+
const containerResponse = await database.containers.createIfNotExists({
|
|
397
|
+
id: containerName,
|
|
398
|
+
partitionKey: {
|
|
399
|
+
paths: ['/id'],
|
|
400
|
+
kind: PartitionKeyKind.Hash,
|
|
401
|
+
version: 2
|
|
410
402
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
// Continue with other containers
|
|
403
|
+
});
|
|
404
|
+
console.log(`✅ Container "${containerName}" ready (status: ${containerResponse.statusCode})`);
|
|
405
|
+
containerCreated = true;
|
|
406
|
+
} catch (error: unknown) {
|
|
407
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
408
|
+
console.log(`⚠️ Failed with full partition key definition: ${message}`);
|
|
409
|
+
console.log(`🔄 Retrying with simple partition key...`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// If first attempt failed, try with simple partition key definition
|
|
413
|
+
if (!containerCreated) {
|
|
414
|
+
try {
|
|
415
|
+
const containerResponse = await database.containers.createIfNotExists({
|
|
416
|
+
id: containerName,
|
|
417
|
+
partitionKey: {
|
|
418
|
+
paths: ['/id']
|
|
429
419
|
}
|
|
420
|
+
});
|
|
421
|
+
console.log(`✅ Container "${containerName}" ready (status: ${containerResponse.statusCode})`);
|
|
422
|
+
} catch (containerError: any) {
|
|
423
|
+
console.error(`❌ Failed to create container "${containerName}":`, containerError.message);
|
|
424
|
+
console.error(`Error code: ${containerError.code}`);
|
|
425
|
+
if (containerError.body) {
|
|
426
|
+
console.error(`Response body:`, JSON.stringify(containerError.body, null, 2));
|
|
430
427
|
}
|
|
428
|
+
// Continue with other containers
|
|
431
429
|
}
|
|
432
430
|
}
|
|
433
431
|
}
|
|
434
432
|
|
|
435
433
|
console.log('✅ Cosmos DB initialization complete\n');
|
|
434
|
+
return { endpoint, key: keyMatch[1], databaseName: dbName, models };
|
|
436
435
|
} catch (error: any) {
|
|
437
436
|
console.error('⚠️ Cosmos DB initialization failed:', error.message);
|
|
438
437
|
if (error.stack) {
|
|
439
438
|
console.error('Stack trace:', error.stack);
|
|
440
439
|
}
|
|
441
440
|
console.log('💡 Make sure Cosmos DB Emulator is running');
|
|
441
|
+
return null;
|
|
442
442
|
}
|
|
443
443
|
}
|
|
444
444
|
|
|
@@ -563,7 +563,18 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
563
563
|
const appName = packageJson.name || 'App';
|
|
564
564
|
const databaseName = `${appName.charAt(0).toUpperCase() + appName.slice(1)}Database`;
|
|
565
565
|
|
|
566
|
-
await initializeCosmosDB(databaseName);
|
|
566
|
+
const cosmosInitialization = await initializeCosmosDB(databaseName);
|
|
567
|
+
if (options.seedEnv && cosmosInitialization) {
|
|
568
|
+
await applyDevSeedEnvironment({
|
|
569
|
+
client: new CosmosClient({
|
|
570
|
+
endpoint: cosmosInitialization.endpoint,
|
|
571
|
+
key: cosmosInitialization.key,
|
|
572
|
+
}),
|
|
573
|
+
databaseName: cosmosInitialization.databaseName,
|
|
574
|
+
environment: options.seedEnv,
|
|
575
|
+
models: cosmosInitialization.models,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
567
578
|
|
|
568
579
|
console.log('');
|
|
569
580
|
console.log('🚀 Starting Azure Functions...');
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -424,6 +424,30 @@ export function buildCSharpFunctionsProjectSource(): string {
|
|
|
424
424
|
`;
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
+
export function buildSwallowKitConfigSource(backendLanguage: BackendLanguage): string {
|
|
428
|
+
return `module.exports = {
|
|
429
|
+
backend: {
|
|
430
|
+
language: '${backendLanguage}',
|
|
431
|
+
},
|
|
432
|
+
functions: {
|
|
433
|
+
baseUrl: process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
|
|
434
|
+
},
|
|
435
|
+
deployment: {
|
|
436
|
+
resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
|
|
437
|
+
swaName: process.env.AZURE_SWA_NAME || '',
|
|
438
|
+
},
|
|
439
|
+
}
|
|
440
|
+
`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function buildGeneratedProjectDependencies(projectName: string): Record<string, string> {
|
|
444
|
+
return {
|
|
445
|
+
'@azure/cosmos': '^4.0.0',
|
|
446
|
+
'applicationinsights': '^3.3.0',
|
|
447
|
+
[`@${projectName}/shared`]: '*',
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
427
451
|
async function addSwallowKitFiles(
|
|
428
452
|
projectDir: string,
|
|
429
453
|
options: InitOptions,
|
|
@@ -436,18 +460,14 @@ async function addSwallowKitFiles(
|
|
|
436
460
|
|
|
437
461
|
const projectName = options.name;
|
|
438
462
|
|
|
439
|
-
// 1. Update package.json to add
|
|
463
|
+
// 1. Update package.json to add runtime dependencies for generated projects
|
|
440
464
|
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
441
465
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
442
466
|
|
|
443
|
-
// Add SwallowKit dependencies (Next.js version already upgraded by upgradeNextJs)
|
|
444
467
|
// zod is in the shared workspace package, not here
|
|
445
468
|
packageJson.dependencies = {
|
|
446
469
|
...packageJson.dependencies,
|
|
447
|
-
|
|
448
|
-
'@azure/cosmos': '^4.0.0',
|
|
449
|
-
'applicationinsights': '^3.3.0',
|
|
450
|
-
[`@${projectName}/shared`]: '*',
|
|
470
|
+
...buildGeneratedProjectDependencies(projectName),
|
|
451
471
|
};
|
|
452
472
|
|
|
453
473
|
if (backendLanguage !== "typescript") {
|
|
@@ -526,20 +546,7 @@ async function addSwallowKitFiles(
|
|
|
526
546
|
}
|
|
527
547
|
|
|
528
548
|
// 3. Create SwallowKit config
|
|
529
|
-
const swallowkitConfig =
|
|
530
|
-
module.exports = {
|
|
531
|
-
backend: {
|
|
532
|
-
language: '${backendLanguage}',
|
|
533
|
-
},
|
|
534
|
-
functions: {
|
|
535
|
-
baseUrl: process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
|
|
536
|
-
},
|
|
537
|
-
deployment: {
|
|
538
|
-
resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
|
|
539
|
-
swaName: process.env.AZURE_SWA_NAME || '',
|
|
540
|
-
},
|
|
541
|
-
}
|
|
542
|
-
`;
|
|
549
|
+
const swallowkitConfig = buildSwallowKitConfigSource(backendLanguage);
|
|
543
550
|
fs.writeFileSync(path.join(projectDir, 'swallowkit.config.js'), swallowkitConfig);
|
|
544
551
|
|
|
545
552
|
// 4. Create shared workspace package for Zod models (Single Source of Truth)
|
package/src/cli/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ if (process.platform === 'win32') {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
import { Command } from "commander";
|
|
11
|
-
import { initCommand, devCommand, scaffoldCommand, createModelCommand } from "./commands";
|
|
11
|
+
import { initCommand, devCommand, devSeedsCommand, scaffoldCommand, createModelCommand } from "./commands";
|
|
12
12
|
import { provisionCommand } from "./commands/provision";
|
|
13
13
|
|
|
14
14
|
const program = new Command();
|
|
@@ -16,7 +16,7 @@ const program = new Command();
|
|
|
16
16
|
program
|
|
17
17
|
.name("swallowkit")
|
|
18
18
|
.description("Next.js framework optimized for Azure deployment - Automatically splits SSR into individual Azure Functions")
|
|
19
|
-
.version("1.0.0-beta.
|
|
19
|
+
.version("1.0.0-beta.9");
|
|
20
20
|
|
|
21
21
|
// Register commands
|
|
22
22
|
program
|
|
@@ -41,6 +41,7 @@ program
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
program.addCommand(devCommand);
|
|
44
|
+
program.addCommand(devSeedsCommand);
|
|
44
45
|
|
|
45
46
|
program.addCommand(provisionCommand);
|
|
46
47
|
|