@agent-native/core 0.164.11 → 0.164.12
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/corpus/README.md +1 -1
- package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +84 -0
- package/corpus/templates/calendar/AGENTS.md +5 -0
- package/corpus/templates/calendar/actions/delete-event.ts +1 -1
- package/corpus/templates/calendar/actions/delete-events.ts +508 -0
- package/corpus/templates/calendar/actions/event-action-helpers.ts +1 -1
- package/corpus/templates/calendar/actions/list-events.ts +53 -0
- package/corpus/templates/calendar/changelog/2026-08-18-the-agent-can-now-remove-many-meetings-at-once-such-as-every.md +6 -0
- package/corpus/templates/calendar/server/lib/event-weekday.ts +112 -0
- package/corpus/templates/factory/AGENTS.md +2 -1
- package/corpus/templates/factory/actions/get-factory-graph-version.ts +69 -0
- package/corpus/templates/factory/actions/list-factory-graph-versions.ts +82 -0
- package/corpus/templates/factory/actions/restore-factory-graph-version.ts +163 -0
- package/corpus/templates/factory/actions/save-factory-graph.ts +93 -60
- package/corpus/templates/factory/app/components/factory/FactoryHistoryView.tsx +709 -0
- package/corpus/templates/factory/app/components/factory/FactoryInspector.tsx +62 -2
- package/corpus/templates/factory/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/factory/app/i18n/en-US.ts +43 -0
- package/corpus/templates/factory/app/routes/factory.tsx +115 -15
- package/corpus/templates/factory/server/plugins/agent-chat.ts +3 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/docs/content/actions-access-control.mdx +136 -2
- package/docs/content/actions-agent-tools.mdx +101 -0
- package/docs/content/actions-defining.mdx +2 -2
- package/docs/content/audit-log.mdx +1 -1
- package/docs/content/authentication.mdx +1 -1
- package/docs/content/cloudflare.mdx +43 -0
- package/docs/content/creating-templates.mdx +1 -1
- package/docs/content/deploy-an-app.mdx +109 -0
- package/docs/content/deployment-environment-variables.mdx +101 -0
- package/docs/content/deployment.mdx +125 -792
- package/docs/content/docs-components.mdx +2 -2
- package/docs/content/environment-variables.mdx +41 -41
- package/docs/content/file-uploads.mdx +74 -40
- package/docs/content/harness-agents.mdx +2 -2
- package/docs/content/internationalization.mdx +61 -65
- package/docs/content/key-concepts.mdx +3 -3
- package/docs/content/multi-app-workspace.mdx +2 -2
- package/docs/content/netlify.mdx +47 -0
- package/docs/content/node-docker.mdx +138 -0
- package/docs/content/organizations-teams-permissions.mdx +5 -5
- package/docs/content/other-platforms.mdx +27 -0
- package/docs/content/ssr-caching.mdx +45 -0
- package/docs/content/template-assets-developers.mdx +2 -2
- package/docs/content/template-chat-developers.mdx +2 -2
- package/docs/content/template-content-developers.mdx +2 -2
- package/docs/content/{local-file-mode.mdx → template-content-local-files.mdx} +2 -1
- package/docs/content/template-content-sync.mdx +2 -2
- package/docs/content/toolkit-agent-ux.mdx +1 -1
- package/docs/content/toolkit-resources.mdx +2 -2
- package/docs/content/updating-ui-in-production.mdx +35 -0
- package/docs/content/vercel.mdx +38 -0
- package/docs/content/what-is-agent-native.mdx +1 -1
- package/docs/content/workspace-deployment.mdx +350 -0
- package/package.json +1 -1
- package/docs/content/agents.mdx +0 -9
- package/docs/content/database.mdx +0 -317
- package/docs/content/human-approval.mdx +0 -156
|
@@ -554,6 +554,59 @@ async function listLocalBookingEvents(
|
|
|
554
554
|
});
|
|
555
555
|
}
|
|
556
556
|
|
|
557
|
+
/**
|
|
558
|
+
* Which of these Google event ids are backing a still-active booking.
|
|
559
|
+
*
|
|
560
|
+
* Deleting such an event without cancelling its booking leaves the row
|
|
561
|
+
* confirmed, and `shouldShowLocalBookingEvent` then republishes the booking as a
|
|
562
|
+
* local calendar event — so the "deleted" meeting reappears. Scoped through the
|
|
563
|
+
* same booking-link access filter as the calendar read.
|
|
564
|
+
*
|
|
565
|
+
* Returns `calendarAccountId` so callers can tell which account a booking'"'"'s event
|
|
566
|
+
* lives on. It is nullable — the column was added after bookings already
|
|
567
|
+
* existed — so a null must be read as "unknown account", never as "other
|
|
568
|
+
* account".
|
|
569
|
+
*/
|
|
570
|
+
export async function findBookedGoogleEvents(
|
|
571
|
+
googleEventIds: readonly string[],
|
|
572
|
+
): Promise<Array<{ googleEventId: string; calendarAccountId: string | null }>> {
|
|
573
|
+
const ids = Array.from(new Set(googleEventIds.filter(Boolean)));
|
|
574
|
+
if (ids.length === 0) return [];
|
|
575
|
+
|
|
576
|
+
const db = getDb();
|
|
577
|
+
const links = await db
|
|
578
|
+
.select({ slug: schema.bookingLinks.slug })
|
|
579
|
+
.from(schema.bookingLinks)
|
|
580
|
+
.where(accessFilter(schema.bookingLinks, schema.bookingLinkShares));
|
|
581
|
+
const slugs = links.map((link) => link.slug);
|
|
582
|
+
if (slugs.length === 0) return [];
|
|
583
|
+
|
|
584
|
+
const rows = await db
|
|
585
|
+
.select({
|
|
586
|
+
googleEventId: schema.bookings.googleEventId,
|
|
587
|
+
calendarAccountId: schema.bookings.calendarAccountId,
|
|
588
|
+
})
|
|
589
|
+
.from(schema.bookings)
|
|
590
|
+
.where(
|
|
591
|
+
and(
|
|
592
|
+
inArray(schema.bookings.slug, slugs),
|
|
593
|
+
ne(schema.bookings.status, "cancelled"),
|
|
594
|
+
inArray(schema.bookings.googleEventId, ids),
|
|
595
|
+
),
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
return rows.flatMap((row) =>
|
|
599
|
+
row.googleEventId
|
|
600
|
+
? [
|
|
601
|
+
{
|
|
602
|
+
googleEventId: row.googleEventId,
|
|
603
|
+
calendarAccountId: row.calendarAccountId ?? null,
|
|
604
|
+
},
|
|
605
|
+
]
|
|
606
|
+
: [],
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
557
610
|
function shouldShowLocalBookingEvent({
|
|
558
611
|
event,
|
|
559
612
|
googleEventIds,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
2
|
+
|
|
3
|
+
export const WEEKDAY_NAMES = [
|
|
4
|
+
"sunday",
|
|
5
|
+
"monday",
|
|
6
|
+
"tuesday",
|
|
7
|
+
"wednesday",
|
|
8
|
+
"thursday",
|
|
9
|
+
"friday",
|
|
10
|
+
"saturday",
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
export type WeekdayName = (typeof WEEKDAY_NAMES)[number];
|
|
14
|
+
|
|
15
|
+
const WEEKDAY_ALIASES = new Map<string, WeekdayName>([
|
|
16
|
+
...WEEKDAY_NAMES.map((day) => [day, day] as const),
|
|
17
|
+
...WEEKDAY_NAMES.map((day) => [day.slice(0, 3), day] as const),
|
|
18
|
+
["tues", "tuesday"],
|
|
19
|
+
["thur", "thursday"],
|
|
20
|
+
["thurs", "thursday"],
|
|
21
|
+
["weekend", "saturday"],
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Accepts the shapes a natural-language request arrives in — "Saturday",
|
|
26
|
+
* "sat", "SUN", or an array/comma string mixing them — and rejects anything
|
|
27
|
+
* else loudly. A silently dropped day name would delete a different set of
|
|
28
|
+
* events than the user asked about.
|
|
29
|
+
*/
|
|
30
|
+
export function normalizeWeekdays(
|
|
31
|
+
input: string | readonly string[] | undefined,
|
|
32
|
+
): WeekdayName[] {
|
|
33
|
+
if (input === undefined) return [];
|
|
34
|
+
const raw = (Array.isArray(input) ? input : String(input).split(","))
|
|
35
|
+
.map((value) => value.trim().toLowerCase())
|
|
36
|
+
.filter(Boolean);
|
|
37
|
+
const resolved = new Set<WeekdayName>();
|
|
38
|
+
for (const value of raw) {
|
|
39
|
+
if (value === "weekend" || value === "weekends") {
|
|
40
|
+
resolved.add("saturday");
|
|
41
|
+
resolved.add("sunday");
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (value === "weekday" || value === "weekdays") {
|
|
45
|
+
for (const day of WEEKDAY_NAMES) {
|
|
46
|
+
if (day !== "saturday" && day !== "sunday") resolved.add(day);
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const match = WEEKDAY_ALIASES.get(value.replace(/s$/, ""));
|
|
51
|
+
if (!match) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Unrecognized day of week: ${value}. Use full or 3-letter English day names.`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
resolved.add(match);
|
|
57
|
+
}
|
|
58
|
+
return WEEKDAY_NAMES.filter((day) => resolved.has(day));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The weekday the user sees for an event, which is the weekday in the calendar
|
|
63
|
+
* view's timezone — not UTC. A Sunday 5pm America/Los_Angeles meeting is Monday
|
|
64
|
+
* in UTC, so filtering on the raw ISO string silently misses it and instead
|
|
65
|
+
* deletes a Monday meeting the user never mentioned.
|
|
66
|
+
*
|
|
67
|
+
* All-day starts carry no instant, so their `YYYY-MM-DD` date IS the local day
|
|
68
|
+
* and must not be re-projected through a timezone.
|
|
69
|
+
*/
|
|
70
|
+
export function eventWeekday(start: string, timezone: string): WeekdayName {
|
|
71
|
+
if (DATE_ONLY_RE.test(start)) {
|
|
72
|
+
const [year, month, day] = start.split("-").map(Number);
|
|
73
|
+
return WEEKDAY_NAMES[new Date(Date.UTC(year, month - 1, day)).getUTCDay()];
|
|
74
|
+
}
|
|
75
|
+
const parsed = new Date(start);
|
|
76
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
77
|
+
throw new Error(`Invalid event start: ${start}`);
|
|
78
|
+
}
|
|
79
|
+
const label = new Intl.DateTimeFormat("en-US", {
|
|
80
|
+
timeZone: timezone,
|
|
81
|
+
weekday: "long",
|
|
82
|
+
})
|
|
83
|
+
.format(parsed)
|
|
84
|
+
.toLowerCase();
|
|
85
|
+
const match = WEEKDAY_NAMES.find((day) => day === label);
|
|
86
|
+
if (!match) throw new Error(`Unable to resolve weekday for ${start}`);
|
|
87
|
+
return match;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function matchesWeekdays(
|
|
91
|
+
start: string,
|
|
92
|
+
timezone: string,
|
|
93
|
+
weekdays: readonly WeekdayName[],
|
|
94
|
+
): boolean {
|
|
95
|
+
if (weekdays.length === 0) return true;
|
|
96
|
+
return weekdays.includes(eventWeekday(start, timezone));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validate a caller-supplied IANA timezone instead of falling back to UTC.
|
|
101
|
+
* `normalizeTimezone` silently returns UTC for an unparseable zone, which on a
|
|
102
|
+
* destructive weekday filter would quietly reclassify every event and delete a
|
|
103
|
+
* different set of days than the caller named.
|
|
104
|
+
*/
|
|
105
|
+
export function requireValidTimezone(timezone: string): string {
|
|
106
|
+
try {
|
|
107
|
+
new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
|
|
108
|
+
} catch {
|
|
109
|
+
throw new Error(`Invalid IANA timezone: ${timezone}`);
|
|
110
|
+
}
|
|
111
|
+
return timezone;
|
|
112
|
+
}
|
|
@@ -71,7 +71,8 @@ decisions, feedback, agent runs, and provider audit records.
|
|
|
71
71
|
| `suggest-factory-rules` | Mine feedback into proposals. |
|
|
72
72
|
| `reconcile-triage-run` | Persist callback/provider reconciliation. |
|
|
73
73
|
| `list-factories` / `get-factory-graph` | Inspect definitions, versions, and metrics. |
|
|
74
|
-
| `save-factory-graph` | Create
|
|
74
|
+
| `save-factory-graph` | Create/version a graph with inspected `expectedGraphVersion`; never starts provider work. |
|
|
75
|
+
| graph history actions | Factory graph version history. |
|
|
75
76
|
| `list-factory-comments` / `add-factory-comment` | Read or attach comments to a canvas, node, or edge. |
|
|
76
77
|
| `provider-api-catalog` / `provider-api-docs` / `provider-api-request` | Use connected provider APIs with shared credentials; never request raw keys. |
|
|
77
78
|
| `list-workspace-apps` / `update-workspace-app-metadata` | Inventory and edit mounted apps. |
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core/action";
|
|
2
|
+
import { and, eq } from "drizzle-orm";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import { getDb } from "../server/db/index.js";
|
|
6
|
+
import { factoryGraphVersions } from "../server/db/schema.js";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_FACTORY_ID,
|
|
9
|
+
parseFactoryGraph,
|
|
10
|
+
readFactoryDefinition,
|
|
11
|
+
} from "../server/factory-graph/store.js";
|
|
12
|
+
import {
|
|
13
|
+
requireWorkspaceMember,
|
|
14
|
+
workspaceMemberIdentityFromContext,
|
|
15
|
+
} from "../server/lib/require-workspace-member.js";
|
|
16
|
+
|
|
17
|
+
export default defineAction({
|
|
18
|
+
description:
|
|
19
|
+
"Read one validated Factory visual graph version for history preview. The snapshot is scoped to the active workspace and does not change the current Factory.",
|
|
20
|
+
schema: z.object({
|
|
21
|
+
factoryId: z
|
|
22
|
+
.string()
|
|
23
|
+
.trim()
|
|
24
|
+
.min(1)
|
|
25
|
+
.max(120)
|
|
26
|
+
.regex(/^[a-z0-9][a-z0-9-]*$/)
|
|
27
|
+
.default(DEFAULT_FACTORY_ID),
|
|
28
|
+
versionId: z.string().trim().min(1).max(240),
|
|
29
|
+
}),
|
|
30
|
+
http: { method: "GET" },
|
|
31
|
+
readOnly: true,
|
|
32
|
+
parallelSafe: true,
|
|
33
|
+
run: async ({ factoryId, versionId }, context) => {
|
|
34
|
+
const { orgId } = await requireWorkspaceMember(
|
|
35
|
+
workspaceMemberIdentityFromContext(context),
|
|
36
|
+
);
|
|
37
|
+
const currentDefinition = await readFactoryDefinition(orgId, factoryId);
|
|
38
|
+
const currentVersion = currentDefinition?.graphVersion ?? null;
|
|
39
|
+
const row = (
|
|
40
|
+
await getDb()
|
|
41
|
+
.select()
|
|
42
|
+
.from(factoryGraphVersions)
|
|
43
|
+
.where(
|
|
44
|
+
and(
|
|
45
|
+
eq(factoryGraphVersions.id, versionId),
|
|
46
|
+
eq(factoryGraphVersions.factoryId, factoryId),
|
|
47
|
+
eq(factoryGraphVersions.orgId, orgId),
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
.limit(1)
|
|
51
|
+
)[0];
|
|
52
|
+
|
|
53
|
+
if (!row) {
|
|
54
|
+
throw new Error("Factory graph version not found.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
id: row.id,
|
|
59
|
+
factoryId: row.factoryId,
|
|
60
|
+
version: row.version,
|
|
61
|
+
graph: parseFactoryGraph(row.graphJson),
|
|
62
|
+
source: row.source,
|
|
63
|
+
changeSummary: row.changeSummary,
|
|
64
|
+
createdAt: row.createdAt,
|
|
65
|
+
createdBy: row.createdBy,
|
|
66
|
+
isCurrent: currentVersion === row.version,
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core/action";
|
|
2
|
+
import { and, desc, eq, lt } from "drizzle-orm";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import { getDb } from "../server/db/index.js";
|
|
6
|
+
import { factoryGraphVersions } from "../server/db/schema.js";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_FACTORY_ID,
|
|
9
|
+
readFactoryDefinition,
|
|
10
|
+
} from "../server/factory-graph/store.js";
|
|
11
|
+
import {
|
|
12
|
+
requireWorkspaceMember,
|
|
13
|
+
workspaceMemberIdentityFromContext,
|
|
14
|
+
} from "../server/lib/require-workspace-member.js";
|
|
15
|
+
|
|
16
|
+
export default defineAction({
|
|
17
|
+
description:
|
|
18
|
+
"List bounded saved visual graph version metadata for a Factory, newest first. Use get-factory-graph-version to read one validated snapshot for preview or restore.",
|
|
19
|
+
schema: z.object({
|
|
20
|
+
factoryId: z
|
|
21
|
+
.string()
|
|
22
|
+
.trim()
|
|
23
|
+
.min(1)
|
|
24
|
+
.max(120)
|
|
25
|
+
.regex(/^[a-z0-9][a-z0-9-]*$/)
|
|
26
|
+
.default(DEFAULT_FACTORY_ID),
|
|
27
|
+
limit: z.coerce.number().int().min(1).max(50).default(25),
|
|
28
|
+
beforeVersion: z.coerce.number().int().positive().optional(),
|
|
29
|
+
}),
|
|
30
|
+
http: { method: "GET" },
|
|
31
|
+
readOnly: true,
|
|
32
|
+
parallelSafe: true,
|
|
33
|
+
run: async ({ factoryId, limit, beforeVersion }, context) => {
|
|
34
|
+
const { orgId } = await requireWorkspaceMember(
|
|
35
|
+
workspaceMemberIdentityFromContext(context),
|
|
36
|
+
);
|
|
37
|
+
const currentDefinition = await readFactoryDefinition(orgId, factoryId);
|
|
38
|
+
const currentVersion = currentDefinition?.graphVersion ?? null;
|
|
39
|
+
const rows = await getDb()
|
|
40
|
+
.select({
|
|
41
|
+
id: factoryGraphVersions.id,
|
|
42
|
+
factoryId: factoryGraphVersions.factoryId,
|
|
43
|
+
version: factoryGraphVersions.version,
|
|
44
|
+
source: factoryGraphVersions.source,
|
|
45
|
+
changeSummary: factoryGraphVersions.changeSummary,
|
|
46
|
+
createdAt: factoryGraphVersions.createdAt,
|
|
47
|
+
createdBy: factoryGraphVersions.createdBy,
|
|
48
|
+
})
|
|
49
|
+
.from(factoryGraphVersions)
|
|
50
|
+
.where(
|
|
51
|
+
and(
|
|
52
|
+
eq(factoryGraphVersions.factoryId, factoryId),
|
|
53
|
+
eq(factoryGraphVersions.orgId, orgId),
|
|
54
|
+
beforeVersion === undefined
|
|
55
|
+
? undefined
|
|
56
|
+
: lt(factoryGraphVersions.version, beforeVersion),
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
.orderBy(desc(factoryGraphVersions.version))
|
|
60
|
+
.limit(limit + 1);
|
|
61
|
+
const page = rows.slice(0, limit);
|
|
62
|
+
const hasMore = rows.length > limit;
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
factoryId,
|
|
66
|
+
currentVersion,
|
|
67
|
+
hasMore,
|
|
68
|
+
nextBeforeVersion:
|
|
69
|
+
hasMore && page.length > 0 ? page[page.length - 1].version : null,
|
|
70
|
+
versions: page.map((row) => ({
|
|
71
|
+
id: row.id,
|
|
72
|
+
factoryId: row.factoryId,
|
|
73
|
+
version: row.version,
|
|
74
|
+
source: row.source,
|
|
75
|
+
changeSummary: row.changeSummary,
|
|
76
|
+
createdAt: row.createdAt,
|
|
77
|
+
createdBy: row.createdBy,
|
|
78
|
+
isCurrent: currentVersion === row.version,
|
|
79
|
+
})),
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core/action";
|
|
2
|
+
import { buildDeepLink } from "@agent-native/core/server";
|
|
3
|
+
import { and, eq } from "drizzle-orm";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { getDb } from "../server/db/index.js";
|
|
7
|
+
import {
|
|
8
|
+
factoryDefinitions,
|
|
9
|
+
factoryGraphVersions,
|
|
10
|
+
} from "../server/db/schema.js";
|
|
11
|
+
import { normalizeFactoryGraph } from "../server/factory-graph/contracts.js";
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_FACTORY_ID,
|
|
14
|
+
parseFactoryGraph,
|
|
15
|
+
} from "../server/factory-graph/store.js";
|
|
16
|
+
import {
|
|
17
|
+
requireWorkspaceMember,
|
|
18
|
+
workspaceMemberIdentityFromContext,
|
|
19
|
+
} from "../server/lib/require-workspace-member.js";
|
|
20
|
+
import { stableId } from "../server/triage/ids.js";
|
|
21
|
+
|
|
22
|
+
export default defineAction({
|
|
23
|
+
description:
|
|
24
|
+
"Restore a Factory visual graph version. Restoring never deletes history: it validates the selected snapshot, appends it as a new version, and makes that new version current while preserving the Factory prompt.",
|
|
25
|
+
schema: z.object({
|
|
26
|
+
factoryId: z
|
|
27
|
+
.string()
|
|
28
|
+
.trim()
|
|
29
|
+
.min(1)
|
|
30
|
+
.max(120)
|
|
31
|
+
.regex(/^[a-z0-9][a-z0-9-]*$/)
|
|
32
|
+
.default(DEFAULT_FACTORY_ID),
|
|
33
|
+
versionId: z.string().trim().min(1).max(240),
|
|
34
|
+
}),
|
|
35
|
+
link: ({ result }) => ({
|
|
36
|
+
url: buildDeepLink({
|
|
37
|
+
app: "factory",
|
|
38
|
+
view: "factory",
|
|
39
|
+
params: { factoryId: result.factoryId, tab: "history" },
|
|
40
|
+
}),
|
|
41
|
+
label: `Open ${result.name} history in Factory`,
|
|
42
|
+
}),
|
|
43
|
+
run: async ({ factoryId, versionId }, context) => {
|
|
44
|
+
const { userEmail, orgId } = await requireWorkspaceMember(
|
|
45
|
+
workspaceMemberIdentityFromContext(context),
|
|
46
|
+
);
|
|
47
|
+
const db = getDb();
|
|
48
|
+
|
|
49
|
+
return db.transaction(async (tx) => {
|
|
50
|
+
const definition = (
|
|
51
|
+
await tx
|
|
52
|
+
.select()
|
|
53
|
+
.from(factoryDefinitions)
|
|
54
|
+
.where(
|
|
55
|
+
and(
|
|
56
|
+
eq(factoryDefinitions.id, factoryId),
|
|
57
|
+
eq(factoryDefinitions.orgId, orgId),
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
.limit(1)
|
|
61
|
+
)[0];
|
|
62
|
+
if (!definition) {
|
|
63
|
+
throw new Error("Factory has no saved definition to restore.");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const version = (
|
|
67
|
+
await tx
|
|
68
|
+
.select()
|
|
69
|
+
.from(factoryGraphVersions)
|
|
70
|
+
.where(
|
|
71
|
+
and(
|
|
72
|
+
eq(factoryGraphVersions.id, versionId),
|
|
73
|
+
eq(factoryGraphVersions.factoryId, factoryId),
|
|
74
|
+
eq(factoryGraphVersions.orgId, orgId),
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
.limit(1)
|
|
78
|
+
)[0];
|
|
79
|
+
if (!version) {
|
|
80
|
+
throw new Error("Factory graph version not found.");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const selectedGraph = parseFactoryGraph(version.graphJson);
|
|
84
|
+
|
|
85
|
+
if (version.version === definition.graphVersion) {
|
|
86
|
+
return {
|
|
87
|
+
ok: true,
|
|
88
|
+
alreadyCurrent: true,
|
|
89
|
+
factoryId,
|
|
90
|
+
versionId: version.id,
|
|
91
|
+
restoredFromVersion: version.version,
|
|
92
|
+
graphVersion: definition.graphVersion,
|
|
93
|
+
name: definition.name,
|
|
94
|
+
graph: selectedGraph,
|
|
95
|
+
source: "restore" as const,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const nextVersion = definition.graphVersion + 1;
|
|
100
|
+
const restoredGraph = normalizeFactoryGraph({
|
|
101
|
+
...selectedGraph,
|
|
102
|
+
version: nextVersion,
|
|
103
|
+
});
|
|
104
|
+
const now = new Date().toISOString();
|
|
105
|
+
const nextVersionId = stableId(
|
|
106
|
+
"factory-graph",
|
|
107
|
+
orgId,
|
|
108
|
+
factoryId,
|
|
109
|
+
String(nextVersion),
|
|
110
|
+
);
|
|
111
|
+
const changeSummary = `Restored version ${version.version}.`;
|
|
112
|
+
|
|
113
|
+
const updated = await tx
|
|
114
|
+
.update(factoryDefinitions)
|
|
115
|
+
.set({
|
|
116
|
+
name: restoredGraph.name,
|
|
117
|
+
description: restoredGraph.description,
|
|
118
|
+
graphVersion: nextVersion,
|
|
119
|
+
graphJson: JSON.stringify(restoredGraph),
|
|
120
|
+
updatedAt: now,
|
|
121
|
+
ownerEmail: userEmail,
|
|
122
|
+
})
|
|
123
|
+
.where(
|
|
124
|
+
and(
|
|
125
|
+
eq(factoryDefinitions.id, factoryId),
|
|
126
|
+
eq(factoryDefinitions.orgId, orgId),
|
|
127
|
+
eq(factoryDefinitions.graphVersion, definition.graphVersion),
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
.returning({ id: factoryDefinitions.id });
|
|
131
|
+
if (updated.length === 0) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
"Factory changed while restoring. Refresh history and try again.",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await tx.insert(factoryGraphVersions).values({
|
|
138
|
+
id: nextVersionId,
|
|
139
|
+
factoryId,
|
|
140
|
+
version: nextVersion,
|
|
141
|
+
graphJson: JSON.stringify(restoredGraph),
|
|
142
|
+
source: "restore",
|
|
143
|
+
changeSummary,
|
|
144
|
+
createdAt: now,
|
|
145
|
+
createdBy: userEmail,
|
|
146
|
+
ownerEmail: userEmail,
|
|
147
|
+
orgId,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
ok: true,
|
|
152
|
+
alreadyCurrent: false,
|
|
153
|
+
factoryId,
|
|
154
|
+
versionId: nextVersionId,
|
|
155
|
+
restoredFromVersion: version.version,
|
|
156
|
+
graphVersion: nextVersion,
|
|
157
|
+
name: restoredGraph.name,
|
|
158
|
+
graph: restoredGraph,
|
|
159
|
+
source: "restore" as const,
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
});
|