@vantageos/convex-doc-forge 0.1.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 (56) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +110 -0
  3. package/README.md +169 -0
  4. package/convex.config.ts +22 -0
  5. package/dist/_generated/api.d.ts +3 -0
  6. package/dist/_generated/api.d.ts.map +1 -0
  7. package/dist/_generated/api.js +13 -0
  8. package/dist/_generated/dataModel.d.ts +13 -0
  9. package/dist/_generated/dataModel.d.ts.map +1 -0
  10. package/dist/_generated/dataModel.js +1 -0
  11. package/dist/_generated/server.d.ts +20 -0
  12. package/dist/_generated/server.d.ts.map +1 -0
  13. package/dist/_generated/server.js +8 -0
  14. package/dist/actions.d.ts +20 -0
  15. package/dist/actions.d.ts.map +1 -0
  16. package/dist/actions.js +176 -0
  17. package/dist/convex.config.d.ts +20 -0
  18. package/dist/convex.config.d.ts.map +1 -0
  19. package/dist/convex.config.js +20 -0
  20. package/dist/index.d.ts +10 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +9 -0
  23. package/dist/mutations.d.ts +84 -0
  24. package/dist/mutations.d.ts.map +1 -0
  25. package/dist/mutations.js +166 -0
  26. package/dist/queries.d.ts +30 -0
  27. package/dist/queries.d.ts.map +1 -0
  28. package/dist/queries.js +117 -0
  29. package/dist/schema.d.ts +97 -0
  30. package/dist/schema.d.ts.map +1 -0
  31. package/dist/schema.js +59 -0
  32. package/dist/src/_generated/api.d.ts +3 -0
  33. package/dist/src/_generated/api.d.ts.map +1 -0
  34. package/dist/src/_generated/api.js +13 -0
  35. package/dist/src/_generated/dataModel.d.ts +13 -0
  36. package/dist/src/_generated/dataModel.d.ts.map +1 -0
  37. package/dist/src/_generated/dataModel.js +1 -0
  38. package/dist/src/_generated/server.d.ts +20 -0
  39. package/dist/src/_generated/server.d.ts.map +1 -0
  40. package/dist/src/_generated/server.js +8 -0
  41. package/dist/src/actions.d.ts +20 -0
  42. package/dist/src/actions.d.ts.map +1 -0
  43. package/dist/src/actions.js +176 -0
  44. package/dist/src/index.d.ts +10 -0
  45. package/dist/src/index.d.ts.map +1 -0
  46. package/dist/src/index.js +9 -0
  47. package/dist/src/mutations.d.ts +84 -0
  48. package/dist/src/mutations.d.ts.map +1 -0
  49. package/dist/src/mutations.js +166 -0
  50. package/dist/src/queries.d.ts +30 -0
  51. package/dist/src/queries.d.ts.map +1 -0
  52. package/dist/src/queries.js +117 -0
  53. package/dist/src/schema.d.ts +97 -0
  54. package/dist/src/schema.d.ts.map +1 -0
  55. package/dist/src/schema.js +59 -0
  56. package/package.json +44 -0
