diff --git a/apps/frontend/app/api/v1/osograph/schema.graphql b/apps/frontend/app/api/v1/osograph/schema.graphql index 767945a98..efb2b60a7 100644 --- a/apps/frontend/app/api/v1/osograph/schema.graphql +++ b/apps/frontend/app/api/v1/osograph/schema.graphql @@ -175,6 +175,11 @@ type Query { """ osoApp_orgDatasets(orgName: String!): [Dataset!]! + """ + Get a specific dataset by name + """ + osoApp_dataset(orgName: String!, datasetName: String!): Dataset + osoApp_datasetTableMetadata( orgName: String! catalogName: String! @@ -240,6 +245,11 @@ type Mutation { Create a new dataset """ osoApp_createDataset(input: CreateDatasetInput!): CreateDatasetPayload! + + """ + Update a dataset + """ + osoApp_updateDataset(input: UpdateDatasetInput!): UpdateDatasetPayload! } input CreateDatasetInput { @@ -257,6 +267,20 @@ type CreateDatasetPayload { success: Boolean! } +input UpdateDatasetInput { + datasetId: ID! + name: String + displayName: String + description: String + isPublic: Boolean +} + +type UpdateDatasetPayload { + dataset: Dataset + message: String! + success: Boolean! +} + input CreateInvitationInput { """ Email address to invite diff --git a/apps/frontend/app/api/v1/osograph/schema/resolvers/dataset.ts b/apps/frontend/app/api/v1/osograph/schema/resolvers/dataset.ts index 0ccefb999..0934fcc94 100644 --- a/apps/frontend/app/api/v1/osograph/schema/resolvers/dataset.ts +++ b/apps/frontend/app/api/v1/osograph/schema/resolvers/dataset.ts @@ -6,6 +6,7 @@ import { type GraphQLContext, } from "@/app/api/v1/osograph/utils/auth"; import { + AuthenticationErrors, ResourceErrors, ServerErrors, } from "@/app/api/v1/osograph/utils/errors"; @@ -153,6 +154,34 @@ export const datasetResolver: GraphQLResolverModule = { return response; }, + osoApp_dataset: async ( + _: unknown, + { orgName, datasetName }: { orgName: string; datasetName: string }, + context: GraphQLContext, + ) => { + const authenticatedUser = requireAuthentication(context.user); + const organization = await getOrganizationByName(orgName); + await requireOrgMembership(authenticatedUser.userId, organization.id); + + const supabase = createAdminClient(); + const { data, error } = await supabase + .from("datasets") + .select("*, models:model(*)") + .eq("org_id", organization.id) + .eq("name", datasetName) + .is("deleted_at", null) + .single(); + + if (error) { + throw ResourceErrors.notFound("Dataset", `name: ${datasetName}`); + } + + return { + ...data, + tables: data.models.map((m) => ({ name: m.name })), + }; + }, + osoApp_datasetTableMetadata: async ( _: unknown, { @@ -273,6 +302,63 @@ export const datasetResolver: GraphQLResolverModule = { success: true, }; }, + osoApp_updateDataset: async ( + _: unknown, + { + datasetId, + name, + displayName, + description, + isPublic, + }: { + datasetId: string; + name?: string; + displayName?: string; + description?: string; + isPublic?: boolean; + }, + context: GraphQLContext, + ) => { + const authenticatedUser = requireAuthentication(context.user); + const supabase = createAdminClient(); + + const { data: existingDataset, error: existingError } = await supabase + .from("datasets") + .select("org_id") + .eq("id", datasetId) + .single(); + + if (existingError || !existingDataset) { + throw AuthenticationErrors.notAuthorized(); + } + + await requireOrgMembership( + authenticatedUser.userId, + existingDataset.org_id, + ); + + const { data, error } = await supabase + .from("datasets") + .update({ + name, + display_name: displayName, + description, + is_public: isPublic, + }) + .eq("id", datasetId) + .select() + .single(); + + if (error) { + throw ServerErrors.database("Failed to update dataset"); + } + + return { + dataset: data, + message: "Dataset updated successfully", + success: true, + }; + }, }, Dataset: { id: (parent: DatasetsRow) => parent.id, diff --git a/apps/frontend/lib/clients/oso-app/oso-app.ts b/apps/frontend/lib/clients/oso-app/oso-app.ts index 5be44acd5..ca8e4757b 100644 --- a/apps/frontend/lib/clients/oso-app/oso-app.ts +++ b/apps/frontend/lib/clients/oso-app/oso-app.ts @@ -2519,6 +2519,77 @@ class OsoAppClient { return payload.dataset; } + + async updateDataset( + args: Partial<{ + datasetId: string; + name: string; + displayName: string; + description: string; + isPublic: boolean; + }>, + ) { + const { datasetId, name, displayName, description, isPublic } = { + datasetId: ensure(args.datasetId, "Missing datasetId argument"), + name: args.name, + displayName: args.displayName, + description: args.description, + isPublic: args.isPublic, + }; + + const UPDATE_DATASET_MUTATION = gql(` + mutation UpdateDataset($input: UpdateDatasetInput!) { + osoApp_updateDataset(input: $input) { + success + message + dataset { + id + name + displayName + description + isPublic + } + } + } + `); + + const response = await fetch("/api/v1/osograph", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query: print(UPDATE_DATASET_MUTATION), + variables: { + input: { + datasetId, + name, + displayName, + description, + isPublic, + }, + }, + }), + }); + + const result = await response.json(); + + if (result.errors) { + logger.error("Failed to update dataset:", result.errors[0].message); + throw new Error(`Failed to update dataset: ${result.errors[0].message}`); + } + + const payload = result.data?.osoApp_updateDataset; + if (!payload) { + throw new Error("No response data from update dataset mutation"); + } + + if (payload.success) { + logger.log(`Successfully updated dataset "${displayName}"`); + } + + return payload.dataset; + } } export { OsoAppClient }; diff --git a/apps/frontend/lib/graphql/generated/gql.ts b/apps/frontend/lib/graphql/generated/gql.ts index a0664a3b2..d91cd9fe5 100644 --- a/apps/frontend/lib/graphql/generated/gql.ts +++ b/apps/frontend/lib/graphql/generated/gql.ts @@ -14,8 +14,9 @@ import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/ * Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size */ type Documents = { - "\n mutation CreateDataset($input: CreateDatasetInput!) {\n osoApp_createDataset(input: $input) {\n success\n message\n dataset {\n id\n name\n displayName\n description\n catalog\n schema\n datasetType\n isPublic\n }\n }\n }\n ": typeof types.CreateDatasetDocument; + "\n mutation UpdateDataset($input: UpdateDatasetInput!) {\n osoApp_updateDataset(input: $input) {\n success\n message\n dataset {\n id\n name\n displayName\n description\n isPublic\n }\n }\n }\n ": typeof types.UpdateDatasetDocument; "\n mutation SavePreview($input: SaveNotebookPreviewInput!) {\n osoApp_saveNotebookPreview(input: $input) {\n success\n message\n }\n }\n ": typeof types.SavePreviewDocument; + "\n mutation CreateDataset($input: CreateDatasetInput!) {\n osoApp_createDataset(input: $input) {\n success\n message\n dataset {\n id\n name\n displayName\n description\n catalog\n schema\n datasetType\n isPublic\n }\n }\n }\n ": typeof types.CreateDatasetDocument; "\nquery AssetGraph {\n assetNodes {\n assetKey {\n path\n }\n dependencyKeys {\n path\n }\n }\n}": typeof types.AssetGraphDocument; '\nquery AssetMaterializedData($assetKeys: [AssetKeyInput!] = {path: ""}) {\n assetNodes(assetKeys: $assetKeys) {\n assetKey {\n path\n }\n partitionStats {\n numFailed\n numMaterialized\n numMaterializing\n numPartitions\n }\n assetPartitionStatuses {\n ... on TimePartitionStatuses {\n __typename\n ranges {\n endKey\n startKey\n status\n }\n }\n }\n assetMaterializations(limit: 1) {\n runOrError {\n ... on Run {\n endTime\n }\n }\n }\n }\n}': typeof types.AssetMaterializedDataDocument; "\n query TimeseriesMetricsByArtifact(\n $artifactIds: [String!],\n $metricIds: [String!],\n $startDate: Oso_Date!,\n $endDate: Oso_Date!, \n ) {\n oso_timeseriesMetricsByArtifactV0(where: {\n artifactId: {_in: $artifactIds},\n metricId: {_in: $metricIds},\n sampleDate: { _gte: $startDate, _lte: $endDate }\n }) {\n amount\n artifactId\n metricId\n sampleDate\n unit\n }\n oso_artifactsV1(where: { artifactId: { _in: $artifactIds }}) {\n artifactId\n artifactSource\n artifactNamespace\n artifactName\n }\n oso_metricsV0(where: {metricId: {_in: $metricIds}}) {\n metricId\n metricSource\n metricNamespace\n metricName\n displayName\n description\n }\n }\n": typeof types.TimeseriesMetricsByArtifactDocument; @@ -23,10 +24,12 @@ type Documents = { "\n query TimeseriesMetricsByCollection(\n $collectionIds: [String!],\n $metricIds: [String!],\n $startDate: Oso_Date!,\n $endDate: Oso_Date!, \n ) {\n oso_timeseriesMetricsByCollectionV0(where: {\n collectionId: {_in: $collectionIds},\n metricId: {_in: $metricIds},\n sampleDate: { _gte: $startDate, _lte: $endDate }\n }) {\n amount\n metricId\n collectionId\n sampleDate\n unit\n }\n oso_collectionsV1(where: { collectionId: { _in: $collectionIds }}) {\n collectionId\n collectionSource\n collectionNamespace\n collectionName\n displayName\n description\n }\n oso_metricsV0(where: {metricId: {_in: $metricIds}}) {\n metricId\n metricSource\n metricNamespace\n metricName\n displayName\n description\n }\n }\n": typeof types.TimeseriesMetricsByCollectionDocument; }; const documents: Documents = { - "\n mutation CreateDataset($input: CreateDatasetInput!) {\n osoApp_createDataset(input: $input) {\n success\n message\n dataset {\n id\n name\n displayName\n description\n catalog\n schema\n datasetType\n isPublic\n }\n }\n }\n ": - types.CreateDatasetDocument, + "\n mutation UpdateDataset($input: UpdateDatasetInput!) {\n osoApp_updateDataset(input: $input) {\n success\n message\n dataset {\n id\n name\n displayName\n description\n isPublic\n }\n }\n }\n ": + types.UpdateDatasetDocument, "\n mutation SavePreview($input: SaveNotebookPreviewInput!) {\n osoApp_saveNotebookPreview(input: $input) {\n success\n message\n }\n }\n ": types.SavePreviewDocument, + "\n mutation CreateDataset($input: CreateDatasetInput!) {\n osoApp_createDataset(input: $input) {\n success\n message\n dataset {\n id\n name\n displayName\n description\n catalog\n schema\n datasetType\n isPublic\n }\n }\n }\n ": + types.CreateDatasetDocument, "\nquery AssetGraph {\n assetNodes {\n assetKey {\n path\n }\n dependencyKeys {\n path\n }\n }\n}": types.AssetGraphDocument, '\nquery AssetMaterializedData($assetKeys: [AssetKeyInput!] = {path: ""}) {\n assetNodes(assetKeys: $assetKeys) {\n assetKey {\n path\n }\n partitionStats {\n numFailed\n numMaterialized\n numMaterializing\n numPartitions\n }\n assetPartitionStatuses {\n ... on TimePartitionStatuses {\n __typename\n ranges {\n endKey\n startKey\n status\n }\n }\n }\n assetMaterializations(limit: 1) {\n runOrError {\n ... on Run {\n endTime\n }\n }\n }\n }\n}': @@ -57,14 +60,20 @@ export function gql(source: string): unknown; * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function gql( - source: "\n mutation CreateDataset($input: CreateDatasetInput!) {\n osoApp_createDataset(input: $input) {\n success\n message\n dataset {\n id\n name\n displayName\n description\n catalog\n schema\n datasetType\n isPublic\n }\n }\n }\n ", -): (typeof documents)["\n mutation CreateDataset($input: CreateDatasetInput!) {\n osoApp_createDataset(input: $input) {\n success\n message\n dataset {\n id\n name\n displayName\n description\n catalog\n schema\n datasetType\n isPublic\n }\n }\n }\n "]; + source: "\n mutation UpdateDataset($input: UpdateDatasetInput!) {\n osoApp_updateDataset(input: $input) {\n success\n message\n dataset {\n id\n name\n displayName\n description\n isPublic\n }\n }\n }\n ", +): (typeof documents)["\n mutation UpdateDataset($input: UpdateDatasetInput!) {\n osoApp_updateDataset(input: $input) {\n success\n message\n dataset {\n id\n name\n displayName\n description\n isPublic\n }\n }\n }\n "]; /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function gql( source: "\n mutation SavePreview($input: SaveNotebookPreviewInput!) {\n osoApp_saveNotebookPreview(input: $input) {\n success\n message\n }\n }\n ", ): (typeof documents)["\n mutation SavePreview($input: SaveNotebookPreviewInput!) {\n osoApp_saveNotebookPreview(input: $input) {\n success\n message\n }\n }\n "]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function gql( + source: "\n mutation CreateDataset($input: CreateDatasetInput!) {\n osoApp_createDataset(input: $input) {\n success\n message\n dataset {\n id\n name\n displayName\n description\n catalog\n schema\n datasetType\n isPublic\n }\n }\n }\n ", +): (typeof documents)["\n mutation CreateDataset($input: CreateDatasetInput!) {\n osoApp_createDataset(input: $input) {\n success\n message\n dataset {\n id\n name\n displayName\n description\n catalog\n schema\n datasetType\n isPublic\n }\n }\n }\n "]; /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/apps/frontend/lib/graphql/generated/graphql.ts b/apps/frontend/lib/graphql/generated/graphql.ts index 204184a6d..823218e9d 100644 --- a/apps/frontend/lib/graphql/generated/graphql.ts +++ b/apps/frontend/lib/graphql/generated/graphql.ts @@ -2927,6 +2927,8 @@ export type Mutation = { osoApp_revokeInvitation: RevokeInvitationPayload; /** Save notebook preview PNG image */ osoApp_saveNotebookPreview: SaveNotebookPreviewPayload; + /** Update a dataset */ + osoApp_updateDataset: UpdateDatasetPayload; /** Update a member's role in an organization */ osoApp_updateMemberRole: UpdateMemberRolePayload; /** Update the current user's profile */ @@ -3100,6 +3102,11 @@ export type MutationOsoApp_SaveNotebookPreviewArgs = { input: SaveNotebookPreviewInput; }; +/** The root for all mutations to modify data in your Dagster instance. */ +export type MutationOsoApp_UpdateDatasetArgs = { + input: UpdateDatasetInput; +}; + /** The root for all mutations to modify data in your Dagster instance. */ export type MutationOsoApp_UpdateMemberRoleArgs = { orgName: Scalars["String"]["input"]; @@ -5325,6 +5332,8 @@ export type Query = { locationStatusesOrError: WorkspaceLocationStatusEntriesOrError; /** Retrieve event logs after applying a run id filter, cursor, and limit. */ logsForRun: EventConnectionOrError; + /** Get a specific dataset by name */ + osoApp_dataset?: Maybe; osoApp_datasetTableMetadata: Array; /** Get a specific invitation by ID */ osoApp_invitation?: Maybe; @@ -5614,6 +5623,12 @@ export type QueryLogsForRunArgs = { runId: Scalars["ID"]["input"]; }; +/** The root for all queries to retrieve data from the Dagster instance. */ +export type QueryOsoApp_DatasetArgs = { + datasetName: Scalars["String"]["input"]; + orgName: Scalars["String"]["input"]; +}; + /** The root for all queries to retrieve data from the Dagster instance. */ export type QueryOsoApp_DatasetTableMetadataArgs = { catalogName: Scalars["String"]["input"]; @@ -7654,6 +7669,21 @@ export type UnsupportedOperationError = Error & { message: Scalars["String"]["output"]; }; +export type UpdateDatasetInput = { + datasetId: Scalars["ID"]["input"]; + description?: InputMaybe; + displayName?: InputMaybe; + isPublic?: InputMaybe; + name?: InputMaybe; +}; + +export type UpdateDatasetPayload = { + __typename?: "UpdateDatasetPayload"; + dataset?: Maybe; + message: Scalars["String"]["output"]; + success: Scalars["Boolean"]["output"]; +}; + export type UpdateMemberRolePayload = { __typename?: "UpdateMemberRolePayload"; member?: Maybe; @@ -7794,14 +7824,14 @@ export enum Link__Purpose { Security = "SECURITY", } -export type CreateDatasetMutationVariables = Exact<{ - input: CreateDatasetInput; +export type UpdateDatasetMutationVariables = Exact<{ + input: UpdateDatasetInput; }>; -export type CreateDatasetMutation = { +export type UpdateDatasetMutation = { __typename?: "Mutation"; - osoApp_createDataset: { - __typename?: "CreateDatasetPayload"; + osoApp_updateDataset: { + __typename?: "UpdateDatasetPayload"; success: boolean; message: string; dataset?: { @@ -7810,9 +7840,6 @@ export type CreateDatasetMutation = { name: string; displayName: string; description?: string | null; - catalog: string; - schema: string; - datasetType: DatasetType; isPublic: boolean; } | null; }; @@ -7831,6 +7858,30 @@ export type SavePreviewMutation = { }; }; +export type CreateDatasetMutationVariables = Exact<{ + input: CreateDatasetInput; +}>; + +export type CreateDatasetMutation = { + __typename?: "Mutation"; + osoApp_createDataset: { + __typename?: "CreateDatasetPayload"; + success: boolean; + message: string; + dataset?: { + __typename?: "Dataset"; + id: string; + name: string; + displayName: string; + description?: string | null; + catalog: string; + schema: string; + datasetType: DatasetType; + isPublic: boolean; + } | null; + }; +}; + export type AssetGraphQueryVariables = Exact<{ [key: string]: never }>; export type AssetGraphQuery = { @@ -8001,13 +8052,13 @@ export type TimeseriesMetricsByCollectionQuery = { }> | null; }; -export const CreateDatasetDocument = { +export const UpdateDatasetDocument = { kind: "Document", definitions: [ { kind: "OperationDefinition", operation: "mutation", - name: { kind: "Name", value: "CreateDataset" }, + name: { kind: "Name", value: "UpdateDataset" }, variableDefinitions: [ { kind: "VariableDefinition", @@ -8019,7 +8070,7 @@ export const CreateDatasetDocument = { kind: "NonNullType", type: { kind: "NamedType", - name: { kind: "Name", value: "CreateDatasetInput" }, + name: { kind: "Name", value: "UpdateDatasetInput" }, }, }, }, @@ -8029,7 +8080,7 @@ export const CreateDatasetDocument = { selections: [ { kind: "Field", - name: { kind: "Name", value: "osoApp_createDataset" }, + name: { kind: "Name", value: "osoApp_updateDataset" }, arguments: [ { kind: "Argument", @@ -8061,18 +8112,6 @@ export const CreateDatasetDocument = { kind: "Field", name: { kind: "Name", value: "description" }, }, - { - kind: "Field", - name: { kind: "Name", value: "catalog" }, - }, - { - kind: "Field", - name: { kind: "Name", value: "schema" }, - }, - { - kind: "Field", - name: { kind: "Name", value: "datasetType" }, - }, { kind: "Field", name: { kind: "Name", value: "isPublic" }, @@ -8088,8 +8127,8 @@ export const CreateDatasetDocument = { }, ], } as unknown as DocumentNode< - CreateDatasetMutation, - CreateDatasetMutationVariables + UpdateDatasetMutation, + UpdateDatasetMutationVariables >; export const SavePreviewDocument = { kind: "Document", @@ -8143,6 +8182,96 @@ export const SavePreviewDocument = { }, ], } as unknown as DocumentNode; +export const CreateDatasetDocument = { + kind: "Document", + definitions: [ + { + kind: "OperationDefinition", + operation: "mutation", + name: { kind: "Name", value: "CreateDataset" }, + variableDefinitions: [ + { + kind: "VariableDefinition", + variable: { + kind: "Variable", + name: { kind: "Name", value: "input" }, + }, + type: { + kind: "NonNullType", + type: { + kind: "NamedType", + name: { kind: "Name", value: "CreateDatasetInput" }, + }, + }, + }, + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "osoApp_createDataset" }, + arguments: [ + { + kind: "Argument", + name: { kind: "Name", value: "input" }, + value: { + kind: "Variable", + name: { kind: "Name", value: "input" }, + }, + }, + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "success" } }, + { kind: "Field", name: { kind: "Name", value: "message" } }, + { + kind: "Field", + name: { kind: "Name", value: "dataset" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "id" } }, + { kind: "Field", name: { kind: "Name", value: "name" } }, + { + kind: "Field", + name: { kind: "Name", value: "displayName" }, + }, + { + kind: "Field", + name: { kind: "Name", value: "description" }, + }, + { + kind: "Field", + name: { kind: "Name", value: "catalog" }, + }, + { + kind: "Field", + name: { kind: "Name", value: "schema" }, + }, + { + kind: "Field", + name: { kind: "Name", value: "datasetType" }, + }, + { + kind: "Field", + name: { kind: "Name", value: "isPublic" }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode< + CreateDatasetMutation, + CreateDatasetMutationVariables +>; export const AssetGraphDocument = { kind: "Document", definitions: [