@paroicms/site-generator-plugin 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/gen-backend/ddl/site-generator.ddl.sql +57 -9
  2. package/gen-backend/dist/commands/execute-command.js +35 -9
  3. package/gen-backend/dist/commands/generator-session.js +49 -10
  4. package/gen-backend/dist/data-format.js +32 -4
  5. package/gen-backend/dist/db/db-init.js +3 -1
  6. package/gen-backend/dist/db/db-read.queries.js +142 -0
  7. package/gen-backend/dist/db/db-write.queries.js +144 -0
  8. package/gen-backend/dist/db/ddl-migration.js +8 -6
  9. package/gen-backend/dist/db/formatters.js +46 -0
  10. package/gen-backend/dist/generator/fake-content-generator.ts/content-report.js +9 -5
  11. package/gen-backend/dist/generator/fake-content-generator.ts/create-database-with-fake-content.js +18 -13
  12. package/gen-backend/dist/generator/fake-content-generator.ts/generate-fake-content.js +16 -12
  13. package/gen-backend/dist/generator/fake-content-generator.ts/invoke-generate-fake-content.js +26 -17
  14. package/gen-backend/dist/generator/lib/calling-llm-anthropic.js +33 -0
  15. package/gen-backend/dist/generator/lib/calling-llm-mistral.js +156 -0
  16. package/gen-backend/dist/generator/lib/create-prompt.js +2 -2
  17. package/gen-backend/dist/generator/lib/debug-utils.js +74 -48
  18. package/gen-backend/dist/generator/lib/llm-tokens.js +7 -9
  19. package/gen-backend/dist/generator/lib/llm-utils.js +8 -0
  20. package/gen-backend/dist/generator/lib/prompt-template.js +10 -0
  21. package/gen-backend/dist/generator/lib/session-utils.js +31 -0
  22. package/gen-backend/dist/generator/llm-queries/invoke-message-guard.js +20 -9
  23. package/gen-backend/dist/generator/llm-queries/invoke-new-site-analysis.js +73 -47
  24. package/gen-backend/dist/generator/llm-queries/invoke-update-site-schema.js +106 -43
  25. package/gen-backend/dist/generator/site-generator/site-generator.js +26 -18
  26. package/gen-backend/dist/lib/create-raw-context.js +31 -0
  27. package/gen-backend/dist/lib/site-remover.js +1 -1
  28. package/gen-backend/dist/plugin.js +8 -54
  29. package/gen-backend/prompts/generate-fake-content-multiple-documents.md +5 -5
  30. package/gen-backend/prompts/generate-fake-content-multiple-parts.md +5 -5
  31. package/gen-backend/prompts/generate-fake-content-single.md +4 -4
  32. package/gen-backend/prompts/{new-site-1-analysis.md → initial-1-analysis.md} +38 -29
  33. package/gen-backend/prompts/{new-site-2-fields.md → initial-2-fields.md} +3 -3
  34. package/gen-backend/prompts/message-guard.md +1 -1
  35. package/gen-backend/prompts/update-site-schema-1-write-details.md +5 -5
  36. package/gen-backend/prompts/update-site-schema-2-execute.md +29 -29
  37. package/gen-front/dist/gen-front.css +1 -1
  38. package/gen-front/dist/gen-front.mjs +128 -1175
  39. package/package.json +30 -32
  40. package/gen-backend/dist/db/db.queries.js +0 -60
  41. package/gen-backend/prompts/test-message1.txt +0 -1
  42. package/gen-front/dist/gen-front.eot +0 -0
  43. package/gen-front/dist/gen-front.svg +0 -345
  44. package/gen-front/dist/gen-front.ttf +0 -0
  45. package/gen-front/dist/gen-front.woff +0 -0
  46. package/gen-front/dist/gen-front.woff2 +0 -0
  47. package/gen-front/dist/gen-front2.woff2 +0 -0
  48. package/gen-front/dist/gen-front3.woff2 +0 -0
@@ -8,15 +8,63 @@ create table PaMetadata (
8
8
  primary key (dbSchema, k)
9
9
  );
10
10
 
11
- insert into PaMetadata (dbSchema, k, val) values ('site-generator', 'dbSchemaVersion', '1');
11
+ insert into PaMetadata (dbSchema, k, val) values ('site-generator', 'dbSchemaVersion', '2');
12
12
 