@@ -0,0 +1,8 @@
1
+ import { actionGeneric, httpActionGeneric, queryGeneric, mutationGeneric, internalActionGeneric, internalMutationGeneric, internalQueryGeneric, } from "convex/server";
2
+ export const query = queryGeneric;
3
+ export const internalQuery = internalQueryGeneric;
4
+ export const mutation = mutationGeneric;
5
+ export const internalMutation = internalMutationGeneric;
6
+ export const action = actionGeneric;
7
+ export const internalAction = internalActionGeneric;
8
+ export const httpAction = httpActionGeneric;
@@ -0,0 +1,20 @@
1
+ export declare function renderNowHandler(ctx: any, args: {
2
+ template_id: string;
3
+ context: unknown;
4
+ output_format: string;
5
+ actor: string;
6
+ source?: "manual" | "workflow" | "agent";
7
+ }): Promise<{
8
+ output_url: string;
9
+ render_job_id: string;
10
+ duration_ms: number;
11
+ pages?: number;
12
+ }>;
13
+ export declare const render_now: import("convex/server").RegisteredAction<"public", {
14
+ source?: "manual" | "workflow" | "agent" | undefined;
15
+ template_id: import("convex/values").GenericId<"templates">;
16
+ context: any;
17
+ output_format: string;
18
+ actor: string;
19
+ }, any>;
20
+ //# sourceMappingURL=actions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../src/actions.ts"],"names":[],"mappings":"AAgCA,wBAAsB,gBAAgB,CAEpC,GAAG,EAAE,GAAG,EACR,IAAI,EAAE;IACJ,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC;CAC1C,GACA,OAAO,CAAC;IACT,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC,CAoID;AAMD,eAAO,MAAM,UAAU;;;;;;OAsBrB,CAAC"}
@@ -0,0 +1,176 @@
1
+ "use node";
2
+ import { v } from "convex/values";
3
+ import { action } from "./_generated/server";
4
+ import { internal } from "./_generated/api";
5
+ // ---------------------------------------------------------------------------
6
+ // SHA-256 helper (Node built-in crypto)
7
+ // ---------------------------------------------------------------------------
8
+ async function sha256hex(input) {
9
+ const { createHash } = await import("node:crypto");
10
+ return createHash("sha256").update(input).digest("hex");
11
+ }
12
+ // ---------------------------------------------------------------------------
13
+ // renderNowHandler — exported for unit testing
14
+ // ---------------------------------------------------------------------------
15
+ const SIGNED_URL_TTL_MS = 7 * 24 * 60 * 60 * 1000;
16
+ export async function renderNowHandler(
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ ctx, args) {
19
+ const rendererUrl = process.env.CONVEX_DOC_FORGE_RENDERER_URL;
20
+ const rendererKey = process.env.CONVEX_DOC_FORGE_RENDERER_KEY;
21
+ if (!rendererUrl)
22
+ throw new Error("CONVEX_DOC_FORGE_RENDERER_URL not set");
23
+ if (!rendererKey)
24
+ throw new Error("CONVEX_DOC_FORGE_RENDERER_KEY not set");
25
+ // 1. Load template
26
+ const template = await ctx.runQuery(internal.queries.get_template, {
27
+ template_id: args.template_id,
28
+ });
29
+ if (template === null)
30
+ throw new Error("Template not found");
31
+ if (!template.output_formats.includes(args.output_format)) {
32
+ throw new Error(`output_format '${args.output_format}' not supported. ` +
33
+ `Supported: ${template.output_formats.join(", ")}`);
34
+ }
35
+ // 2. Get signed URL for the binary asset
36
+ const assetUrl = await ctx.storage.getUrl(template.asset_storage_id);
37
+ if (!assetUrl)
38
+ throw new Error("Asset storage URL unavailable");
39
+ // 3. Create a queued job
40
+ const jobId = await ctx.runMutation(internal.mutations.queue_render, {
41
+ template_id: args.template_id,
42
+ context: args.context,
43
+ output_format: args.output_format,
44
+ createdBy: args.actor,
45
+ });
46
+ // 4. Transition to rendering
47
+ await ctx.runMutation(internal.mutations._set_job_rendering, {
48
+ job_id: jobId,
49
+ });
50
+ const contextHash = await sha256hex(JSON.stringify(args.context));
51
+ const startTs = Date.now();
52
+ try {
53
+ // 5. Call renderer Railway
54
+ const rendererRes = await fetch(`${rendererUrl}/v1/render`, {
55
+ method: "POST",
56
+ headers: {
57
+ "Content-Type": "application/json",
58
+ Authorization: `Bearer ${rendererKey}`,
59
+ },
60
+ body: JSON.stringify({
61
+ template_url: assetUrl,
62
+ context: args.context,
63
+ output_format: args.output_format,
64
+ }),
65
+ });
66
+ if (!rendererRes.ok) {
67
+ const errText = await rendererRes.text().catch(() => "<no body>");
68
+ throw new Error(`Renderer returned ${rendererRes.status}: ${errText}`);
69
+ }
70
+ const rendererJson = (await rendererRes.json());
71
+ const duration_ms = rendererJson.duration_ms ?? Date.now() - startTs;
72
+ // 6. Store output in Convex storage
73
+ let outputStorageId;
74
+ let signedUrl;
75
+ if (rendererJson.output_bytes) {
76
+ const binaryStr = atob(rendererJson.output_bytes);
77
+ const bytes = new Uint8Array(binaryStr.length);
78
+ for (let i = 0; i < binaryStr.length; i++) {
79
+ bytes[i] = binaryStr.charCodeAt(i);
80
+ }
81
+ const mimeType = mimeForFormat(args.output_format);
82
+ const blob = new Blob([bytes], { type: mimeType });
83
+ outputStorageId = await ctx.storage.store(blob);
84
+ const url = await ctx.storage.getUrl(outputStorageId);
85
+ if (!url)
86
+ throw new Error("Failed to get signed URL after storage");
87
+ signedUrl = url;
88
+ }
89
+ else if (rendererJson.output_url) {
90
+ const downloadRes = await fetch(rendererJson.output_url);
91
+ if (!downloadRes.ok)
92
+ throw new Error(`Failed to download output from renderer URL: ${downloadRes.status}`);
93
+ const mimeType = mimeForFormat(args.output_format);
94
+ const blob = new Blob([await downloadRes.arrayBuffer()], {
95
+ type: mimeType,
96
+ });
97
+ outputStorageId = await ctx.storage.store(blob);
98
+ const url = await ctx.storage.getUrl(outputStorageId);
99
+ if (!url)
100
+ throw new Error("Failed to get signed URL after storage");
101
+ signedUrl = url;
102
+ }
103
+ else {
104
+ throw new Error("Renderer response missing both output_bytes and output_url");
105
+ }
106
+ // 7. Mark job done
107
+ await ctx.runMutation(internal.mutations._complete_render_job, {
108
+ job_id: jobId,
109
+ output_storage_id: outputStorageId,
110
+ });
111
+ // 8. Write audit record
112
+ const expiresAt = Date.now() + SIGNED_URL_TTL_MS;
113
+ await ctx.runMutation(internal.mutations._write_audit, {
114
+ render_job_id: jobId,
115
+ tenant_id: template.tenant_id,
116
+ template_id: args.template_id,
117
+ template_version: template.version,
118
+ context_hash: contextHash,
119
+ output_signed_url: signedUrl,
120
+ expires_at: expiresAt,
121
+ actor: args.actor,
122
+ source: args.source ?? "manual",
123
+ });
124
+ return {
125
+ output_url: signedUrl,
126
+ render_job_id: jobId,
127
+ duration_ms,
128
+ pages: rendererJson.pages,
129
+ };
130
+ }
131
+ catch (err) {
132
+ const message = err instanceof Error ? err.message : String(err);
133
+ await ctx.runMutation(internal.mutations._fail_render_job, {
134
+ job_id: jobId,
135
+ error: message,
136
+ });
137
+ throw err;
138
+ }
139
+ }
140
+ // ---------------------------------------------------------------------------
141
+ // Convex registration
142
+ // ---------------------------------------------------------------------------
143
+ export const render_now = action({
144
+ args: {
145
+ template_id: v.id("templates"),
146
+ context: v.any(),
147
+ output_format: v.string(),
148
+ actor: v.string(),
149
+ source: v.optional(v.union(v.literal("manual"), v.literal("workflow"), v.literal("agent"))),
150
+ },
151
+ returns: v.object({
152
+ output_url: v.string(),
153
+ render_job_id: v.id("render_jobs"),
154
+ duration_ms: v.number(),
155
+ pages: v.optional(v.number()),
156
+ }),
157
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
158
+ handler: renderNowHandler,
159
+ });
160
+ // ---------------------------------------------------------------------------
161
+ // Helpers
162
+ // ---------------------------------------------------------------------------
163
+ function mimeForFormat(format) {
164
+ switch (format.toLowerCase()) {
165
+ case "pdf":
166
+ return "application/pdf";
167
+ case "docx":
168
+ return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
169
+ case "pptx":
170
+ return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
171
+ case "doc":
172
+ return "application/msword";
173
+ default:
174
+ return "application/octet-stream";
175
+ }
176
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @vantageos/convex-doc-forge — public re-exports.
3
+ *
4
+ * Consumer apps import functions via the component API object, not directly.
5
+ * This barrel is provided for type-only imports.
6
+ */
7
+ export * from "./queries";
8
+ export * from "./mutations";
9
+ export * from "./actions";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @vantageos/convex-doc-forge — public re-exports.
3
+ *
4
+ * Consumer apps import functions via the component API object, not directly.
5
+ * This barrel is provided for type-only imports.
6
+ */
7
+ export * from "./queries";
8
+ export * from "./mutations";
9
+ export * from "./actions";
@@ -0,0 +1,84 @@
1
+ import type { Id } from "./_generated/dataModel";
2
+ type AnyCtx = any;
3
+ export declare function upsertTemplateHandler(ctx: AnyCtx, args: {
4
+ name: string;
5
+ team: string;
6
+ tenant_id: string;
7
+ renderer_kind: "docxtpl" | "python-pptx" | "jinja2-html";
8
+ input_schema: unknown;
9
+ asset_storage_id: Id<"_storage">;
10
+ output_formats: string[];
11
+ metadata?: Record<string, string>;
12
+ version: string;
13
+ createdBy: string;
14
+ }): Promise<Id<"templates">>;
15
+ export declare function queueRenderHandler(ctx: AnyCtx, args: {
16
+ template_id: Id<"templates">;
17
+ context: unknown;
18
+ output_format: string;
19
+ createdBy: string;
20
+ }): Promise<Id<"render_jobs">>;
21
+ export declare function setJobRenderingHandler(ctx: AnyCtx, args: {
22
+ job_id: Id<"render_jobs">;
23
+ }): Promise<null>;
24
+ export declare function completeRenderJobHandler(ctx: AnyCtx, args: {
25
+ job_id: Id<"render_jobs">;
26
+ output_storage_id: Id<"_storage">;
27
+ }): Promise<null>;
28
+ export declare function failRenderJobHandler(ctx: AnyCtx, args: {
29
+ job_id: Id<"render_jobs">;
30
+ error: string;
31
+ }): Promise<null>;
32
+ export declare function writeAuditHandler(ctx: AnyCtx, args: {
33
+ render_job_id: Id<"render_jobs">;
34
+ tenant_id: string;
35
+ template_id: Id<"templates">;
36
+ template_version: string;
37
+ context_hash: string;
38
+ output_signed_url: string;
39
+ expires_at: number;
40
+ actor: string;
41
+ source: "manual" | "workflow" | "agent";
42
+ }): Promise<Id<"render_audit">>;
43
+ export declare const upsert_template: import("convex/server").RegisteredMutation<"public", {
44
+ name: string;
45
+ team: string;
46
+ tenant_id: string;
47
+ renderer_kind: "docxtpl" | "python-pptx" | "jinja2-html";
48
+ input_schema: unknown;
49
+ asset_storage_id: Id<"_storage">;
50
+ output_formats: string[];
51
+ metadata?: Record<string, string>;
52
+ version: string;
53
+ createdBy: string;
54
+ }, Promise<Id<"templates">>>;
55
+ export declare const queue_render: import("convex/server").RegisteredMutation<"public", {
56
+ template_id: Id<"templates">;
57
+ context: unknown;
58
+ output_format: string;
59
+ createdBy: string;
60
+ }, Promise<Id<"render_jobs">>>;
61
+ export declare const _set_job_rendering: import("convex/server").RegisteredMutation<"internal", {
62
+ job_id: Id<"render_jobs">;
63
+ }, Promise<null>>;
64
+ export declare const _complete_render_job: import("convex/server").RegisteredMutation<"internal", {
65
+ job_id: Id<"render_jobs">;
66
+ output_storage_id: Id<"_storage">;
67
+ }, Promise<null>>;
68
+ export declare const _fail_render_job: import("convex/server").RegisteredMutation<"internal", {
69
+ job_id: Id<"render_jobs">;
70
+ error: string;
71
+ }, Promise<null>>;
72
+ export declare const _write_audit: import("convex/server").RegisteredMutation<"internal", {
73
+ render_job_id: Id<"render_jobs">;
74
+ tenant_id: string;
75
+ template_id: Id<"templates">;
76
+ template_version: string;
77
+ context_hash: string;
78
+ output_signed_url: string;
79
+ expires_at: number;
80
+ actor: string;
81
+ source: "manual" | "workflow" | "agent";
82
+ }, Promise<Id<"render_audit">>>;
83
+ export {};
84
+ //# sourceMappingURL=mutations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mutations.d.ts","sourceRoot":"","sources":["../../src/mutations.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,166 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { v } from "convex/values";
3
+ import { mutation, internalMutation } from "./_generated/server";
4
+ // ---------------------------------------------------------------------------
5
+ // Handler functions — exported separately for unit testing
6
+ // ---------------------------------------------------------------------------
7
+ export async function upsertTemplateHandler(ctx, args) {
8
+ if (!args.tenant_id)
9
+ throw new Error("tenant_id is required");
10
+ if (!args.name.trim())
11
+ throw new Error("name must not be empty");
12
+ if (args.output_formats.length === 0)
13
+ throw new Error("output_formats must have at least one entry");
14
+ const existing = await ctx.db
15
+ .query("templates")
16
+ .withIndex("by_tenant_team", (q) => q.eq("tenant_id", args.tenant_id).eq("team", args.team))
17
+ .filter((q) => q.eq(q.field("name"), args.name))
18
+ .first();
19
+ const now = Date.now();
20
+ if (existing !== null) {
21
+ await ctx.db.patch(existing._id, {
22
+ renderer_kind: args.renderer_kind,
23
+ input_schema: args.input_schema,
24
+ asset_storage_id: args.asset_storage_id,
25
+ output_formats: args.output_formats,
26
+ metadata: args.metadata,
27
+ version: args.version,
28
+ updatedAt: now,
29
+ });
30
+ return existing._id;
31
+ }
32
+ return await ctx.db.insert("templates", {
33
+ name: args.name,
34
+ team: args.team,
35
+ tenant_id: args.tenant_id,
36
+ template_type: "doc-binary",
37
+ renderer_kind: args.renderer_kind,
38
+ input_schema: args.input_schema,
39
+ asset_storage_id: args.asset_storage_id,
40
+ output_formats: args.output_formats,
41
+ metadata: args.metadata,
42
+ version: args.version,
43
+ createdAt: now,
44
+ updatedAt: now,
45
+ });
46
+ }
47
+ export async function queueRenderHandler(ctx, args) {
48
+ const template = await ctx.db.get(args.template_id);
49
+ if (template === null)
50
+ throw new Error("Template not found");
51
+ if (!args.output_format.trim())
52
+ throw new Error("output_format required");
53
+ if (!template.output_formats.includes(args.output_format)) {
54
+ throw new Error(`output_format '${args.output_format}' not supported by this template. ` +
55
+ `Supported: ${template.output_formats.join(", ")}`);
56
+ }
57
+ return await ctx.db.insert("render_jobs", {
58
+ template_id: args.template_id,
59
+ tenant_id: template.tenant_id,
60
+ context: args.context,
61
+ output_format: args.output_format,
62
+ status: "queued",
63
+ createdAt: Date.now(),
64
+ createdBy: args.createdBy,
65
+ });
66
+ }
67
+ export async function setJobRenderingHandler(ctx, args) {
68
+ const job = await ctx.db.get(args.job_id);
69
+ if (job === null)
70
+ throw new Error("render_job not found");
71
+ await ctx.db.patch(args.job_id, { status: "rendering" });
72
+ return null;
73
+ }
74
+ export async function completeRenderJobHandler(ctx, args) {
75
+ const job = await ctx.db.get(args.job_id);
76
+ if (job === null)
77
+ throw new Error("render_job not found");
78
+ await ctx.db.patch(args.job_id, {
79
+ status: "done",
80
+ output_storage_id: args.output_storage_id,
81
+ completedAt: Date.now(),
82
+ });
83
+ return null;
84
+ }
85
+ export async function failRenderJobHandler(ctx, args) {
86
+ const job = await ctx.db.get(args.job_id);
87
+ if (job === null)
88
+ throw new Error("render_job not found");
89
+ await ctx.db.patch(args.job_id, {
90
+ status: "failed",
91
+ error: args.error,
92
+ completedAt: Date.now(),
93
+ });
94
+ return null;
95
+ }
96
+ export async function writeAuditHandler(ctx, args) {
97
+ return await ctx.db.insert("render_audit", {
98
+ ...args,
99
+ createdAt: Date.now(),
100
+ });
101
+ }
102
+ // ---------------------------------------------------------------------------
103
+ // Convex registrations
104
+ // ---------------------------------------------------------------------------
105
+ export const upsert_template = mutation({
106
+ args: {
107
+ name: v.string(),
108
+ team: v.string(),
109
+ tenant_id: v.string(),
110
+ renderer_kind: v.union(v.literal("docxtpl"), v.literal("python-pptx"), v.literal("jinja2-html")),
111
+ input_schema: v.any(),
112
+ asset_storage_id: v.id("_storage"),
113
+ output_formats: v.array(v.string()),
114
+ metadata: v.optional(v.record(v.string(), v.string())),
115
+ version: v.string(),
116
+ createdBy: v.string(),
117
+ },
118
+ returns: v.id("templates"),
119
+ handler: upsertTemplateHandler,
120
+ });
121
+ export const queue_render = mutation({
122
+ args: {
123
+ template_id: v.id("templates"),
124
+ context: v.any(),
125
+ output_format: v.string(),
126
+ createdBy: v.string(),
127
+ },
128
+ returns: v.id("render_jobs"),
129
+ handler: queueRenderHandler,
130
+ });
131
+ export const _set_job_rendering = internalMutation({
132
+ args: { job_id: v.id("render_jobs") },
133
+ returns: v.null(),
134
+ handler: setJobRenderingHandler,
135
+ });
136
+ export const _complete_render_job = internalMutation({
137
+ args: {
138
+ job_id: v.id("render_jobs"),
139
+ output_storage_id: v.id("_storage"),
140
+ },
141
+ returns: v.null(),
142
+ handler: completeRenderJobHandler,
143
+ });
144
+ export const _fail_render_job = internalMutation({
145
+ args: {
146
+ job_id: v.id("render_jobs"),
147
+ error: v.string(),
148
+ },
149
+ returns: v.null(),
150
+ handler: failRenderJobHandler,
151
+ });
152
+ export const _write_audit = internalMutation({
153
+ args: {
154
+ render_job_id: v.id("render_jobs"),
155
+ tenant_id: v.string(),
156
+ template_id: v.id("templates"),
157
+ template_version: v.string(),
158
+ context_hash: v.string(),
159
+ output_signed_url: v.string(),
160
+ expires_at: v.number(),
161
+ actor: v.string(),
162
+ source: v.union(v.literal("manual"), v.literal("workflow"), v.literal("agent")),
163
+ },
164
+ returns: v.id("render_audit"),
165
+ handler: writeAuditHandler,
166
+ });
@@ -0,0 +1,30 @@
1
+ import type { Id } from "./_generated/dataModel";
2
+ type AnyCtx = any;
3
+ export declare function listTemplatesHandler(ctx: AnyCtx, args: {
4
+ tenant_id: string;
5
+ team?: string;
6
+ type?: "doc-binary";
7
+ }): Promise<any[]>;
8
+ export declare function getTemplateHandler(ctx: AnyCtx, args: {
9
+ template_id: Id<"templates">;
10
+ }): Promise<any>;
11
+ export declare function listRenderHistoryHandler(ctx: AnyCtx, args: {
12
+ tenant_id: string;
13
+ template_id?: Id<"templates">;
14
+ since?: number;
15
+ }): Promise<any[]>;
16
+ export declare const list_templates: import("convex/server").RegisteredQuery<"public", {
17
+ tenant_id: string;
18
+ team?: string;
19
+ type?: "doc-binary";
20
+ }, Promise<any[]>>;
21
+ export declare const get_template: import("convex/server").RegisteredQuery<"public", {
22
+ template_id: Id<"templates">;
23
+ }, Promise<any>>;
24
+ export declare const list_render_history: import("convex/server").RegisteredQuery<"public", {
25
+ tenant_id: string;
26
+ template_id?: Id<"templates">;
27
+ since?: number;
28
+ }, Promise<any[]>>;
29
+ export {};
30
+ //# sourceMappingURL=queries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queries.d.ts","sourceRoot":"","sources":["../../src/queries.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,wBAAwB,CAAC;AAGjD,KAAK,MAAM,GAAG,GAAG,CAAC;AAMlB,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,YAAY,CAAA;CAAE,kBAsBhE;AAED,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,WAAW,EAAE,EAAE,CAAC,WAAW,CAAC,CAAA;CAAE,gBAGvC;AAED,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,kBA0B3E;AAMD,eAAO,MAAM,cAAc;eAjEN,MAAM;WAAS,MAAM;WAAS,YAAY;kBA8F7D,CAAC;AAEH,eAAO,MAAM,YAAY;iBAtEF,EAAE,CAAC,WAAW,CAAC;gBAgGpC,CAAC;AAEH,eAAO,MAAM,mBAAmB;eA3FX,MAAM;kBAAgB,EAAE,CAAC,WAAW,CAAC;YAAU,MAAM;kBAsHxE,CAAC"}
@@ -0,0 +1,117 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { v } from "convex/values";
3
+ import { query } from "./_generated/server";
4
+ // ---------------------------------------------------------------------------
5
+ // Handler functions — exported separately for unit testing
6
+ // ---------------------------------------------------------------------------
7
+ export async function listTemplatesHandler(ctx, args) {
8
+ let results;
9
+ if (args.team !== undefined) {
10
+ results = await ctx.db
11
+ .query("templates")
12
+ .withIndex("by_tenant_team", (q) => q.eq("tenant_id", args.tenant_id).eq("team", args.team))
13
+ .collect();
14
+ }
15
+ else {
16
+ results = await ctx.db
17
+ .query("templates")
18
+ .withIndex("by_tenant", (q) => q.eq("tenant_id", args.tenant_id))
19
+ .collect();
20
+ }
21
+ if (args.type !== undefined) {
22
+ return results.filter((t) => t.template_type === args.type);
23
+ }
24
+ return results;
25
+ }
26
+ export async function getTemplateHandler(ctx, args) {
27
+ return await ctx.db.get(args.template_id);
28
+ }
29
+ export async function listRenderHistoryHandler(ctx, args) {
30
+ if (args.template_id !== undefined) {
31
+ const rows = await ctx.db
32
+ .query("render_audit")
33
+ .withIndex("by_template", (q) => q.eq("template_id", args.template_id))
34
+ .order("desc")
35
+ .collect();
36
+ const since = args.since ?? 0;
37
+ return rows.filter((r) => r.tenant_id === args.tenant_id && r.createdAt >= since);
38
+ }
39
+ const rows = await ctx.db
40
+ .query("render_audit")
41
+ .withIndex("by_tenant", (q) => q.eq("tenant_id", args.tenant_id))
42
+ .order("desc")
43
+ .collect();
44
+ if (args.since !== undefined) {
45
+ return rows.filter((r) => r.createdAt >= args.since);
46
+ }
47
+ return rows;
48
+ }
49
+ // ---------------------------------------------------------------------------
50
+ // Convex registrations
51
+ // ---------------------------------------------------------------------------
52
+ export const list_templates = query({
53
+ args: {
54
+ tenant_id: v.string(),
55
+ team: v.optional(v.string()),
56
+ type: v.optional(v.literal("doc-binary")),
57
+ },
58
+ returns: v.array(v.object({
59
+ _id: v.id("templates"),
60
+ _creationTime: v.number(),
61
+ name: v.string(),
62
+ team: v.string(),
63
+ tenant_id: v.string(),
64
+ template_type: v.literal("doc-binary"),
65
+ renderer_kind: v.union(v.literal("docxtpl"), v.literal("python-pptx"), v.literal("jinja2-html")),
66
+ input_schema: v.any(),
67
+ asset_storage_id: v.id("_storage"),
68
+ output_formats: v.array(v.string()),
69
+ metadata: v.optional(v.record(v.string(), v.string())),
70
+ version: v.string(),
71
+ createdAt: v.number(),
72
+ updatedAt: v.number(),
73
+ })),
74
+ handler: listTemplatesHandler,
75
+ });
76
+ export const get_template = query({
77
+ args: { template_id: v.id("templates") },
78
+ returns: v.union(v.object({
79
+ _id: v.id("templates"),
80
+ _creationTime: v.number(),
81
+ name: v.string(),
82
+ team: v.string(),
83
+ tenant_id: v.string(),
84
+ template_type: v.literal("doc-binary"),
85
+ renderer_kind: v.union(v.literal("docxtpl"), v.literal("python-pptx"), v.literal("jinja2-html")),
86
+ input_schema: v.any(),
87
+ asset_storage_id: v.id("_storage"),
88
+ output_formats: v.array(v.string()),
89
+ metadata: v.optional(v.record(v.string(), v.string())),
90
+ version: v.string(),
91
+ createdAt: v.number(),
92
+ updatedAt: v.number(),
93
+ }), v.null()),
94
+ handler: getTemplateHandler,
95
+ });
96
+ export const list_render_history = query({
97
+ args: {
98
+ tenant_id: v.string(),
99
+ template_id: v.optional(v.id("templates")),
100
+ since: v.optional(v.number()),
101
+ },
102
+ returns: v.array(v.object({
103
+ _id: v.id("render_audit"),
104
+ _creationTime: v.number(),
105
+ render_job_id: v.id("render_jobs"),
106
+ tenant_id: v.string(),
107
+ template_id: v.id("templates"),
108
+ template_version: v.string(),
109
+ context_hash: v.string(),
110
+ output_signed_url: v.string(),
111
+ expires_at: v.number(),
112
+ actor: v.string(),
113
+ source: v.union(v.literal("manual"), v.literal("workflow"), v.literal("agent")),
114
+ createdAt: v.number(),
115
+ })),
116
+ handler: listRenderHistoryHandler,
117
+ });