Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/frontend/app/api/v1/osograph/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { gql } from "graphql-tag";
import { getSystemCredentials, getUser } from "@/lib/auth/auth";
import { buildSubgraphSchema } from "@apollo/subgraph";
import { resolvers } from "@/app/api/v1/osograph/schema/resolvers";
import { GraphQLContext } from "@/app/api/v1/osograph/types/context";

function loadSchemas(): string {
const schemaDir = path.join(
Expand Down Expand Up @@ -44,7 +45,7 @@ const apolloHandler = startServerAndCreateNextHandler<NextRequest>(server, {
context: async (req) => {
const user = await getUser(req);
const systemCredentials = await getSystemCredentials(req);
return { req, user, systemCredentials };
return { req, user, systemCredentials } satisfies GraphQLContext;
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ type Notebook {
id: ID!
orgId: ID!
name: String!
data: String!
description: String
creatorId: ID!
preview: String
Expand Down Expand Up @@ -59,7 +60,19 @@ extend type Mutation {
"""
Save notebook preview image
"""
saveNotebookPreview(input: SaveNotebookPreviewInput!): SaveNotebookPreviewPayload!
saveNotebookPreview(
input: SaveNotebookPreviewInput!
): SaveNotebookPreviewPayload!

"""
Publish the notebook HTML to the public
"""
publishNotebook(input: PublishNotebookInput!): PublishNotebookPayload!

"""
Unpublish the notebook
"""
unpublishNotebook(notebookId: ID!): UnpublishNotebookPayload!
}

input CreateNotebookInput {
Expand Down Expand Up @@ -97,3 +110,18 @@ type SaveNotebookPreviewPayload {
previewUrl: String
message: String
}

input PublishNotebookInput {
notebookId: ID!
}

type PublishNotebookPayload {
success: Boolean!
message: String
run: Run!
}

type UnpublishNotebookPayload {
success: Boolean!
message: String
}
24 changes: 21 additions & 3 deletions apps/frontend/app/api/v1/osograph/schema/graphql/system.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
Special type for system related reads that are only allowed by authenticated
service accounts that have system privileges.
"""

type SystemResolvedTableReference {
reference: String!
fqn: String!
Expand All @@ -25,7 +24,7 @@ extend type Mutation {
System only. Mark a run as started.
"""
startRun(input: StartRunInput!): StartRunPayload!

"""
System only. Update run metadata. This can be called at any time
"""
Expand All @@ -49,7 +48,16 @@ extend type Mutation {
"""
System only. Create a materialization for a step
"""
createMaterialization(input: CreateMaterializationInput!): CreateMaterializationPayload!
createMaterialization(
input: CreateMaterializationInput!
): CreateMaterializationPayload!

"""
System only. Save the generated published notebook HTML to object storage
"""
savePublishedNotebookHtml(
input: SavePublishedNotebookHtmlInput!
): SavePublishedNotebookHtmlPayload!
}

extend type Query {
Expand Down Expand Up @@ -135,3 +143,13 @@ type CreateMaterializationPayload {
message: String
materialization: Materialization!
}

input SavePublishedNotebookHtmlInput {
notebookId: ID!
htmlContent: String!
}

type SavePublishedNotebookHtmlPayload {
success: Boolean!
message: String
}
124 changes: 124 additions & 0 deletions apps/frontend/app/api/v1/osograph/schema/resolvers/notebook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import {
validateInput,
} from "@/app/api/v1/osograph/utils/validation";
import { queryWithPagination } from "@/app/api/v1/osograph/utils/query-helpers";
import { signOsoJwt } from "@/lib/auth/auth";
import { createQueueService } from "@/lib/services/queue/factory";
import { PublishNotebookRunRequest } from "@opensource-observer/osoprotobufs/publish-notebook";
import { revalidateTag } from "next/cache";

const PREVIEWS_BUCKET = "notebook-previews";
const SIGNED_URL_EXPIRY = 900;
Expand Down Expand Up @@ -186,6 +190,126 @@ export const notebookResolvers: GraphQLResolverModule<GraphQLContext> = {
throw ServerErrors.storage("Failed to save notebook preview");
}
},

publishNotebook: async (
_: unknown,
args: { input: { notebookId: string } },
context: GraphQLContext,
) => {
const authenticatedUser = requireAuthentication(context.user);
const { notebookId } = args.input;

const supabase = createAdminClient();

const { data: notebook } = await supabase
.from("notebooks")
.select("id, organizations!inner(id, org_name)")
.eq("id", notebookId)
.single();

if (!notebook) {
throw NotebookErrors.notFound();
}

await requireOrgMembership(
authenticatedUser.userId,
notebook.organizations.id,
);

const osoToken = await signOsoJwt(authenticatedUser, {
orgId: notebook.organizations.id,
orgName: notebook.organizations.org_name,
});

const { data: queuedRun, error: queuedRunError } = await supabase
.from("run")
.insert({
org_id: notebook.organizations.id,
run_type: "manual",
requested_by: authenticatedUser.userId,
})
.select()
.single();
if (queuedRunError || !queuedRun) {
logger.error(
`Error creating run for notebook ${notebook.id}: ${queuedRunError?.message}`,
);
throw ServerErrors.database("Failed to create run request");
}

const queueService = createQueueService();

const runIdBuffer = Buffer.from(queuedRun.id.replace(/-/g, ""), "hex");
const publishMessage: PublishNotebookRunRequest = {
runId: new Uint8Array(runIdBuffer),
notebookId: notebook.id,
osoApiKey: osoToken,
};

const result = await queueService.queueMessage({
queueName: "publish_notebook_run_requests",
message: publishMessage,
encoder: PublishNotebookRunRequest,
});
if (!result.success) {
logger.error(
`Failed to publish message to queue: ${result.error?.message}`,
);
throw ServerErrors.queueError(
result.error?.message || "Failed to publish to queue",
);
}

return {
success: true,
run: queuedRun,
message: "Notebook publish run queued successfully",
};
},
unpublishNotebook: async (
_: unknown,
args: { notebookId: string },
context: GraphQLContext,
) => {
const authenticatedUser = requireAuthentication(context.user);
const { notebookId } = args;

const supabase = createAdminClient();

const { data: publishedNotebook, error } = await supabase
.from("published_notebooks")
.select("*")
.eq("notebook_id", notebookId)
.single();
if (error) {
logger.log("Failed to find published notebook:", error);
throw NotebookErrors.notFound();
}
const { error: deleteError } = await supabase.storage
.from("published-notebooks")
.remove([publishedNotebook.data_path]);
if (deleteError) {
logger.log("Failed to delete notebook file:", deleteError);
throw ServerErrors.database("Failed to delete notebook file");
}
const { error: updateError } = await supabase
.from("published_notebooks")
.update({
deleted_at: new Date().toISOString(),
updated_by: authenticatedUser.userId,
})
.eq("id", publishedNotebook.id);
if (updateError) {
logger.log("Failed to delete notebook file:", updateError);
throw ServerErrors.database("Failed to delete notebook file");
}

revalidateTag(publishedNotebook.id);
return {
success: true,
message: "Notebook unpublished successfully",
};
},
},

Notebook: {
Expand Down
74 changes: 74 additions & 0 deletions apps/frontend/app/api/v1/osograph/schema/resolvers/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ import {
StartStepSchema,
validateInput,
UpdateMetadataSchema,
SavePublishedNotebookHtmlSchema,
} from "@/app/api/v1/osograph/utils/validation";
import z from "zod";
import { logger } from "@/lib/logger";
import { Json } from "@/lib/types/supabase";
import { generatePublishedNotebookPath } from "@/lib/notebook/utils";
import { revalidateTag } from "next/cache";

type SystemMutationOptions<T extends z.ZodTypeAny, O> = {
inputSchema: T;
Expand All @@ -48,6 +51,7 @@ function systemMutation<T extends z.ZodTypeAny, O>({
if (!context.systemCredentials) {
throw AuthenticationErrors.notAuthorized();
}

const validatedInput = validateInput(inputSchema, args.input);
return resolver(validatedInput, context);
};
Expand Down Expand Up @@ -393,6 +397,76 @@ export const systemResolvers: GraphQLResolverModule<GraphQLContext> = {
};
},
}),
savePublishedNotebookHtml: systemMutation({
inputSchema: SavePublishedNotebookHtmlSchema,
resolver: async (input) => {
const supabase = createAdminClient();

const { notebookId, htmlContent } = input;

console.log("Encoded", htmlContent);

// Decode base64 content
const byteArray = Buffer.from(htmlContent, "base64");

const { data: notebook } = await supabase
.from("notebooks")
.select("org_id")
.eq("id", notebookId)
.single();
if (!notebook) {
throw ResourceErrors.notFound(`Notebook ${notebookId} not found`);
}
const filePath = generatePublishedNotebookPath(
notebookId,
notebook.org_id,
);
// Save the HTML content to Supabase Storage
const { data: uploadData, error: uploadError } = await supabase.storage
.from("published-notebooks")
.upload(filePath, byteArray, {
upsert: true,
contentType: "text/html",
headers: {
"Content-Encoding": "gzip",
},
// 5 Minute CDN cache. We will also cache on Vercel side to control it with revalidateTag
cacheControl: "300",
});
if (uploadError || !uploadData) {
throw ServerErrors.internal(
`Failed to upload published notebook HTML for notebook ${notebookId}: ${uploadError.message}`,
);
}

// Update the published_notebooks table with the new data path
const { data: publishedNotebook, error: upsertError } = await supabase
.from("published_notebooks")
.upsert(
{
notebook_id: notebookId,
data_path: filePath,
updated_at: new Date().toISOString(),
deleted_at: null,
},
{ onConflict: "notebook_id" },
)
.select("id")
.single();

if (upsertError || !publishedNotebook) {
throw ServerErrors.internal(
`Failed to update published_notebooks for notebook ${notebookId}: ${upsertError.message}`,
);
}

revalidateTag(publishedNotebook.id);
return {
message: "Saved published notebook HTML",
success: true,
};
},
}),
},
Query: {
system: async (_: unknown, _args: unknown, context: GraphQLContext) => {
Expand Down
5 changes: 5 additions & 0 deletions apps/frontend/app/api/v1/osograph/utils/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,11 @@ export const CreateMaterializationSchema = z.object({
schema: z.array(DataModelColumnSchema),
});

export const SavePublishedNotebookHtmlSchema = z.object({
notebookId: z.string().uuid(),
htmlContent: z.string(),
});

export const ResolveTablesSchema = z.object({
references: z.array(z.string()).min(1, "At least one reference is required"),
metadata: z
Expand Down
Loading