13
- create table PaGenSession (
14
- id char(36) not null primary key,
13
+ create table PgSession (
14
+ id varchar(100) not null primary key,
15
15
  createdAt timestamp not null default current_timestamp,
16
+ expireAt timestamp not null,
17
+ language varchar(5) not null
18
+ );
19
+
20
+ create table PgStep (
21
+ stepNumber integer not null,
22
+ sessionId varchar(100) not null references PgSession(id),
23
+ startedAt timestamp not null default current_timestamp,
24
+ kind varchar(100) not null,
16
25
  status varchar(100) not null,
17
- guardCount integer not null default 0,
18
- promptCount integer not null default 0,
19
- nodeTypeCount integer not null default 0,
20
- contentCount integer not null default 0,
21
- errorMessage varchar(500)
22
- );
26
+ durationMs integer,
27
+ currentActivity varchar(100),
28
+ explanation text,
29
+ primary key (stepNumber, sessionId)
30
+ );
31
+
32
+ create table PgSchemaStep (
33
+ stepNumber integer not null,
34
+ sessionId varchar(100) not null,
35
+ siteSchema text not null,
36
+ l10n text not null,
37
+ localizedValues text not null,
38
+ nodeTypeCount integer not null,
39
+ inputTokenCount integer not null,
40
+ outputTokenCount integer not null,
41
+ promptTitle varchar(200),
42
+ primary key (stepNumber, sessionId),
43
+ foreign key (stepNumber, sessionId) references PgStep(stepNumber, sessionId)
44
+ );
45
+
46
+ create table PgGeneratedSiteStep (
47
+ siteId varchar(100) not null primary key,
48
+ stepNumber integer not null,
49
+ sessionId varchar(100) not null,
50
+ siteUrl varchar(200) not null,
51
+ loginEmail varchar(100) not null,
52
+ loginPassword varchar(100) not null,
53
+ localizedValues text not null,
54
+ contentEntryCount integer,
55
+ contentInputTokenCount integer,
56
+ contentOutputTokenCount integer,
57
+ contentErrors text,
58
+ unique (stepNumber, sessionId),
59
+ foreign key (stepNumber, sessionId) references PgStep(stepNumber, sessionId)
60
+ );
61
+
62
+ create table PgIssueEvent(
63
+ id integer not null primary key autoincrement,
64
+ sessionId varchar(100) not null references PgSession(id),
65
+ createdAt timestamp not null default current_timestamp,
66
+ eventType varchar(100) not null,
67
+ issueMessage text,
68
+ llmTaskName varchar(100),
69
+ stepNumber integer
70
+ );
@@ -1,15 +1,22 @@
1
1
  import { messageOf } from "@paroi/data-formatters-lib";
2
- import { updateSession } from "../db/db.queries.js";
2
+ import { fetchWorkSession, loadStep } from "../db/db-read.queries.js";
3
+ import { insertIssueEvent, updateSession } from "../db/db-write.queries.js";
3
4
  import { invokeMessageGuard } from "../generator/llm-queries/invoke-message-guard.js";
4
- import { invokeNewSiteAnalysis } from "../generator/llm-queries/invoke-new-site-analysis.js";
5
- import { invokeUpdateSiteSchema } from "../generator/llm-queries/invoke-update-site-schema.js";
5
+ import { startInitialAnalysis } from "../generator/llm-queries/invoke-new-site-analysis.js";
6
+ import { startUpdateSiteSchema } from "../generator/llm-queries/invoke-update-site-schema.js";
6
7
  import { generateSite } from "../generator/site-generator/site-generator.js";
7
- import { newSession, verifySessionToken } from "./generator-session.js";
8
+ import { newSessionCommand, renewSessionCommand, verifySessionToken } from "./generator-session.js";
8
9
  export async function executeCommand(commandCtx, input) {
9
10
  if (input.command === "newSession") {
10
11
  return {
11
12
  success: true,
12
- result: await newSession(commandCtx, input),
13
+ result: await newSessionCommand(commandCtx, input),
14
+ };
15
+ }
16
+ if (input.command === "renewSession") {
17
+ return {
18
+ success: true,
19
+ result: await renewSessionCommand(commandCtx, input),
13
20
  };
14
21
  }
15
22
  const { sessionId } = await verifySessionToken(commandCtx, input.sessionToken);
@@ -18,13 +25,32 @@ export async function executeCommand(commandCtx, input) {
18
25
  sessionId,
19
26
  };
20
27
  try {
28
+ if (input.command === "setLanguage") {
29
+ await updateSession(ctx, { language: input.language });
30
+ return {
31
+ success: true,
32
+ result: { updated: true },
33
+ };
34
+ }
35
+ if (input.command === "loadWorkSession") {
36
+ return {
37
+ success: true,
38
+ result: await fetchWorkSession(ctx),
39
+ };
40
+ }
41
+ if (input.command === "loadStep") {
42
+ return {
43
+ success: true,
44
+ result: await loadStep(ctx, input.stepNumber),
45
+ };
46
+ }
21
47
  if (input.command === "createSiteSchema") {
22
48
  const errorResponse = await invokeMessageGuard(ctx, input);
23
49
  if (errorResponse)
24
50
  return errorResponse;
25
51
  return {
26
52
  success: true,
27
- result: await invokeNewSiteAnalysis(ctx, input),
53
+ result: await startInitialAnalysis(ctx, input),
28
54
  };
29
55
  }
30
56
  if (input.command === "updateSiteSchema") {
@@ -33,7 +59,7 @@ export async function executeCommand(commandCtx, input) {
33
59
  return errorResponse;
34
60
  return {
35
61
  success: true,
36
- result: await invokeUpdateSiteSchema(ctx, input),
62
+ result: await startUpdateSiteSchema(ctx, input),
37
63
  };
38
64
  }
39
65
  if (input.command === "generateSite") {
@@ -50,10 +76,10 @@ export async function executeCommand(commandCtx, input) {
50
76
  }
51
77
  catch (error) {
52
78
  try {
53
- await updateSession(ctx, { status: "error", errorMessage: messageOf(error) });
79
+ await insertIssueEvent(ctx, { eventType: "unexpectedError", issueMessage: messageOf(error) });
54
80
  }
55
81
  catch {
56
- ctx.logger.error("Failed to update session error message", error);
82
+ ctx.logger.error("Failed to insert issue event", error);
57
83
  }
58
84
  throw error;
59
85
  }
@@ -1,16 +1,56 @@
1
1
  import { ApiError } from "@paroicms/public-server-lib";
2
- import { randomUUID } from "node:crypto";
3
- import { insertSession, readSession } from "../db/db.queries.js";
2
+ import { nanoid } from "nanoid";
3
+ import { getInvalidSessionError } from "../db/db-read.queries.js";
4
+ import { insertSession, updateSession } from "../db/db-write.queries.js";
4
5
  const { sign, verify } = (await import("jsonwebtoken")).default;
5
- export async function newSession(ctx, command) {
6
+ export async function newSessionCommand(ctx, command) {
7
+ const { service } = ctx;
8
+ const { recaptchaToken, language } = command;
9
+ const isValid = await service.validateRecaptchaResponse(recaptchaToken);
10
+ if (!isValid)
11
+ throw new ApiError("Invalid reCAPTCHA token", 400);
12
+ const date = new Date();
13
+ const compressedTs = `${date.toISOString().replace(/[-:]/g, "").split(".")[0]}Z`;
14
+ const sessionId = `${compressedTs}-${nanoid()}`;
15
+ const token = sign({ sessionId }, ctx.jwtSecret, { expiresIn: "48h" });
16
+ const expireAt = new Date(date.getTime() + 48 * 60 * 60 * 1000);
17
+ await insertSession(ctx, { sessionId, language, expireAt });
18
+ return { token };
19
+ }
20
+ /**
21
+ * Renews a session token and extends its expiration time
22
+ */
23
+ export async function renewSessionCommand(ctx, command) {
6
24
  const { service } = ctx;
7
25
  const { recaptchaToken } = command;
8
26
  const isValid = await service.validateRecaptchaResponse(recaptchaToken);
9
27
  if (!isValid)
10
28
  throw new ApiError("Invalid reCAPTCHA token", 400);
11
- const sessionId = randomUUID();
29
+ let payload;
30
+ try {
31
+ // When renewing, we verify the token but allow expired tokens
32
+ payload = verify(command.sessionToken, ctx.jwtSecret, { ignoreExpiration: true });
33
+ }
34
+ catch (error) {
35
+ if (error.name === "JsonWebTokenError")
36
+ throw new ApiError("Invalid session token", 401);
37
+ throw error;
38
+ }
39
+ if (!payload || !("sessionId" in payload) || typeof payload.sessionId !== "string") {
40
+ throw new ApiError("Invalid session token", 401);
41
+ }
42
+ const sessionId = payload.sessionId;
43
+ // Check if the session is valid (not deleted, not too many steps)
44
+ const errorMessage = await getInvalidSessionError(ctx, sessionId);
45
+ if (errorMessage && errorMessage !== "expired") {
46
+ throw new ApiError(`Invalid session "${sessionId}": ${errorMessage}`, 401);
47
+ }
48
+ // Generate new expiration time - 48 hours from now
49
+ const expireAt = new Date(Date.now() + 48 * 60 * 60 * 1000);
50
+ // Update the session's expiration time
51
+ await updateSession({ ...ctx, sessionId }, { expireAt });
52
+ // Generate a new token
12
53
  const token = sign({ sessionId }, ctx.jwtSecret, { expiresIn: "48h" });
13
- await insertSession(ctx, { sessionId });
14
54
  return { token };
15
55
  }
16
56
  /**
@@ -32,10 +72,9 @@ export async function verifySessionToken(ctx, token) {
32
72
  throw new ApiError("Invalid session token", 401);
33
73
  }
34
74
  const sessionId = payload.sessionId;
35
- const session = await readSession(ctx, sessionId);
36
- if (!session)
37
- throw new ApiError(`Missing session "${sessionId}"`, 500);
38
- if (session.promptCount >= 20)
39
- throw new ApiError("Session consumed", 401);
75
+ const errorMessage = await getInvalidSessionError(ctx, sessionId);
76
+ if (errorMessage) {
77
+ throw new ApiError(`Invalid session "${sessionId}": ${errorMessage}`, 401);
78
+ }
40
79
  return { sessionId };
41
80
  }
@@ -1,4 +1,4 @@
1
- import { boolVal, isObj, strVal, strValOrUndef } from "@paroi/data-formatters-lib";
1
+ import { boolVal, isObj, nbVal, strVal, strValOrUndef } from "@paroi/data-formatters-lib";
2
2
  import { ApiError } from "@paroicms/public-server-lib";
3
3
  export function formatGeneratorPluginConfiguration(pluginConf) {
4
4
  const anthropicApiKey = strValOrUndef(pluginConf.anthropicApiKey, { varName: "anthropicApiKey" });
@@ -17,9 +17,37 @@ export function formatGeneratorCommand(data) {
17
17
  if (command === "newSession") {
18
18
  return {
19
19
  command: "newSession",
20
+ language: strVal(data.language, { varName: "language" }),
20
21
  recaptchaToken: strVal(data.recaptchaToken, { varName: "recaptchaToken" }),
21
22
  };
22
23
  }
24
+ if (command === "renewSession") {
25
+ return {
26
+ command: "renewSession",
27
+ sessionToken: strVal(data.sessionToken, { varName: "sessionToken" }),
28
+ recaptchaToken: strVal(data.recaptchaToken, { varName: "recaptchaToken" }),
29
+ };
30
+ }
31
+ if (command === "setLanguage") {
32
+ return {
33
+ command: "setLanguage",
34
+ sessionToken: strVal(data.sessionToken, { varName: "sessionToken" }),
35
+ language: strVal(data.language, { varName: "language" }),
36
+ };
37
+ }
38
+ if (command === "loadWorkSession") {
39
+ return {
40
+ command: "loadWorkSession",
41
+ sessionToken: strVal(data.sessionToken, { varName: "sessionToken" }),
42
+ };
43
+ }
44
+ if (command === "loadStep") {
45
+ return {
46
+ command: "loadStep",
47
+ sessionToken: strVal(data.sessionToken, { varName: "sessionToken" }),
48
+ stepNumber: nbVal(data.stepNumber, { varName: "stepNumber" }),
49
+ };
50
+ }
23
51
  if (command === "createSiteSchema") {
24
52
  return {
25
53
  command: "createSiteSchema",
@@ -32,15 +60,15 @@ export function formatGeneratorCommand(data) {
32
60
  command: "updateSiteSchema",
33
61
  sessionToken: strVal(data.sessionToken, { varName: "sessionToken" }),
34
62
  prompt: formatPrompt(data.prompt),
35
- generatedSchema: data.generatedSchema,
63
+ fromStepNumber: nbVal(data.fromStepNumber),
36
64
  };
37
65
  }
38
66
  if (command === "generateSite") {
39
67
  return {
40
68
  command: "generateSite",
41
69
  sessionToken: strVal(data.sessionToken, { varName: "sessionToken" }),
42
- generatedSchema: data.generatedSchema,
43
- withFakeContent: boolVal(data.withFakeContent, { varName: "withFakeContent" }),
70
+ fromStepNumber: nbVal(data.fromStepNumber),
71
+ withSampleData: boolVal(data.withSampleData, { varName: "withSampleData" }),
44
72
  };
45
73
  }
46
74
  throw new ApiError(`Invalid command: ${command}`, 400);
@@ -4,11 +4,12 @@ import { projectDir } from "../context.js";
4
4
  import { currentDbSchemaVersion, dbSchemaName, migrateSiteGeneratorDb } from "./ddl-migration.js";
5
5
  export async function createOrOpenSiteGeneratorConnection({ sqliteFile, canCreate, logger, }) {
6
6
  const { logNextQuery, knexLogger } = createSqlLogger({ logger, dbSchemaName });
7
+ const ddlFile = join(projectDir, "ddl", "site-generator.ddl.sql");
7
8
  const { cn } = await createOrOpenSqliteConnection({
8
9
  canCreate,
9
10
  dbSchemaName,
10
11
  sqliteFile,
11
- ddlFile: join(projectDir, "ddl", "site-generator.ddl.sql"),
12
+ ddlFile,
12
13
  migrateDb,
13
14
  knexLogger,
14
15
  logger,
@@ -24,6 +25,7 @@ export async function createOrOpenSiteGeneratorConnection({ sqliteFile, canCreat
24
25
  await migrateSiteGeneratorDb(cn, {
25
26
  fromVersion: dbVersion,
26
27
  logger,
28
+ ddlFile,
27
29
  });
28
30
  return {
29
31
  migrated: true,
@@ -0,0 +1,142 @@
1
+ import { dateVal, nbVal, strVal, strValOrUndef } from "@paroi/data-formatters-lib";
2
+ import { ensureType, formatActivityCode, formatStepKind, formatStepStatus } from "./formatters.js";
3
+ export async function getInvalidSessionError(ctx, sessionId) {
4
+ const { cn } = ctx;
5
+ const row = await cn("PgSession as s")
6
+ .leftJoin("PgSchemaStep as u", "u.sessionId", "s.id")
7
+ .select("s.id", "s.expireAt", cn.raw("count(u.sessionId) as successStepCount"))
8
+ .where({ "s.id": sessionId })
9
+ .groupBy("s.id")
10
+ .first();
11
+ if (!row)
12
+ return "not found";
13
+ const successStepCount = nbVal(row.successStepCount);
14
+ if (successStepCount >= 20)
15
+ return "too many steps";
16
+ const expireAt = dateVal(row.expireAt).getTime();
17
+ if (expireAt < Date.now())
18
+ return "expired";
19
+ }
20
+ export async function readStepSchema(ctx, stepNumber) {
21
+ const { cn, sessionId } = ctx;
22
+ const row = await cn("PgSchemaStep")
23
+ .select("siteSchema", "l10n", "localizedValues")
24
+ .where({ sessionId, stepNumber })
25
+ .first();
26
+ if (!row)
27
+ throw new Error(`[${sessionId}] Step "${stepNumber}" not found`);
28
+ return {
29
+ siteSchema: JSON.parse(strVal(row.siteSchema)),
30
+ l10n: JSON.parse(strVal(row.l10n)),
31
+ localizedValues: JSON.parse(strVal(row.localizedValues)),
32
+ };
33
+ }
34
+ export async function fetchWorkSession(ctx) {
35
+ const { cn, sessionId } = ctx;
36
+ const row = await cn("PgSession")
37
+ .select("id", "createdAt", "expireAt", "language")
38
+ .where({ id: sessionId })
39
+ .first();
40
+ if (!row) {
41
+ throw new Error(`Session not found: ${sessionId}`);
42
+ }
43
+ const steps = await loadSessionSteps(ctx);
44
+ return {
45
+ id: strVal(row.id),
46
+ createdAt: dateVal(row.createdAt).toISOString(),
47
+ expireAt: dateVal(row.expireAt).toISOString(),
48
+ language: strVal(row.language),
49
+ steps,
50
+ };
51
+ }
52
+ async function loadSessionSteps(ctx) {
53
+ const { cn, sessionId } = ctx;
54
+ const rows = await buildStepsQuery(cn)
55
+ .where({ "s.sessionId": sessionId })
56
+ .orderBy("s.stepNumber");
57
+ return rows.map((row) => mapRowToStep(row, sessionId));
58
+ }
59
+ export async function loadStep(ctx, stepNumber) {
60
+ const { cn, sessionId } = ctx;
61
+ const row = await buildStepsQuery(cn)
62
+ .where({ "s.sessionId": sessionId, "s.stepNumber": stepNumber })
63
+ .first();
64
+ if (!row) {
65
+ throw new Error(`[Session "${sessionId}"] Step "${stepNumber}" not found`);
66
+ }
67
+ return mapRowToStep(row, sessionId);
68
+ }
69
+ function buildStepsQuery(cn) {
70
+ return cn("PgStep as s")
71
+ .select("s.stepNumber", "s.kind", "s.status", "s.currentActivity", "s.explanation", "c.promptTitle", "c.siteSchema", "c.l10n", "c.localizedValues as schemaLocalizedValues", "i.siteId", "i.localizedValues as siteLocalizedValues", "i.siteUrl", "i.loginEmail", "i.loginPassword")
72
+ .leftJoin("PgSchemaStep as c", {
73
+ "c.stepNumber": "s.stepNumber",
74
+ "c.sessionId": "s.sessionId",
75
+ })
76
+ .leftJoin("PgGeneratedSiteStep as i", {
77
+ "i.stepNumber": "s.stepNumber",
78
+ "i.sessionId": "s.sessionId",
79
+ });
80
+ }
81
+ function mapRowToStep(row, sessionId) {
82
+ const baseStep = {
83
+ stepNumber: nbVal(row.stepNumber),
84
+ explanation: strValOrUndef(row.explanation),
85
+ };
86
+ const status = formatStepStatus(row.status);
87
+ const kind = formatStepKind(row.kind);
88
+ if (status === "pending") {
89
+ return ensureType({
90
+ ...baseStep,
91
+ status,
92
+ kind,
93
+ currentActivity: formatActivityCode(row.currentActivity),
94
+ });
95
+ }
96
+ if (status === "completed") {
97
+ if (kind === "initialSchema" || kind === "updateSchema") {
98
+ if (!row.siteSchema) {
99
+ throw new Error(`[Session "${sessionId}"] PgSchemaStep row is missing for completed step "${row.stepNumber}"`);
100
+ }
101
+ return ensureType({
102
+ ...baseStep,
103
+ status,
104
+ kind,
105
+ promptTitle: strValOrUndef(row.promptTitle),
106
+ siteSchema: JSON.parse(strVal(row.siteSchema)),
107
+ l10n: JSON.parse(strVal(row.l10n)),
108
+ localizedValues: JSON.parse(strVal(row.schemaLocalizedValues)),
109
+ });
110
+ }
111
+ if (kind === "generateSite") {
112
+ if (!row.siteId) {
113
+ throw new Error(`[Session "${sessionId}"] PgGeneratedSiteStep row is missing for completed step "${row.stepNumber}"`);
114
+ }
115
+ const siteUrl = strVal(row.siteUrl);
116
+ return ensureType({
117
+ ...baseStep,
118
+ status,
119
+ kind,
120
+ siteId: strVal(row.siteId),
121
+ siteUrl,
122
+ backOfficeUrl: `${siteUrl}/adm`,
123
+ loginEmail: strVal(row.loginEmail),
124
+ loginPassword: strVal(row.loginPassword),
125
+ localizedValues: JSON.parse(strVal(row.siteLocalizedValues)),
126
+ });
127
+ }
128
+ throw new Error(`[Session "${sessionId}"] invalid step kind "${kind}"`);
129
+ }
130
+ if (status === "failed") {
131
+ return ensureType({
132
+ ...baseStep,
133
+ status,
134
+ kind,
135
+ });
136
+ }
137
+ return ensureType({
138
+ ...baseStep,
139
+ status,
140
+ kind,
141
+ });
142
+ }
@@ -0,0 +1,144 @@
1
+ import { strVal } from "@paroi/data-formatters-lib";
2
+ import { sqliteDateTime } from "@paroicms/internal-server-lib";
3
+ export async function insertSession(ctx, values) {
4
+ const { cn } = ctx;
5
+ await cn("PgSession").insert({
6
+ id: values.sessionId,
7
+ language: values.language,
8
+ expireAt: sqliteDateTime(values.expireAt),
9
+ });
10
+ }
11
+ export async function updateSession(ctx, values) {
12
+ const { cn, sessionId } = ctx;
13
+ if (Object.keys(values).length === 0)
14
+ return;
15
+ await cn("PgSession")
16
+ .where({ id: sessionId })
17
+ .update({
18
+ language: values.language,
19
+ expireAt: values.expireAt ? sqliteDateTime(values.expireAt) : undefined,
20
+ });
21
+ }
22
+ export async function insertStep(ctx, values) {
23
+ const { cn, sessionId } = ctx;
24
+ // Find the highest step number for this session
25
+ const result = await cn("PgStep").where({ sessionId }).max("stepNumber as maxStep").first();
26
+ const stepNumber = (result?.maxStep || 0) + 1;
27
+ const startedAt = new Date();
28
+ await cn("PgStep").insert({
29
+ stepNumber,
30
+ sessionId,
31
+ startedAt: sqliteDateTime(startedAt),
32
+ kind: values.kind,
33
+ status: values.status,
34
+ currentActivity: values.currentActivity,
35
+ explanation: values.explanation,
36
+ });
37
+ return { stepNumber, startedAt };
38
+ }
39
+ export async function updateStep(ctx, stepHandle, values) {
40
+ const { cn, sessionId } = ctx;
41
+ const { stepNumber, startedAt } = stepHandle;
42
+ const updateValues = {};
43
+ if (values.status) {
44
+ updateValues.status = values.status;
45
+ if (values.status !== "pending") {
46
+ updateValues.durationMs = Date.now() - startedAt.getTime();
47
+ }
48
+ }
49
+ if (values.currentActivity !== undefined) {
50
+ updateValues.currentActivity = values.currentActivity;
51
+ }
52
+ if (values.explanation !== undefined)
53
+ updateValues.explanation = values.explanation;
54
+ if (Object.keys(updateValues).length === 0)
55
+ return;
56
+ await cn("PgStep").where({ sessionId, stepNumber }).update(updateValues);
57
+ }
58
+ export async function saveCompletedSchemaStep(ctx, stepHandle, values) {
59
+ const { cn, sessionId } = ctx;
60
+ const { stepNumber, startedAt } = stepHandle;
61
+ // Transaction to ensure both operations succeed or fail together
62
+ await cn.transaction(async (tx) => {
63
+ // Update the step status
64
+ await tx("PgStep")
65
+ .where({ sessionId, stepNumber })
66
+ .update({
67
+ status: "completed",
68
+ durationMs: Date.now() - startedAt.getTime(),
69
+ currentActivity: null,
70
+ explanation: values.explanation,
71
+ });
72
+ // Insert the completed step details
73
+ await tx("PgSchemaStep").insert({
74
+ stepNumber,
75
+ sessionId,
76
+ promptTitle: values.promptTitle,
77
+ siteSchema: JSON.stringify(values.siteSchema),
78
+ l10n: JSON.stringify(values.l10n),
79
+ localizedValues: JSON.stringify(values.localizedValues),
80
+ nodeTypeCount: values.siteSchema.nodeTypes?.length ?? 0,
81
+ inputTokenCount: values.inputTokenCount,
82
+ outputTokenCount: values.outputTokenCount,
83
+ });
84
+ });
85
+ }
86
+ export async function saveGeneratedSiteStep(ctx, stepHandle, values) {
87
+ const { cn, sessionId } = ctx;
88
+ const { stepNumber, startedAt } = stepHandle;
89
+ const completed = values.status === "completed";
90
+ await cn.transaction(async (tx) => {
91
+ await tx("PgStep")
92
+ .where({ sessionId, stepNumber })
93
+ .update({
94
+ status: values.status,
95
+ durationMs: completed ? Date.now() - startedAt.getTime() : null,
96
+ currentActivity: completed ? null : undefined,
97
+ });
98
+ await tx("PgGeneratedSiteStep").insert({
99
+ siteId: values.siteId,
100
+ stepNumber,
101
+ sessionId,
102
+ siteUrl: values.siteUrl,
103
+ loginEmail: values.loginEmail,
104
+ loginPassword: values.loginPassword,
105
+ localizedValues: JSON.stringify(values.localizedValues),
106
+ contentEntryCount: completed ? 0 : undefined,
107
+ contentInputTokenCount: completed ? 0 : undefined,
108
+ contentOutputTokenCount: completed ? 0 : undefined,
109
+ });
110
+ });
111
+ }
112
+ export async function updateGeneratedSiteStepSetAsCompleted(ctx, stepHandle, values) {
113
+ const { cn, sessionId } = ctx;
114
+ const { stepNumber, startedAt } = stepHandle;
115
+ await cn.transaction(async (tx) => {
116
+ await tx("PgStep")
117
+ .where({ sessionId, stepNumber })
118
+ .update({
119
+ status: values.status,
120
+ durationMs: Date.now() - startedAt.getTime(),
121
+ currentActivity: null,
122
+ });
123
+ await tx("PgGeneratedSiteStep").where({ sessionId, stepNumber }).update({
124
+ contentEntryCount: values.contentEntryCount,
125
+ contentInputTokenCount: values.contentInputTokenCount,
126
+ contentOutputTokenCount: values.contentOutputTokenCount,
127
+ contentErrors: values.contentErrors,
128
+ });
129
+ });
130
+ }
131
+ export async function insertIssueEvent(ctx, values) {
132
+ const { cn, sessionId } = ctx;
133
+ const [inserted] = await cn("PgIssueEvent")
134
+ .insert({
135
+ sessionId,
136
+ eventType: values.eventType,
137
+ issueMessage: values.issueMessage,
138
+ llmTaskName: values.llmTaskName,
139
+ stepNumber: values.stepNumber,
140
+ siteId: values.siteId,
141
+ })
142
+ .returning("id");
143
+ return { id: strVal(inserted.id) };
144
+ }
@@ -1,12 +1,14 @@
1
+ import { executeDdl } from "@paroicms/internal-server-lib";
1
2
  export const dbSchemaName = "site-generator";
2
- export const currentDbSchemaVersion = 1;
3
- export async function migrateSiteGeneratorDb(_cn, { fromVersion, logger }) {
3
+ export const currentDbSchemaVersion = 2;
4
+ export async function migrateSiteGeneratorDb(cn, { fromVersion, logger, ddlFile }) {
4
5
  const toVersion = currentDbSchemaVersion;
5
- const currentVersion = fromVersion;
6
+ let currentVersion = fromVersion;
6
7
  if (currentVersion === 1) {
7
- // await cn.raw("...");
8
- // await setMetadataDbSchemaVersion(cn, { dbSchemaName, value: 2 });
9
- // currentVersion = 2;
8
+ await cn.raw("drop table PaGenSession");
9
+ await cn.raw("drop table PaMetadata");
10
+ await executeDdl(cn, ddlFile);
11
+ currentVersion = currentDbSchemaVersion;
10
12
  }
11
13
  if (currentVersion !== toVersion) {
12
14
  throw new Error(`version of ${dbSchemaName} database should be '${toVersion}', but is '${currentVersion}'`);
@@ -0,0 +1,46 @@
1
+ import { strVal, strValOrUndef } from "@paroi/data-formatters-lib";
2
+ export function ensureType(value) {
3
+ return value;
4
+ }
5
+ export function formatStepKind(value) {
6
+ const v = strVal(value);
7
+ if (v === "initialSchema" || v === "updateSchema" || v === "generateSite") {
8
+ return v;
9
+ }
10
+ throw new Error(`Invalid step kind "${v}"`);
11
+ }
12
+ export function formatSchemaStepKind(value) {
13
+ const v = strVal(value);
14
+ if (v === "initialSchema" || v === "updateSchema") {
15
+ return v;
16
+ }
17
+ throw new Error(`Invalid schema step kind "${v}"`);
18
+ }
19
+ export function formatGeneratedSiteStepKind(value) {
20
+ const v = strVal(value);
21
+ if (v === "generateSite") {
22
+ return v;
23
+ }
24
+ throw new Error(`Invalid generated site step kind "${v}"`);
25
+ }
26
+ export function formatStepStatus(value) {
27
+ const v = strVal(value);
28
+ if (v === "completed" || v === "pending" || v === "failed" || v === "noEffect") {
29
+ return v;
30
+ }
31
+ throw new Error(`Invalid step status "${v}"`);
32
+ }
33
+ export function formatActivityCode(value) {
34
+ const v = strValOrUndef(value);
35
+ if (v === undefined)
36
+ return;
37
+ if (v === "initial1" ||
38
+ v === "initial2" ||
39
+ v === "updating1" ||
40
+ v === "updating2" ||
41
+ v === "generatingSite" ||
42
+ v === "generatingContent") {
43
+ return v;
44
+ }
45
+ throw new Error(`Invalid activity code "${v}"`);
46
+ }