diff --git a/.changeset/nonzero-ontology-versions.md b/.changeset/nonzero-ontology-versions.md new file mode 100644 index 00000000000..348767273dc --- /dev/null +++ b/.changeset/nonzero-ontology-versions.md @@ -0,0 +1,5 @@ +--- +"@blockprotocol/type-system": patch +--- + +Reject zero major versions in versioned URLs. Require ontology version constructors to receive integers between 1 and 4294967295. diff --git a/apps/hash-frontend/src/pages/@/[shortname]/types/data-type/[...slug-maybe-version].page.test.tsx b/apps/hash-frontend/src/pages/@/[shortname]/types/data-type/[...slug-maybe-version].page.test.tsx new file mode 100644 index 00000000000..aa7b87eff36 --- /dev/null +++ b/apps/hash-frontend/src/pages/@/[shortname]/types/data-type/[...slug-maybe-version].page.test.tsx @@ -0,0 +1,63 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, test, vi } from "vitest"; + +import Page from "./[...slug-maybe-version].page"; + +const router = vi.hoisted(() => ({ + query: {} as Record, + asPath: "", +})); + +vi.mock("next/router", () => ({ useRouter: () => router })); +vi.mock("../../../../../shared/layout", () => ({ + getLayoutWithSidebar: () => null, +})); +vi.mock("../../../../shared/not-found", () => ({ + NotFound: () => "not found", +})); +vi.mock("../../../../shared/data-type", () => ({ + DataType: ({ requestedVersion }: { requestedVersion: string | null }) => + requestedVersion ?? "latest", +})); +vi.mock("../../../../../shared/generate-link-parameters", () => ({ + generateLinkParameters: () => ({ href: "/" }), +})); +vi.mock("../../shared/use-route-namespace", () => ({ + useRouteNamespace: () => ({ + loading: false, + routeNamespace: { webId: "alice" }, + }), +})); +vi.mock("../shared/get-type-base-url", () => ({ + getTypeBaseUrl: () => "http://example.com/types/data-type/test/", +})); + +const renderVersion = (version?: string) => { + router.asPath = `/@alice/types/data-type/test${version === undefined ? "" : `/v/${version}`}`; + return renderToStaticMarkup(); +}; + +describe("data type route versions", () => { + test.each(["0", "4294967296", "-1", "1.5", "invalid", "1invalid"])( + "renders not found for version %s", + (version) => { + expect(renderVersion(version)).toBe("not found"); + }, + ); + + test.each(["1", "4294967295"])("renders version %s", (version) => { + expect(renderVersion(version)).toContain(version); + }); + + test.each(["1?tab=definition", "1#definition"])( + "renders a valid version with a query or fragment: %s", + (version) => { + expect(renderVersion(version)).toContain("1"); + expect(renderVersion(version)).not.toContain("not found"); + }, + ); + + test("renders the latest version when no version is requested", () => { + expect(renderVersion()).toContain("latest"); + }); +}); diff --git a/apps/hash-frontend/src/pages/@/[shortname]/types/data-type/[...slug-maybe-version].page.tsx b/apps/hash-frontend/src/pages/@/[shortname]/types/data-type/[...slug-maybe-version].page.tsx index 418b6fae392..21047500f26 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/types/data-type/[...slug-maybe-version].page.tsx +++ b/apps/hash-frontend/src/pages/@/[shortname]/types/data-type/[...slug-maybe-version].page.tsx @@ -7,11 +7,13 @@ import { componentsFromVersionedUrl, currentTimestamp, parseOntologyTypeVersion, + validateVersionedUrl, } from "@blockprotocol/type-system"; import { generateLinkParameters } from "../../../../../shared/generate-link-parameters"; import { getLayoutWithSidebar } from "../../../../../shared/layout"; import { DataType } from "../../../../shared/data-type"; +import { NotFound } from "../../../../shared/not-found"; import { useRouteNamespace } from "../../shared/use-route-namespace"; import { getTypeBaseUrl } from "../shared/get-type-base-url"; @@ -45,7 +47,7 @@ const Page: NextPageWithLayout = () => { slug, _v, requestedVersionString, - ] = router.asPath.split("/") as [ + ] = router.asPath.replace(/[?#].*$/, "").split("/") as [ "", `@${string}`, "types", @@ -109,6 +111,14 @@ const Page: NextPageWithLayout = () => { } }, [router.query.draft]); + if ( + requestedVersionString && + validateVersionedUrl(`${dataTypeBaseUrl}v/${requestedVersionString}`) + .type === "Err" + ) { + return ; + } + const requestedVersion = requestedVersionString ? parseOntologyTypeVersion(requestedVersionString) : null; diff --git a/apps/hash-frontend/src/pages/@/[shortname]/types/entity-type/[...slug-maybe-version].page.tsx b/apps/hash-frontend/src/pages/@/[shortname]/types/entity-type/[...slug-maybe-version].page.tsx index b297152f22f..c17df2347eb 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/types/entity-type/[...slug-maybe-version].page.tsx +++ b/apps/hash-frontend/src/pages/@/[shortname]/types/entity-type/[...slug-maybe-version].page.tsx @@ -7,10 +7,12 @@ import { componentsFromVersionedUrl, currentTimestamp, parseOntologyTypeVersion, + validateVersionedUrl, } from "@blockprotocol/type-system"; import { getLayoutWithSidebar } from "../../../../../shared/layout"; import { EntityType } from "../../../../shared/entity-type"; +import { NotFound } from "../../../../shared/not-found"; import { useRouteNamespace } from "../../shared/use-route-namespace"; import { getTypeBaseUrl } from "../shared/get-type-base-url"; @@ -44,7 +46,7 @@ const Page: NextPageWithLayout = () => { slug, _v, requestedVersionString, - ] = router.asPath.split("/") as [ + ] = router.asPath.replace(/[?#].*$/, "").split("/") as [ "", `@${string}`, "types", @@ -107,6 +109,14 @@ const Page: NextPageWithLayout = () => { } }, [router.query.draft]); + if ( + requestedVersionString && + validateVersionedUrl(`${entityTypeBaseUrl}v/${requestedVersionString}`) + .type === "Err" + ) { + return ; + } + const requestedVersion = requestedVersionString ? parseOntologyTypeVersion(requestedVersionString) : null; diff --git a/apps/hash-frontend/src/pages/shared/data-type.tsx b/apps/hash-frontend/src/pages/shared/data-type.tsx index dd711d9cca3..9724150bc91 100644 --- a/apps/hash-frontend/src/pages/shared/data-type.tsx +++ b/apps/hash-frontend/src/pages/shared/data-type.tsx @@ -10,7 +10,6 @@ import { getRoots } from "@blockprotocol/graph/stdlib"; import { compareOntologyTypeVersions, extractVersion, - makeOntologyTypeVersion, versionedUrlFromComponents, } from "@blockprotocol/type-system"; import { deserializeQueryDataTypeSubgraphResponse } from "@local/hash-graph-sdk/data-type"; @@ -331,9 +330,7 @@ export const DataType = ({ throw new Error("Cannot render data type without data type"); } - const currentVersion = draftNewDataType - ? makeOntologyTypeVersion({ major: 0 }) - : extractVersion(dataType.schema.$id); + const currentVersion = extractVersion(dataType.schema.$id); const isLatest = !requestedVersion || requestedVersion === latestVersion; @@ -374,6 +371,7 @@ export const DataType = ({ {!isReadOnly && ( ({ + dirtyFields: {} as Record, + isSubmitting: false, +})); + +const confirmButton = vi.hoisted(() => ({ disabled: false })); + +vi.mock("@hashintel/type-editor", () => ({ + useEntityTypeFormState: () => formState, +})); + +vi.mock("@hashintel/design-system", () => ({ + FontAwesomeIcon: () => null, +})); + +vi.mock("../../../shared/icons/svg", () => ({ + PencilSimpleLine: () => null, +})); + +vi.mock("../shared/edit-bar-contents", () => ({ + EditBarCollapse: ({ + children, + in: visible, + }: { + children: ReactNode; + in: boolean; + }) => (visible ? children : null), + EditBarContainer: ({ children }: { children: ReactNode }) => children, + EditBarContents: ({ + label, + confirmButtonProps, + discardButtonProps, + }: { + label: string; + confirmButtonProps: { children: ReactNode; disabled: boolean }; + discardButtonProps: { children: ReactNode }; + }) => { + confirmButton.disabled = confirmButtonProps.disabled; + return ( + <> + {label} {confirmButtonProps.children} {discardButtonProps.children} + + ); + }, + useFreezeScrollWhileTransitioning: () => null, +})); + +const renderEditor = (isDraft: boolean, major = 1) => + renderToStaticMarkup( + , + ); + +describe("EditBarTypeEditor", () => { + beforeEach(() => { + formState.dirtyFields = {}; + confirmButton.disabled = false; + }); + + it("shows creation actions for a pristine draft at version one", () => { + const markup = renderEditor(true); + + expect(markup).toContain("this type has not yet been created"); + expect(markup).toContain("Create"); + expect(markup).toContain("Discard this type"); + }); + + it("shows update actions for an edited existing type at version one", () => { + formState.dirtyFields = { title: true }; + + const markup = renderEditor(false); + + expect(markup).toContain("Version 1 -> 2"); + expect(markup).toContain("Publish update"); + expect(markup).toContain("Discard changes"); + expect(markup).not.toContain("this type has not yet been created"); + }); + + it("hides the edit bar for a pristine existing type", () => { + expect(renderEditor(false)).toBe(""); + }); + + it("renders a pristine existing type at the maximum version", () => { + expect(renderEditor(false, 4_294_967_295)).toBe(""); + }); + + it("disables publishing an update at the maximum version", () => { + formState.dirtyFields = { title: true }; + + expect(renderEditor(false, 4_294_967_295)).toContain( + "this type has reached the maximum version and cannot be updated", + ); + expect(confirmButton.disabled).toBe(true); + }); + + it("allows publishing the last available version", () => { + formState.dirtyFields = { title: true }; + + expect(renderEditor(false, 4_294_967_294)).toContain( + "Version 4294967294 -> 4294967295", + ); + expect(confirmButton.disabled).toBe(false); + }); +}); diff --git a/apps/hash-frontend/src/pages/shared/entity-type-page/edit-bar-type-editor.tsx b/apps/hash-frontend/src/pages/shared/entity-type-page/edit-bar-type-editor.tsx index ff80b728950..7b51219bfc2 100644 --- a/apps/hash-frontend/src/pages/shared/entity-type-page/edit-bar-type-editor.tsx +++ b/apps/hash-frontend/src/pages/shared/entity-type-page/edit-bar-type-editor.tsx @@ -3,7 +3,6 @@ import { useState } from "react"; import { incrementOntologyTypeVersion, - makeOntologyTypeVersion, type OntologyTypeVersion, } from "@blockprotocol/type-system"; import { FontAwesomeIcon } from "@hashintel/design-system"; @@ -20,7 +19,9 @@ import { import type { ButtonProps } from "../../../shared/ui/button"; import type { EntityTypeEditorFormData } from "@hashintel/type-editor"; -const useFrozenValue = (value: T): T => { +const useFrozenValue = ( + value: T, +): T => { const { dirtyFields } = useEntityTypeFormState(); const [frozen, setFrozen] = useState(value); @@ -35,32 +36,38 @@ const useFrozenValue = (value: T): T => { export const EditBarTypeEditor = ({ gentleErrorStyling, currentVersion, + isDraft, discardButtonProps, errorMessage, }: { gentleErrorStyling: boolean; currentVersion: OntologyTypeVersion; + isDraft: boolean; discardButtonProps: Partial; errorMessage?: string; }) => { const { dirtyFields, isSubmitting } = useEntityTypeFormState(); const frozenVersion = useFrozenValue(currentVersion); + const frozenIsDraft = useFrozenValue(isDraft); const ref = useFreezeScrollWhileTransitioning(); - const zeroVersion = makeOntologyTypeVersion({ major: 0 }); - const collapseIn = - currentVersion === zeroVersion || Object.keys(dirtyFields).length > 0; + const collapseIn = isDraft || Object.keys(dirtyFields).length > 0; const frozenDiscardButtonProps = useFrozenValue(discardButtonProps); const frozenSubmitting = useFrozenValue(isSubmitting); + const isVersionExhausted = + !frozenIsDraft && + Number.parseInt(frozenVersion.toString(), 10) === 4_294_967_295; let label; if (errorMessage) { label = `before saving${errorMessage ? `: ${errorMessage}` : ""}`; - } else if (frozenVersion === zeroVersion) { + } else if (frozenIsDraft) { label = "– this type has not yet been created"; + } else if (isVersionExhausted) { + label = "– this type has reached the maximum version and cannot be updated"; } else { label = `Version ${frozenVersion.toString()} -> ${incrementOntologyTypeVersion(frozenVersion).toString()}`; } @@ -74,7 +81,7 @@ export const EditBarTypeEditor = ({ ) : ( @@ -83,10 +90,7 @@ export const EditBarTypeEditor = ({ title={errorMessage ? "Changes required" : "Currently editing"} label={label} discardButtonProps={{ - children: - frozenVersion === zeroVersion - ? "Discard this type" - : "Discard changes", + children: frozenIsDraft ? "Discard this type" : "Discard changes", disabled: frozenSubmitting, sx: errorMessage ? ({ palette }) => ({ @@ -104,10 +108,9 @@ export const EditBarTypeEditor = ({ ...frozenDiscardButtonProps, }} confirmButtonProps={{ - children: - frozenVersion === zeroVersion ? "Create" : "Publish update", + children: frozenIsDraft ? "Create" : "Publish update", loading: frozenSubmitting, - disabled: frozenSubmitting, + disabled: frozenSubmitting || isVersionExhausted, }} /> diff --git a/apps/hash-frontend/src/pages/shared/entity-type-page/shared/entity-type-header.tsx b/apps/hash-frontend/src/pages/shared/entity-type-page/shared/entity-type-header.tsx index 39e9598e13a..b21a7d3403e 100644 --- a/apps/hash-frontend/src/pages/shared/entity-type-page/shared/entity-type-header.tsx +++ b/apps/hash-frontend/src/pages/shared/entity-type-page/shared/entity-type-header.tsx @@ -5,7 +5,6 @@ import { Controller } from "react-hook-form"; import { extractBaseUrl, extractVersion, - makeOntologyTypeVersion, versionedUrlFromComponents, } from "@blockprotocol/type-system"; import { @@ -65,7 +64,7 @@ export const EntityTypeHeader = ({ const latestVersionUrl = versionedUrlFromComponents( extractBaseUrl(entityTypeSchema.$id), - latestVersion ?? makeOntologyTypeVersion({ major: 0 }), + latestVersion ?? currentVersion, ); const { control } = useEntityTypeFormContext(); diff --git a/apps/hash-frontend/src/pages/shared/entity-type.tsx b/apps/hash-frontend/src/pages/shared/entity-type.tsx index 8abc2befc11..10dd7bd278a 100644 --- a/apps/hash-frontend/src/pages/shared/entity-type.tsx +++ b/apps/hash-frontend/src/pages/shared/entity-type.tsx @@ -5,7 +5,6 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { atLeastOne, extractVersion, - makeOntologyTypeVersion, mustHaveAtLeastOne, } from "@blockprotocol/type-system"; import { EntityOrTypeIcon } from "@hashintel/design-system"; @@ -374,9 +373,7 @@ export const EntityType = ({ } } - const currentVersion = draftEntityType - ? makeOntologyTypeVersion({ major: 0 }) - : extractVersion(entityType.schema.$id); + const currentVersion = extractVersion(entityType.schema.$id); const convertToLinkType = wrapHandleSubmit(async (data) => { if (upgradableDependencies.length && !showDependencyUpgradeModal) { @@ -543,6 +540,7 @@ export const EntityType = ({ {!isReadonly && ( { const dataTypeBaseUrl = atob(base64EncodedBaseUrl) as BaseUrl; + if ( + requestedVersionString && + validateVersionedUrl(`${dataTypeBaseUrl}v/${requestedVersionString}`) + .type === "Err" + ) { + return ; + } + const requestedVersion = requestedVersionString ? parseOntologyTypeVersion(requestedVersionString) : null; diff --git a/apps/hash-frontend/src/pages/types/external/entity-type/[...base64-baseurl-maybe-version].page.test.tsx b/apps/hash-frontend/src/pages/types/external/entity-type/[...base64-baseurl-maybe-version].page.test.tsx new file mode 100644 index 00000000000..d9a1d89f350 --- /dev/null +++ b/apps/hash-frontend/src/pages/types/external/entity-type/[...base64-baseurl-maybe-version].page.test.tsx @@ -0,0 +1,46 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, test, vi } from "vitest"; + +import Page from "./[...base64-baseurl-maybe-version].page"; + +const router = vi.hoisted(() => ({ + query: {} as Record, + asPath: "", +})); + +vi.mock("next/router", () => ({ useRouter: () => router })); +vi.mock("../../../../shared/layout", () => ({ + getLayoutWithSidebar: () => null, +})); +vi.mock("../../../shared/not-found", () => ({ NotFound: () => "not found" })); +vi.mock("../../../shared/entity-type", () => ({ + EntityType: ({ requestedVersion }: { requestedVersion: string | null }) => + requestedVersion ?? "latest", +})); + +const renderVersion = (version?: string) => { + router.query = { + "base64-baseurl-maybe-version": [ + btoa("http://example.com/types/entity-type/test/"), + ...(version === undefined ? [] : ["v", version]), + ], + }; + return renderToStaticMarkup(); +}; + +describe("entity type route versions", () => { + test.each(["0", "4294967296", "-1", "1.5", "invalid", "1invalid"])( + "renders not found for version %s", + (version) => { + expect(renderVersion(version)).toBe("not found"); + }, + ); + + test.each(["1", "4294967295"])("renders version %s", (version) => { + expect(renderVersion(version)).toContain(version); + }); + + test("renders the latest version when no version is requested", () => { + expect(renderVersion()).toContain("latest"); + }); +}); diff --git a/apps/hash-frontend/src/pages/types/external/entity-type/[...base64-baseurl-maybe-version].page.tsx b/apps/hash-frontend/src/pages/types/external/entity-type/[...base64-baseurl-maybe-version].page.tsx index 56020eb2dcd..c816859ae47 100644 --- a/apps/hash-frontend/src/pages/types/external/entity-type/[...base64-baseurl-maybe-version].page.tsx +++ b/apps/hash-frontend/src/pages/types/external/entity-type/[...base64-baseurl-maybe-version].page.tsx @@ -1,9 +1,13 @@ import { useRouter } from "next/router"; -import { parseOntologyTypeVersion } from "@blockprotocol/type-system"; +import { + parseOntologyTypeVersion, + validateVersionedUrl, +} from "@blockprotocol/type-system"; import { getLayoutWithSidebar } from "../../../../shared/layout"; import { EntityType } from "../../../shared/entity-type"; +import { NotFound } from "../../../shared/not-found"; import type { NextPageWithLayout } from "../../../../shared/layout"; import type { BaseUrl } from "@blockprotocol/type-system"; @@ -17,6 +21,14 @@ const Page: NextPageWithLayout = () => { const entityTypeBaseUrl = atob(base64EncodedBaseUrl) as BaseUrl; + if ( + requestedVersionString && + validateVersionedUrl(`${entityTypeBaseUrl}v/${requestedVersionString}`) + .type === "Err" + ) { + return ; + } + const requestedVersion = requestedVersionString ? parseOntologyTypeVersion(requestedVersionString) : null; diff --git a/libs/@blockprotocol/type-system/rust/src/ontology/id/error.rs b/libs/@blockprotocol/type-system/rust/src/ontology/id/error.rs index 55a52d03f33..7dd1c9e28f5 100644 --- a/libs/@blockprotocol/type-system/rust/src/ontology/id/error.rs +++ b/libs/@blockprotocol/type-system/rust/src/ontology/id/error.rs @@ -3,6 +3,14 @@ use thiserror::Error; #[cfg(target_arch = "wasm32")] use tsify::Tsify; +#[derive(Debug, Copy, Clone, PartialEq, Eq, derive_more::Display, derive_more::Error)] +pub enum OntologyTypeMajorVersionError { + #[display("Type version cannot be incremented beyond {}", u32::MAX)] + Overflow, + #[display("Type version cannot be decremented below 1")] + Underflow, +} + // TODO: Use error-stack // see https://linear.app/hash/issue/BE-160/simplify-error-handling-in-type-system-package #[cfg_attr(target_arch = "wasm32", derive(Tsify))] diff --git a/libs/@blockprotocol/type-system/rust/src/ontology/id/mod.rs b/libs/@blockprotocol/type-system/rust/src/ontology/id/mod.rs index 098d5a0fb25..70869710ba6 100644 --- a/libs/@blockprotocol/type-system/rust/src/ontology/id/mod.rs +++ b/libs/@blockprotocol/type-system/rust/src/ontology/id/mod.rs @@ -33,11 +33,18 @@ #[cfg(feature = "postgres")] use core::error::Error; -use core::{cmp, fmt, num::IntErrorKind, str::FromStr}; +use core::{ + cmp, fmt, + num::{IntErrorKind, NonZero, ParseIntError}, + str::FromStr, +}; #[cfg(feature = "postgres")] use bytes::BytesMut; -pub use error::{ParseBaseUrlError, ParseDraftInfoError, ParseVersionedUrlError}; +pub use error::{ + OntologyTypeMajorVersionError, ParseBaseUrlError, ParseDraftInfoError, ParseVersionedUrlError, +}; +use error_stack::Report; #[cfg(feature = "postgres")] use postgres_types::{FromSql, IsNull, ToSql, Type}; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; @@ -226,6 +233,101 @@ impl<'a> FromSql<'a> for BaseUrl { } } +/// A major version between 1 and [`u32::MAX`]. +#[derive( + Debug, + Copy, + Clone, + Hash, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + derive_more::Display, + derive_more::From, +)] +#[serde(transparent)] +pub struct OntologyTypeMajorVersion(NonZero); + +impl OntologyTypeMajorVersion { + pub const MAX: Self = Self(NonZero::::MAX); + pub const MIN: Self = Self(NonZero::::MIN); + + #[must_use] + pub const fn new(value: u32) -> Option { + match NonZero::new(value) { + Some(value) => Some(Self(value)), + None => None, + } + } + + #[must_use] + pub const fn get(self) -> u32 { + self.0.get() + } + + /// Parses a major version in the given radix. + /// + /// # Errors + /// + /// Returns an error if the input is not an integer between 1 and [`u32::MAX`]. + /// + /// # Panics + /// + /// Panics if `radix` is outside the range 2 to 36. + pub fn from_str_radix(value: &str, radix: u32) -> Result { + NonZero::::from_str_radix(value, radix).map(Self) + } + + /// # Errors + /// + /// Returns [`OntologyTypeMajorVersionError::Overflow`] at [`Self::MAX`]. + pub fn next(self) -> Result> { + self.0 + .checked_add(1) + .map(Self) + .ok_or_else(|| Report::new(OntologyTypeMajorVersionError::Overflow)) + } + + /// # Errors + /// + /// Returns [`OntologyTypeMajorVersionError::Underflow`] at [`Self::MIN`]. + pub fn previous(self) -> Result> { + self.get() + .checked_sub(1) + .and_then(Self::new) + .ok_or_else(|| Report::new(OntologyTypeMajorVersionError::Underflow)) + } +} + +#[cfg(feature = "postgres")] +impl ToSql for OntologyTypeMajorVersion { + postgres_types::accepts!(INT8); + + postgres_types::to_sql_checked!(); + + fn to_sql( + &self, + ty: &Type, + out: &mut BytesMut, + ) -> Result> { + i64::from(self.get()).to_sql(ty, out) + } +} + +#[cfg(feature = "postgres")] +impl<'a> FromSql<'a> for OntologyTypeMajorVersion { + postgres_types::accepts!(INT8); + + fn from_sql(ty: &Type, raw: &'a [u8]) -> Result> { + Ok(Self(NonZero::try_from(u32::try_from(i64::from_sql( + ty, raw, + )?)?)?)) + } +} + /// Pre-release version information for an ontology type. /// /// Represents different pre-release stages following semantic versioning conventions. @@ -335,12 +437,12 @@ impl FromStr for PreRelease { /// /// // Published version /// let v1 = OntologyTypeVersion::from_str("1").unwrap(); -/// assert_eq!(v1.major, 1); +/// assert_eq!(v1.major.get(), 1); /// assert!(v1.pre_release.is_none()); /// /// // Draft version /// let draft = OntologyTypeVersion::from_str("2-draft.lane123.5").unwrap(); -/// assert_eq!(draft.major, 2); +/// assert_eq!(draft.major.get(), 2); /// assert!(matches!(draft.pre_release, Some(PreRelease::Draft { .. }))); /// /// // Ordering: published > draft (same major) @@ -354,7 +456,7 @@ pub struct OntologyTypeVersion { // We don't really have a way to inform specta that this type is a string so we fake the type // to be a transparent type with only a single string type #[cfg_attr(feature = "codegen", specta(type = String))] - pub major: u32, + pub major: OntologyTypeMajorVersion, #[cfg_attr(feature = "codegen", specta(skip))] pub pre_release: Option, } @@ -395,7 +497,7 @@ impl FromStr for OntologyTypeVersion { }; Ok(Self { - major: u32::from_str(version).map_err(|error| { + major: OntologyTypeMajorVersion::from_str_radix(version, 10).map_err(|error| { if *error.kind() == IntErrorKind::Empty { ParseOntologyTypeVersionError::MissingVersion } else { @@ -472,7 +574,7 @@ impl ToSql for OntologyTypeVersion { if self.pre_release.is_some() { todo!("https://linear.app/hash/issue/BE-161/allow-ids-for-pre-release-type-to-be-stored-in-postgres"); } - i64::from(self.major).to_sql(ty, out) + self.major.to_sql(ty, out) } } @@ -482,7 +584,7 @@ impl<'a> FromSql<'a> for OntologyTypeVersion { fn from_sql(ty: &Type, raw: &'a [u8]) -> Result> { Ok(Self { - major: i64::from_sql(ty, raw)?.try_into()?, + major: OntologyTypeMajorVersion::from_sql(ty, raw)?, pre_release: None, }) } @@ -864,7 +966,7 @@ mod tests { assert_eq!( record_id.version, OntologyTypeVersion { - major: 3, + major: OntologyTypeMajorVersion::new(3).expect("version should be nonzero"), pre_release: None } ); @@ -933,17 +1035,17 @@ mod tests { fn ontology_version_parsing() -> Result<(), Box> { // Test published versions let v1 = OntologyTypeVersion::from_str("1")?; - assert_eq!(v1.major, 1); + assert_eq!(v1.major.get(), 1); assert_eq!(v1.pre_release, None); assert_eq!(v1.to_string(), "1"); let v42 = OntologyTypeVersion::from_str("42")?; - assert_eq!(v42.major, 42); + assert_eq!(v42.major.get(), 42); assert_eq!(v42.pre_release, None); // Test draft versions let draft = OntologyTypeVersion::from_str("2-draft.abcd1234.5")?; - assert_eq!(draft.major, 2); + assert_eq!(draft.major.get(), 2); let Some(PreRelease::Draft { lane, revision }) = draft.pre_release.as_ref() else { panic!("draft should have pre-release information"); }; @@ -954,6 +1056,62 @@ mod tests { Ok(()) } + #[test] + fn ontology_version_zero() { + for version in ["0", "0-draft.lane.1"] { + let error = OntologyTypeVersion::from_str(version) + .expect_err("zero should be rejected as an ontology version"); + assert!( + matches!(error, ParseOntologyTypeVersionError::ParseVersion(_)), + "zero should fail major version parsing" + ); + } + } + + #[test] + fn major_version_steps() { + for value in [1, 2, u32::MAX - 1] { + let version = OntologyTypeMajorVersion::new(value) + .expect("a positive value should create a major version"); + let next = version.next().expect("the version should have a successor"); + assert_eq!( + next.get(), + value + 1, + "the successor should increment the version" + ); + assert_eq!( + next.previous() + .expect("the successor should have a predecessor"), + version, + "the predecessor should restore the original version" + ); + } + } + + #[test] + fn major_version_boundaries() { + assert!( + OntologyTypeMajorVersion::new(0).is_none(), + "zero should be rejected" + ); + let overflow = OntologyTypeMajorVersion::MAX + .next() + .expect_err("the maximum version should have no successor"); + assert_eq!( + overflow.current_context(), + &OntologyTypeMajorVersionError::Overflow, + "incrementing the maximum should report overflow" + ); + let underflow = OntologyTypeMajorVersion::MIN + .previous() + .expect_err("the minimum version should have no predecessor"); + assert_eq!( + underflow.current_context(), + &OntologyTypeMajorVersionError::Underflow, + "decrementing the minimum should report underflow" + ); + } + #[test] fn ontology_version_roundtrip() -> Result<(), Box> { let versions = ["1", "42", "2-draft.lane1234.1", "5-draft.xyz98765.999"]; @@ -966,10 +1124,64 @@ mod tests { Ok(()) } + #[cfg(feature = "postgres")] + #[test] + fn ontology_version_from_sql_zero() { + let mut buffer = BytesMut::new(); + 0_i64 + .to_sql(&Type::INT8, &mut buffer) + .expect("zero should serialize as a PostgreSQL integer"); + + OntologyTypeVersion::from_sql(&Type::INT8, &buffer) + .expect_err("zero should be rejected as an ontology version"); + } + + #[cfg(feature = "postgres")] + #[test] + fn major_version_sql_boundaries() { + for major in [OntologyTypeMajorVersion::MIN, OntologyTypeMajorVersion::MAX] { + let mut buffer = BytesMut::new(); + major + .to_sql_checked(&Type::INT8, &mut buffer) + .expect("major version should serialize as a PostgreSQL integer"); + assert_eq!( + i64::from_sql(&Type::INT8, &buffer) + .expect("major version should use the PostgreSQL INT8 encoding"), + i64::from(major.get()) + ); + assert_eq!( + OntologyTypeMajorVersion::from_sql(&Type::INT8, &buffer) + .expect("major version should deserialize from a PostgreSQL integer"), + major + ); + assert_eq!( + OntologyTypeVersion::from_sql(&Type::INT8, &buffer) + .expect("ontology version should deserialize from a PostgreSQL integer"), + OntologyTypeVersion { + major, + pre_release: None, + } + ); + } + } + + #[cfg(feature = "postgres")] + #[test] + fn major_version_sql_out_of_range() { + for value in [-1_i64, 0, i64::from(u32::MAX) + 1] { + let mut buffer = BytesMut::new(); + value + .to_sql(&Type::INT8, &mut buffer) + .expect("value should serialize as a PostgreSQL integer"); + OntologyTypeMajorVersion::from_sql(&Type::INT8, &buffer) + .expect_err("value outside the major version range should be rejected"); + } + } + #[test] fn ontology_version_ordering_same_major() -> Result<(), Box> { let published = OntologyTypeVersion { - major: 2, + major: OntologyTypeMajorVersion::new(2).expect("version should be nonzero"), pre_release: None, }; @@ -1085,7 +1297,7 @@ mod tests { let parsed = VersionedUrl::from_str(url_str).expect("should parse draft URL"); assert_eq!(parsed.base_url.as_str(), "https://example.com/person/"); - assert_eq!(parsed.version.major, 2); + assert_eq!(parsed.version.major.get(), 2); let Some(PreRelease::Draft { lane, revision }) = parsed.version.pre_release.as_ref() else { panic!("should have pre-release draft info"); @@ -1259,7 +1471,7 @@ mod tests { let version = OntologyTypeVersion::from_str("1-draft.lane.with.dots.5") .expect("Should accept multiple dots in lane identifier"); - assert_eq!(version.major, 1); + assert_eq!(version.major.get(), 1); let Some(PreRelease::Draft { lane, revision }) = version.pre_release.as_ref() else { panic!("should have pre-release draft info"); }; @@ -1270,7 +1482,7 @@ mod tests { let version2 = OntologyTypeVersion::from_str("2-draft.v1.alpha.3.10") .expect("Should accept lane with numbers and dots"); - assert_eq!(version2.major, 2); + assert_eq!(version2.major.get(), 2); let Some(PreRelease::Draft { lane: lane2, revision: rev2, @@ -1288,7 +1500,7 @@ mod tests { let version = OntologyTypeVersion::from_str("3-draft.my-draft.2") .expect("Should accept 'draft' in lane identifier"); - assert_eq!(version.major, 3); + assert_eq!(version.major.get(), 3); let Some(PreRelease::Draft { lane, revision }) = version.pre_release.as_ref() else { panic!("should have pre-release draft info"); }; diff --git a/libs/@blockprotocol/type-system/typescript/src/native/url.ts b/libs/@blockprotocol/type-system/typescript/src/native/url.ts index caf9d9905c1..7b0b1f86639 100644 --- a/libs/@blockprotocol/type-system/typescript/src/native/url.ts +++ b/libs/@blockprotocol/type-system/typescript/src/native/url.ts @@ -10,6 +10,8 @@ import type { import type { BaseUrl, OntologyTypeVersion } from "../generated/types.js"; import type { SemVer } from "semver"; +const u32Max = 4_294_967_295; + /** * Checks if a given URL string is a valid base URL. * @@ -125,8 +127,6 @@ const toSemVer = (version: OntologyTypeVersion): SemVer => { export const validateVersionedUrl = ( url: string, ): Result => { - const U32_MAX = 4294967295; - if (url.length > 2048) { return { type: "Err", @@ -161,7 +161,23 @@ export const validateVersionedUrl = ( try { const parsedVersion = toSemVer(version as OntologyTypeVersion); - if (parsedVersion.major > U32_MAX) { + if (parsedVersion.major === 0) { + return { + type: "Err", + inner: { + reason: "InvalidVersion", + inner: [ + version, + { + reason: "ParseVersion", + inner: "number would be zero for non-zero type", + }, + ], + }, + }; + } + + if (parsedVersion.major > u32Max) { return { type: "Err", inner: { @@ -177,12 +193,12 @@ export const validateVersionedUrl = ( }; } - // Check U32_MAX for revision in draft versions + // Check u32Max for revision in draft versions if (parsedVersion.prerelease.length > 0) { // Format is ["draft", "lane", "revision"] - revision is last element const revision = parsedVersion.prerelease[parsedVersion.prerelease.length - 1]; - if (typeof revision === "number" && revision > U32_MAX) { + if (typeof revision === "number" && revision > u32Max) { return { type: "Err", inner: { @@ -345,8 +361,15 @@ export const ontologyTypeRecordIdToVersionedUrl = ( ): VersionedUrl => `${ontologyTypeRecordId.baseUrl}v/${ontologyTypeRecordId.version.toString()}`; -export const makeOntologyTypeVersion = ({ major }: { major: number }) => - `${major}` as unknown as OntologyTypeVersion; +export const makeOntologyTypeVersion = ({ major }: { major: number }) => { + if (!Number.isInteger(major) || major < 1 || major > u32Max) { + throw new Error( + `Ontology type version must be an integer between 1 and ${u32Max}, received ${major}`, + ); + } + + return `${major}` as unknown as OntologyTypeVersion; +}; export const parseOntologyTypeVersion = ( version: string, diff --git a/libs/@blockprotocol/type-system/typescript/test/url.test.ts b/libs/@blockprotocol/type-system/typescript/test/url.test.ts index ef49fae1cfb..4594a64d5dd 100644 --- a/libs/@blockprotocol/type-system/typescript/test/url.test.ts +++ b/libs/@blockprotocol/type-system/typescript/test/url.test.ts @@ -4,6 +4,7 @@ import { compareOntologyTypeVersions, extractBaseUrl, extractVersion, + makeOntologyTypeVersion, validateBaseUrl, validateVersionedUrl, } from "../src/main.js"; @@ -14,6 +15,8 @@ import type { VersionedUrl, } from "../src/main.js"; +const u32Max = 4_294_967_295; + const invalidBaseUrlCases: string[] = [ "http://example.com", "\\example\\..\\demo/.\\/", @@ -55,9 +58,9 @@ const invalidVersionedUrlCases: string[] = [ describe("validateVersionedUrl", () => { test.each([ - ["http://example.com/v/0"], ["http://example.com/v/1"], ["http://example.com/v/20"], + [`http://example.com/v/${u32Max}`], [`http://exampl${"e".repeat(2027)}.com/v/1`], ])("`validateVersionedUrl(%s)` succeeds", (input) => { expect(validateVersionedUrl(input)).toEqual({ type: "Ok", inner: input }); @@ -69,10 +72,50 @@ describe("validateVersionedUrl", () => { expect(validateVersionedUrl(input)).toMatchSnapshot(); }, ); + + test.each(["0", "0-draft.lane.1"])( + "rejects zero major version %s", + (version) => { + expect(validateVersionedUrl(`http://example.com/v/${version}`)).toEqual({ + type: "Err", + inner: { + reason: "InvalidVersion", + inner: [ + version, + { + reason: "ParseVersion", + inner: "number would be zero for non-zero type", + }, + ], + }, + }); + }, + ); + + test("rejects a major version above u32::MAX", () => { + expect( + validateVersionedUrl(`http://example.com/v/${u32Max + 1}`).type, + ).toBe("Err"); + }); +}); + +describe("makeOntologyTypeVersion", () => { + test.each([1, 20, u32Max])("accepts major version %s", (major) => { + expect(makeOntologyTypeVersion({ major })).toBe(`${major}`); + }); + + test.each([0, -1, 1.5, Number.NaN, Number.POSITIVE_INFINITY, u32Max + 1])( + "rejects invalid major version %s", + (major) => { + expect(() => makeOntologyTypeVersion({ major })).toThrow( + `Ontology type version must be an integer between 1 and ${u32Max}`, + ); + }, + ); }); const extractBaseUrlCases: [VersionedUrl, BaseUrl][] = [ - ["http://example.com/v/0" as VersionedUrl, "http://example.com/" as BaseUrl], + ["http://example.com/v/1" as VersionedUrl, "http://example.com/" as BaseUrl], [ "http://example.com/sandwich/v/1" as VersionedUrl, "http://example.com/sandwich/" as BaseUrl, @@ -97,7 +140,7 @@ describe("extractBaseUrl", () => { }); const extractVersionCases: [VersionedUrl, string][] = [ - ["http://example.com/v/0" as VersionedUrl, "0"], + ["http://example.com/v/1" as VersionedUrl, "1"], ["http://example.com/sandwich/v/1" as VersionedUrl, "1"], ["file://localhost/documents/myfolder/v/10" as VersionedUrl, "10"], ["ftp://rms@example.com/foo/v/5" as VersionedUrl, "5"], @@ -118,6 +161,7 @@ describe("Draft version support", () => { ["http://example.com/v/1-draft.abc12345.1"], ["http://example.com/v/2-draft.xyz98765.999"], ["http://example.com/person/v/5-draft.lane1234.42"], + [`http://example.com/v/1-draft.lane.${u32Max}`], ])("validateVersionedUrl(%s) with draft succeeds", (input) => { expect(validateVersionedUrl(input)).toEqual({ type: "Ok", inner: input }); }); @@ -134,6 +178,21 @@ describe("Draft version support", () => { }); }); + test("rejects a draft revision above u32::MAX", () => { + const version = `1-draft.lane.${u32Max + 1}`; + + expect(validateVersionedUrl(`http://example.com/v/${version}`)).toEqual({ + type: "Err", + inner: { + reason: "InvalidVersion", + inner: [ + version, + { reason: "ParseVersion", inner: "revision number too large" }, + ], + }, + }); + }); + describe("extractVersion with drafts", () => { test.each([ [ diff --git a/libs/@local/graph/api/src/rest/legacy/data_type.rs b/libs/@local/graph/api/src/rest/legacy/data_type.rs index 57f4263c27a..2f740f1d634 100644 --- a/libs/@local/graph/api/src/rest/legacy/data_type.rs +++ b/libs/@local/graph/api/src/rest/legacy/data_type.rs @@ -562,7 +562,11 @@ where conversions, }) = body; - type_to_update.version.major += 1; + type_to_update.version.major = type_to_update + .version + .major + .next() + .map_err(report_to_response)?; let data_type = patch_id_and_parse(&type_to_update, schema).map_err(report_to_response)?; @@ -625,7 +629,11 @@ where provenance, conversions, }| { - type_to_update.version.major += 1; + type_to_update.version.major = type_to_update + .version + .major + .next() + .map_err(report_to_response)?; Ok(UpdateDataTypesParams { schema: patch_id_and_parse(&type_to_update, schema) diff --git a/libs/@local/graph/api/src/rest/legacy/entity_type.rs b/libs/@local/graph/api/src/rest/legacy/entity_type.rs index 73bf54bc322..3954d9a5669 100644 --- a/libs/@local/graph/api/src/rest/legacy/entity_type.rs +++ b/libs/@local/graph/api/src/rest/legacy/entity_type.rs @@ -833,7 +833,11 @@ where provenance, }) = body; - type_to_update.version.major += 1; + type_to_update.version.major = type_to_update + .version + .major + .next() + .map_err(report_to_response)?; let entity_type = patch_id_and_parse(&type_to_update, schema).map_err(report_to_response)?; @@ -894,7 +898,11 @@ where mut type_to_update, provenance, }| { - type_to_update.version.major += 1; + type_to_update.version.major = type_to_update + .version + .major + .next() + .map_err(report_to_response)?; Ok(UpdateEntityTypesParams { schema: patch_id_and_parse(&type_to_update, schema) diff --git a/libs/@local/graph/api/src/rest/legacy/property_type.rs b/libs/@local/graph/api/src/rest/legacy/property_type.rs index 74bf6cbc03e..47ec86978ef 100644 --- a/libs/@local/graph/api/src/rest/legacy/property_type.rs +++ b/libs/@local/graph/api/src/rest/legacy/property_type.rs @@ -494,7 +494,11 @@ where provenance, }) = body; - type_to_update.version.major += 1; + type_to_update.version.major = type_to_update + .version + .major + .next() + .map_err(report_to_response)?; let property_type = patch_id_and_parse(&type_to_update, schema).map_err(report_to_response)?; @@ -555,7 +559,11 @@ where mut type_to_update, provenance, }| { - type_to_update.version.major += 1; + type_to_update.version.major = type_to_update + .version + .major + .next() + .map_err(report_to_response)?; Ok(UpdatePropertyTypesParams { schema: patch_id_and_parse(&type_to_update, schema) diff --git a/libs/@local/graph/api/src/rest/legacy/status.rs b/libs/@local/graph/api/src/rest/legacy/status.rs index ce9af6ee4db..16ea83903d8 100644 --- a/libs/@local/graph/api/src/rest/legacy/status.rs +++ b/libs/@local/graph/api/src/rest/legacy/status.rs @@ -13,6 +13,7 @@ use hash_graph_postgres_store::store::error::BaseUrlAlreadyExists; use hash_graph_store::entity::EntityValidationReport; use hash_status::{Status as HashStatus, StatusCode}; use serde::{Deserialize, Serialize}; +use type_system::ontology::id::OntologyTypeMajorVersionError; /// Generalized information about an error, covering its cause, its origin and a collection of /// weakly-typed metadata. @@ -106,6 +107,8 @@ where .unwrap_or_else(|| { if report.contains::() { StatusCode::AlreadyExists + } else if report.contains::() { + StatusCode::InvalidArgument } else { StatusCode::Unknown } @@ -163,3 +166,43 @@ where status_to_response(HashStatus::new(status_code, Some(message), vec![report])) } } + +#[cfg(test)] +mod tests { + use axum::{body::to_bytes, response::IntoResponse as _}; + use type_system::ontology::id::OntologyTypeMajorVersion; + + use super::report_to_response; + + #[tokio::test] + async fn report_version_overflow() { + let report = OntologyTypeMajorVersion::MAX + .next() + .expect_err("the maximum version should have no successor"); + let response = report_to_response(report).into_response(); + assert_eq!( + response.status(), + axum::http::StatusCode::BAD_REQUEST, + "version overflow should be a client error" + ); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("the error response should be readable"); + let status: serde_json::Value = + serde_json::from_slice(&body).expect("the error response should contain JSON"); + assert_eq!( + status["code"], "INVALID_ARGUMENT", + "overflow should retain its status code" + ); + assert_eq!( + status["message"], "Type version cannot be incremented beyond 4294967295", + "overflow should retain its error message" + ); + assert!( + status["contents"] + .as_array() + .is_some_and(|contents| !contents.is_empty()), + "the response should include the structured error report" + ); + } +} diff --git a/libs/@local/graph/api/src/rest/legacy/utoipa_typedef/subgraph/edges.rs b/libs/@local/graph/api/src/rest/legacy/utoipa_typedef/subgraph/edges.rs index 2f84447c4c5..4af64b63da7 100644 --- a/libs/@local/graph/api/src/rest/legacy/utoipa_typedef/subgraph/edges.rs +++ b/libs/@local/graph/api/src/rest/legacy/utoipa_typedef/subgraph/edges.rs @@ -249,7 +249,7 @@ mod tests { }; use type_system::{ knowledge::entity::id::{EntityId, EntityUuid}, - ontology::id::{BaseUrl, OntologyTypeVersion}, + ontology::id::{BaseUrl, OntologyTypeMajorVersion, OntologyTypeVersion}, principal::actor_group::WebId, }; use uuid::Uuid; @@ -295,7 +295,7 @@ mod tests { base_id: BaseUrl::new("https://example.com/".to_owned()) .expect("should be valid URL"), revision_id: OntologyTypeVersion { - major: 0, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }, diff --git a/libs/@local/graph/authorization/rust/src/policies/cedar/expression_tree.rs b/libs/@local/graph/authorization/rust/src/policies/cedar/expression_tree.rs index 923d66669d5..b16fc0ed93c 100644 --- a/libs/@local/graph/authorization/rust/src/policies/cedar/expression_tree.rs +++ b/libs/@local/graph/authorization/rust/src/policies/cedar/expression_tree.rs @@ -1,5 +1,5 @@ use alloc::{borrow::Cow, sync::Arc}; -use core::{error::Error, str::FromStr as _}; +use core::{error::Error, num::NonZero, str::FromStr as _}; use cedar_policy_core::ast; use error_stack::{Report, ResultExt as _, TryReportTupleExt as _}; @@ -374,10 +374,11 @@ impl PolicyExpressionTree { } (AttributeType::OntologyTypeVersion, ast::ExprKind::Lit(ast::Literal::Long(long))) => { u32::try_from(*long) + .and_then(NonZero::try_from) .change_context(ParseBinaryExpressionError::Right) .map(|major| { Self::OntologyTypeVersion(OntologyTypeVersion { - major, + major: major.into(), pre_release: None, }) }) @@ -477,3 +478,48 @@ impl PolicyExpressionTree { .attach_with(|| Arc::clone(rhs)) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn version_expression(major: i64) -> ast::Expr { + ast::Expr::is_eq( + ast::Expr::get_attr( + ast::Expr::var(ast::Var::Resource), + SmolStr::new_static("ontology_type_version"), + ), + ast::Expr::val(major), + ) + } + + #[test] + fn from_expr_zero_version() { + let expression = ast::Expr::and(ast::Expr::val(true), version_expression(0)); + let error = PolicyExpressionTree::from_expr(&expression) + .expect_err("a policy containing version zero should be rejected"); + + assert!( + matches!(error.current_context(), ParseExpressionError::AndExpression), + "the containing policy expression should fail parsing" + ); + assert!( + error.contains::(), + "the error should retain the invalid version operand context" + ); + } + + #[test] + fn from_expr_version_bounds() { + for major in [1, u32::MAX] { + let expression = + ast::Expr::and(ast::Expr::val(true), version_expression(i64::from(major))); + let parsed = PolicyExpressionTree::from_expr(&expression) + .expect("a policy containing a valid version should parse"); + let PolicyExpressionTree::OntologyTypeVersion(version) = parsed else { + panic!("the policy should preserve its version condition"); + }; + assert_eq!(version.major.get(), major); + } + } +} diff --git a/libs/@local/graph/authorization/rust/src/policies/resource/data_type.rs b/libs/@local/graph/authorization/rust/src/policies/resource/data_type.rs index 46769b59898..48d55234bbd 100644 --- a/libs/@local/graph/authorization/rust/src/policies/resource/data_type.rs +++ b/libs/@local/graph/authorization/rust/src/policies/resource/data_type.rs @@ -89,8 +89,8 @@ impl DataTypeResource<'_> { ( SmolStr::new_static("version"), ast::PartialValue::Value(ast::Value::new( - ast::ValueKind::Lit(ast::Literal::Long(ast::Integer::from( - self.id.as_url().version.major, + ast::ValueKind::Lit(ast::Literal::Long(i64::from( + self.id.as_url().version.major.get(), ))), None, )), diff --git a/libs/@local/graph/authorization/rust/src/policies/resource/entity_type.rs b/libs/@local/graph/authorization/rust/src/policies/resource/entity_type.rs index 32e4f3dbd64..4ec2c786ce8 100644 --- a/libs/@local/graph/authorization/rust/src/policies/resource/entity_type.rs +++ b/libs/@local/graph/authorization/rust/src/policies/resource/entity_type.rs @@ -89,8 +89,8 @@ impl EntityTypeResource<'_> { ( SmolStr::new_static("version"), ast::PartialValue::Value(ast::Value::new( - ast::ValueKind::Lit(ast::Literal::Long(ast::Integer::from( - self.id.as_url().version.major, + ast::ValueKind::Lit(ast::Literal::Long(i64::from( + self.id.as_url().version.major.get(), ))), None, )), diff --git a/libs/@local/graph/authorization/rust/src/policies/resource/property_type.rs b/libs/@local/graph/authorization/rust/src/policies/resource/property_type.rs index 830bb8391f6..4ff0ae79d4c 100644 --- a/libs/@local/graph/authorization/rust/src/policies/resource/property_type.rs +++ b/libs/@local/graph/authorization/rust/src/policies/resource/property_type.rs @@ -89,8 +89,8 @@ impl PropertyTypeResource<'_> { ( SmolStr::new_static("version"), ast::PartialValue::Value(ast::Value::new( - ast::ValueKind::Lit(ast::Literal::Long(ast::Integer::from( - self.id.as_url().version.major, + ast::ValueKind::Lit(ast::Literal::Long(i64::from( + self.id.as_url().version.major.get(), ))), None, )), diff --git a/libs/@local/graph/postgres-store/src/snapshot/ontology/data_type/channel.rs b/libs/@local/graph/postgres-store/src/snapshot/ontology/data_type/channel.rs index c7d27006565..db5b17c6d6d 100644 --- a/libs/@local/graph/postgres-store/src/snapshot/ontology/data_type/channel.rs +++ b/libs/@local/graph/postgres-store/src/snapshot/ontology/data_type/channel.rs @@ -12,12 +12,9 @@ use futures::{ }; use type_system::{ Valid, Validator as _, - ontology::{ - data_type::{ - ClosedDataType, DataTypeUuid, - schema::{DataTypeValidator, ValueLabel}, - }, - id::{OntologyTypeVersion, VersionedUrl}, + ontology::data_type::{ + ClosedDataType, DataTypeUuid, + schema::{DataTypeValidator, ValueLabel}, }, }; @@ -85,13 +82,7 @@ impl Sink for DataTypeSender { // An empty schema is inserted initially. This will be replaced later by the closed // schema. closed_schema: Valid::new_unchecked(ClosedDataType { - id: VersionedUrl { - base_url: schema.id.base_url.clone(), - version: OntologyTypeVersion { - major: 0, - pre_release: None, - }, - }, + id: schema.id.clone(), title: String::new(), title_plural: None, icon: None, diff --git a/libs/@local/graph/postgres-store/src/snapshot/ontology/entity_type/channel.rs b/libs/@local/graph/postgres-store/src/snapshot/ontology/entity_type/channel.rs index 36568d48053..9ffb5e8ae25 100644 --- a/libs/@local/graph/postgres-store/src/snapshot/ontology/entity_type/channel.rs +++ b/libs/@local/graph/postgres-store/src/snapshot/ontology/entity_type/channel.rs @@ -13,12 +13,9 @@ use futures::{ }; use type_system::{ Valid, Validator as _, - ontology::{ - entity_type::{ - ClosedEntityType, EntityTypeUuid, - schema::{EntityConstraints, EntityTypeValidator, InverseEntityTypeMetadata}, - }, - id::{OntologyTypeVersion, VersionedUrl}, + ontology::entity_type::{ + ClosedEntityType, EntityTypeUuid, + schema::{EntityConstraints, EntityTypeValidator, InverseEntityTypeMetadata}, }, }; @@ -84,13 +81,7 @@ impl Sink for EntityTypeSender { // An empty schema is inserted initially. This will be replaced later by the closed // schema. closed_schema: Valid::new_unchecked(ClosedEntityType { - id: VersionedUrl { - base_url: schema.id.base_url.clone(), - version: OntologyTypeVersion { - major: 0, - pre_release: None, - }, - }, + id: schema.id.clone(), title: String::new(), title_plural: None, description: String::new(), diff --git a/libs/@local/graph/postgres-store/src/store/postgres/mod.rs b/libs/@local/graph/postgres-store/src/store/postgres/mod.rs index b2550bfb47c..0f22fe7668e 100644 --- a/libs/@local/graph/postgres-store/src/store/postgres/mod.rs +++ b/libs/@local/graph/postgres-store/src/store/postgres/mod.rs @@ -3505,13 +3505,16 @@ where url: &VersionedUrl, provenance: &OntologyEditionProvenance, ) -> Result<(OntologyTypeUuid, WebId, OntologyTemporalMetadata), Report> { - let previous_version = - OntologyTypeVersion { - major: url.version.major.checked_sub(1).ok_or(UpdateError).attach( - "The version of the data type is already at the lowest possible value", - )?, - pre_release: None, - }; + let previous_version = OntologyTypeVersion { + major: url + .version + .major + .previous() + .change_context(OntologyVersionDoesNotExist) + .change_context(UpdateError) + .attach("The version of the data type is already at the lowest possible value")?, + pre_release: None, + }; let Some(web_id) = self .as_client() .query_opt( diff --git a/libs/@local/graph/postgres-store/src/store/postgres/ontology/data_type.rs b/libs/@local/graph/postgres-store/src/store/postgres/ontology/data_type.rs index df9e240a23d..0384c74c973 100644 --- a/libs/@local/graph/postgres-store/src/store/postgres/ontology/data_type.rs +++ b/libs/@local/graph/postgres-store/src/store/postgres/ontology/data_type.rs @@ -52,7 +52,7 @@ use type_system::{ }; use crate::store::{ - error::DeletionError, + error::{DeletionError, OntologyVersionDoesNotExist}, postgres::{ AsClient, GenericClientIter as _, PostgresStore, TransactionState, TraversalContext, crud::{QueryIndices, QueryRecordDecode, TypedRow}, @@ -954,8 +954,9 @@ where .id .version .major - .checked_sub(1) - .ok_or(UpdateError) + .previous() + .change_context(OntologyVersionDoesNotExist) + .change_context(UpdateError) .attach( "The version of the data type is already at the lowest possible value", )?, diff --git a/libs/@local/graph/postgres-store/src/store/postgres/ontology/entity_type.rs b/libs/@local/graph/postgres-store/src/store/postgres/ontology/entity_type.rs index b82865517e1..1b980f69900 100644 --- a/libs/@local/graph/postgres-store/src/store/postgres/ontology/entity_type.rs +++ b/libs/@local/graph/postgres-store/src/store/postgres/ontology/entity_type.rs @@ -69,7 +69,7 @@ use type_system::{ }; use crate::store::{ - error::DeletionError, + error::{DeletionError, OntologyVersionDoesNotExist}, postgres::{ AsClient, GenericClientIter as _, PostgresStore, ResponseCountMap, TransactionState, TraversalContext, @@ -1646,8 +1646,9 @@ where .id .version .major - .checked_sub(1) - .ok_or(UpdateError) + .previous() + .change_context(OntologyVersionDoesNotExist) + .change_context(UpdateError) .attach( "The version of the entity type is already at the lowest possible \ value", diff --git a/libs/@local/graph/postgres-store/src/store/postgres/ontology/property_type.rs b/libs/@local/graph/postgres-store/src/store/postgres/ontology/property_type.rs index 3e5259e3ea9..df430f7a560 100644 --- a/libs/@local/graph/postgres-store/src/store/postgres/ontology/property_type.rs +++ b/libs/@local/graph/postgres-store/src/store/postgres/ontology/property_type.rs @@ -49,7 +49,7 @@ use type_system::{ }; use crate::store::{ - error::DeletionError, + error::{DeletionError, OntologyVersionDoesNotExist}, postgres::{ AsClient, GenericClientIter as _, PostgresStore, TransactionState, TraversalContext, crud::{QueryIndices, QueryRecordDecode, TypedRow}, @@ -869,8 +869,9 @@ where .id .version .major - .checked_sub(1) - .ok_or(UpdateError) + .previous() + .change_context(OntologyVersionDoesNotExist) + .change_context(UpdateError) .attach( "The version of the property type is already at the lowest possible \ value", diff --git a/libs/@local/graph/postgres-store/src/store/postgres/query/compile/tests.rs b/libs/@local/graph/postgres-store/src/store/postgres/query/compile/tests.rs index 03fd41fac23..f528e717f35 100644 --- a/libs/@local/graph/postgres-store/src/store/postgres/query/compile/tests.rs +++ b/libs/@local/graph/postgres-store/src/store/postgres/query/compile/tests.rs @@ -2052,7 +2052,7 @@ fn transpile_offset() { mod predefined { use type_system::{ knowledge::entity::id::{DraftId, EntityId, EntityUuid}, - ontology::id::{BaseUrl, OntologyTypeVersion, VersionedUrl}, + ontology::id::{BaseUrl, OntologyTypeMajorVersion, OntologyTypeVersion, VersionedUrl}, principal::actor_group::WebId, }; @@ -2066,7 +2066,7 @@ mod predefined { ) .expect("invalid base url"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }; diff --git a/libs/@local/graph/postgres-store/tests/deletion/main.rs b/libs/@local/graph/postgres-store/tests/deletion/main.rs index f7921bbfc02..6935f0298dd 100644 --- a/libs/@local/graph/postgres-store/tests/deletion/main.rs +++ b/libs/@local/graph/postgres-store/tests/deletion/main.rs @@ -44,7 +44,7 @@ use type_system::{ ontology::{ data_type::DataType, entity_type::EntityType, - id::{BaseUrl, OntologyTypeVersion, VersionedUrl}, + id::{BaseUrl, OntologyTypeMajorVersion, OntologyTypeVersion, VersionedUrl}, property_type::PropertyType, provenance::{OntologyOwnership, ProvidedOntologyEditionProvenance}, }, @@ -262,7 +262,7 @@ pub(crate) fn person_type_id() -> VersionedUrl { ) .expect("couldn't construct Base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, } @@ -275,7 +275,7 @@ pub(crate) fn friend_of_type_id() -> VersionedUrl { ) .expect("couldn't construct Base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, } diff --git a/libs/@local/graph/store/rust/src/filter/mod.rs b/libs/@local/graph/store/rust/src/filter/mod.rs index c14f047bf62..81e8aef7ce1 100644 --- a/libs/@local/graph/store/rust/src/filter/mod.rs +++ b/libs/@local/graph/store/rust/src/filter/mod.rs @@ -1527,7 +1527,10 @@ mod tests { use serde_json::json; use type_system::{ knowledge::entity::id::{DraftId, EntityUuid}, - ontology::data_type::{ClosedDataType, ConversionExpression}, + ontology::{ + data_type::{ClosedDataType, ConversionExpression}, + id::OntologyTypeMajorVersion, + }, principal::actor_group::WebId, }; use uuid::Uuid; @@ -1596,7 +1599,7 @@ mod tests { ) .expect("invalid base url"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }; diff --git a/tests/graph/http/tests/friendship.http b/tests/graph/http/tests/friendship.http index 840435ed888..773ee807006 100644 --- a/tests/graph/http/tests/friendship.http +++ b/tests/graph/http/tests/friendship.http @@ -2063,3 +2063,393 @@ X-Authenticated-User-Actor-Id: {{user_id}} }); client.global.set("person_c_entity_id", response.body.metadata.recordId.entityId); %} + +### Reject data type version overflow +PUT http://127.0.0.1:4000/data-types +Content-Type: application/json +Accept: application/json +X-Authenticated-User-Actor-Id: {{user_id}} + +{ + "typeToUpdate": "http://localhost:3000/@alice/types/data-type/meter/v/4294967295", + "schema": { + "$schema": "https://blockprotocol.org/types/modules/graph/0.3/schema/data-type", + "kind": "dataType", + "title": "Meter", + "description": "A non-negative distance in meters.", + "type": "number", + "minimum": 0 + }, + "provenance": { + "actorType": "machine", + "origin": { + "type": "api" + } + }, + "conversions": {} +} + +> {% + client.test("version overflow", function() { + client.assert(response.status === 400, "Version overflow should return HTTP 400"); + client.assert(response.body.code === "INVALID_ARGUMENT", "Version overflow should return INVALID_ARGUMENT"); + client.assert(response.body.message === "Type version cannot be incremented beyond 4294967295", "The error should identify version overflow"); + }); +%} + +### Reject bulk data type version overflow +PUT http://127.0.0.1:4000/data-types/bulk +Content-Type: application/json +Accept: application/json +X-Authenticated-User-Actor-Id: {{user_id}} + +[ + { + "typeToUpdate": "http://localhost:3000/@alice/types/data-type/meter/v/2", + "schema": { + "$schema": "https://blockprotocol.org/types/modules/graph/0.3/schema/data-type", + "kind": "dataType", + "title": "Meter", + "description": "A non-negative distance in meters.", + "type": "number", + "minimum": 0 + }, + "provenance": { + "actorType": "machine", + "origin": { + "type": "api" + } + }, + "conversions": {} + }, + { + "typeToUpdate": "http://localhost:3000/@alice/types/data-type/meter/v/4294967295", + "schema": { + "$schema": "https://blockprotocol.org/types/modules/graph/0.3/schema/data-type", + "kind": "dataType", + "title": "Meter", + "description": "A non-negative distance in meters.", + "type": "number", + "minimum": 0 + }, + "provenance": { + "actorType": "machine", + "origin": { + "type": "api" + } + }, + "conversions": {} + } +] + +> {% + client.test("version overflow", function() { + client.assert(response.status === 400, "Version overflow should return HTTP 400"); + client.assert(response.body.code === "INVALID_ARGUMENT", "Version overflow should return INVALID_ARGUMENT"); + client.assert(response.body.message === "Type version cannot be incremented beyond 4294967295", "The error should identify version overflow"); + }); +%} + +### Verify rejected bulk update did not create a data type version +POST http://127.0.0.1:4000/data-types/query +Content-Type: application/json +Accept: application/json +X-Authenticated-User-Actor-Id: {{user_id}} + +{ + "filter": { + "equal": [ + { + "path": [ + "versionedUrl" + ] + }, + { + "parameter": "http://localhost:3000/@alice/types/data-type/meter/v/3" + } + ] + }, + "temporalAxes": { + "pinned": { + "axis": "transactionTime", + "timestamp": null + }, + "variable": { + "axis": "decisionTime", + "interval": { + "start": { + "kind": "unbounded" + }, + "end": null + } + } + } +} + +> {% + client.test("bulk update is atomic", function() { + client.assert(response.status === 200, "The type query should succeed"); + client.assert(response.body.dataTypes.length === 0, "The valid item preceding the overflow should not create version 3"); + }); +%} + +### Reject property type version overflow +PUT http://127.0.0.1:4000/property-types +Content-Type: application/json +Accept: application/json +X-Authenticated-User-Actor-Id: {{user_id}} + +{ + "typeToUpdate": "http://localhost:3000/@alice/types/property-type/name/v/4294967295", + "schema": { + "$schema": "https://blockprotocol.org/types/modules/graph/0.3/schema/property-type", + "kind": "propertyType", + "title": "Name", + "description": "A person’s name.", + "oneOf": [ + { + "$ref": "{{text_data_type_id}}" + } + ] + }, + "provenance": { + "actorType": "machine", + "origin": { + "type": "api" + } + } +} + +> {% + client.test("version overflow", function() { + client.assert(response.status === 400, "Version overflow should return HTTP 400"); + client.assert(response.body.code === "INVALID_ARGUMENT", "Version overflow should return INVALID_ARGUMENT"); + client.assert(response.body.message === "Type version cannot be incremented beyond 4294967295", "The error should identify version overflow"); + }); +%} + +### Reject bulk property type version overflow +PUT http://127.0.0.1:4000/property-types/bulk +Content-Type: application/json +Accept: application/json +X-Authenticated-User-Actor-Id: {{user_id}} + +[ + { + "typeToUpdate": "http://localhost:3000/@alice/types/property-type/name/v/2", + "schema": { + "$schema": "https://blockprotocol.org/types/modules/graph/0.3/schema/property-type", + "kind": "propertyType", + "title": "Name", + "description": "A person’s name.", + "oneOf": [ + { + "$ref": "{{text_data_type_id}}" + } + ] + }, + "provenance": { + "actorType": "machine", + "origin": { + "type": "api" + } + } + }, + { + "typeToUpdate": "http://localhost:3000/@alice/types/property-type/name/v/4294967295", + "schema": { + "$schema": "https://blockprotocol.org/types/modules/graph/0.3/schema/property-type", + "kind": "propertyType", + "title": "Name", + "description": "A person’s name.", + "oneOf": [ + { + "$ref": "{{text_data_type_id}}" + } + ] + }, + "provenance": { + "actorType": "machine", + "origin": { + "type": "api" + } + } + } +] + +> {% + client.test("version overflow", function() { + client.assert(response.status === 400, "Version overflow should return HTTP 400"); + client.assert(response.body.code === "INVALID_ARGUMENT", "Version overflow should return INVALID_ARGUMENT"); + client.assert(response.body.message === "Type version cannot be incremented beyond 4294967295", "The error should identify version overflow"); + }); +%} + +### Verify rejected bulk update did not create a property type version +POST http://127.0.0.1:4000/property-types/query +Content-Type: application/json +Accept: application/json +X-Authenticated-User-Actor-Id: {{user_id}} + +{ + "filter": { + "equal": [ + { + "path": [ + "versionedUrl" + ] + }, + { + "parameter": "http://localhost:3000/@alice/types/property-type/name/v/3" + } + ] + }, + "temporalAxes": { + "pinned": { + "axis": "transactionTime", + "timestamp": null + }, + "variable": { + "axis": "decisionTime", + "interval": { + "start": { + "kind": "unbounded" + }, + "end": null + } + } + } +} + +> {% + client.test("bulk update is atomic", function() { + client.assert(response.status === 200, "The type query should succeed"); + client.assert(response.body.propertyTypes.length === 0, "The valid item preceding the overflow should not create version 3"); + }); +%} + +### Reject entity type version overflow +PUT http://127.0.0.1:4000/entity-types +Content-Type: application/json +Accept: application/json +X-Authenticated-User-Actor-Id: {{user_id}} + +{ + "typeToUpdate": "http://localhost:3000/@alice/types/entity-type/person/v/4294967295", + "schema": { + "$schema": "https://blockprotocol.org/types/modules/graph/0.3/schema/entity-type", + "kind": "entityType", + "title": "Person", + "description": "A person.", + "type": "object", + "properties": {} + }, + "provenance": { + "actorType": "machine", + "origin": { + "type": "api" + } + } +} + +> {% + client.test("version overflow", function() { + client.assert(response.status === 400, "Version overflow should return HTTP 400"); + client.assert(response.body.code === "INVALID_ARGUMENT", "Version overflow should return INVALID_ARGUMENT"); + client.assert(response.body.message === "Type version cannot be incremented beyond 4294967295", "The error should identify version overflow"); + }); +%} + +### Reject bulk entity type version overflow +PUT http://127.0.0.1:4000/entity-types/bulk +Content-Type: application/json +Accept: application/json +X-Authenticated-User-Actor-Id: {{user_id}} + +[ + { + "typeToUpdate": "http://localhost:3000/@alice/types/entity-type/person/v/2", + "schema": { + "$schema": "https://blockprotocol.org/types/modules/graph/0.3/schema/entity-type", + "kind": "entityType", + "title": "Person", + "description": "A person.", + "type": "object", + "properties": {} + }, + "provenance": { + "actorType": "machine", + "origin": { + "type": "api" + } + } + }, + { + "typeToUpdate": "http://localhost:3000/@alice/types/entity-type/person/v/4294967295", + "schema": { + "$schema": "https://blockprotocol.org/types/modules/graph/0.3/schema/entity-type", + "kind": "entityType", + "title": "Person", + "description": "A person.", + "type": "object", + "properties": {} + }, + "provenance": { + "actorType": "machine", + "origin": { + "type": "api" + } + } + } +] + +> {% + client.test("version overflow", function() { + client.assert(response.status === 400, "Version overflow should return HTTP 400"); + client.assert(response.body.code === "INVALID_ARGUMENT", "Version overflow should return INVALID_ARGUMENT"); + client.assert(response.body.message === "Type version cannot be incremented beyond 4294967295", "The error should identify version overflow"); + }); +%} + +### Verify rejected bulk update did not create an entity type version +POST http://127.0.0.1:4000/entity-types/query +Content-Type: application/json +Accept: application/json +X-Authenticated-User-Actor-Id: {{user_id}} + +{ + "filter": { + "equal": [ + { + "path": [ + "versionedUrl" + ] + }, + { + "parameter": "http://localhost:3000/@alice/types/entity-type/person/v/3" + } + ] + }, + "temporalAxes": { + "pinned": { + "axis": "transactionTime", + "timestamp": null + }, + "variable": { + "axis": "decisionTime", + "interval": { + "start": { + "kind": "unbounded" + }, + "end": null + } + } + } +} + +> {% + client.test("bulk update is atomic", function() { + client.assert(response.status === 200, "The type query should succeed"); + client.assert(response.body.entityTypes.length === 0, "The valid item preceding the overflow should not create version 3"); + }); +%} diff --git a/tests/graph/integration/postgres/clustering.rs b/tests/graph/integration/postgres/clustering.rs index 2082e8a137c..c951490c924 100644 --- a/tests/graph/integration/postgres/clustering.rs +++ b/tests/graph/integration/postgres/clustering.rs @@ -15,7 +15,7 @@ use type_system::{ entity::{EntityId, id::EntityUuid, provenance::ProvidedEntityEditionProvenance}, property::{PropertyObject, PropertyObjectWithMetadata}, }, - ontology::id::{BaseUrl, OntologyTypeVersion, VersionedUrl}, + ontology::id::{BaseUrl, OntologyTypeMajorVersion, OntologyTypeVersion, VersionedUrl}, principal::{actor::ActorType, actor_group::WebId}, provenance::{OriginProvenance, OriginType}, }; @@ -63,7 +63,7 @@ fn person_entity_type_id() -> VersionedUrl { ) .expect("couldn't construct Base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, } diff --git a/tests/graph/integration/postgres/drafts.rs b/tests/graph/integration/postgres/drafts.rs index 8f078ae91fd..1725cc5f560 100644 --- a/tests/graph/integration/postgres/drafts.rs +++ b/tests/graph/integration/postgres/drafts.rs @@ -13,7 +13,7 @@ use type_system::{ PropertyPath, PropertyWithMetadata, }, }, - ontology::id::{BaseUrl, OntologyTypeVersion, VersionedUrl}, + ontology::id::{BaseUrl, OntologyTypeMajorVersion, OntologyTypeVersion, VersionedUrl}, principal::{actor::ActorType, actor_group::WebId}, provenance::{OriginProvenance, OriginType}, }; @@ -56,7 +56,7 @@ fn person_entity_type_id() -> VersionedUrl { ) .expect("couldn't construct Base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, } diff --git a/tests/graph/integration/postgres/email_filter_protection.rs b/tests/graph/integration/postgres/email_filter_protection.rs index 8e4249ca934..dfe9c11d033 100644 --- a/tests/graph/integration/postgres/email_filter_protection.rs +++ b/tests/graph/integration/postgres/email_filter_protection.rs @@ -42,7 +42,7 @@ use type_system::{ entity::{LinkData, id::EntityUuid, provenance::ProvidedEntityEditionProvenance}, property::{PropertyObject, PropertyObjectWithMetadata, metadata::PropertyProvenance}, }, - ontology::id::{BaseUrl, OntologyTypeVersion, VersionedUrl}, + ontology::id::{BaseUrl, OntologyTypeMajorVersion, OntologyTypeVersion, VersionedUrl}, principal::{actor::ActorType, actor_group::WebId}, provenance::{OriginProvenance, OriginType}, }; @@ -264,7 +264,7 @@ impl DatabaseApi<'_> { entity_type_ids: HashSet::from([VersionedUrl { base_url: BaseUrl::new(USER_ENTITY_TYPE_BASE_URL.to_owned()).unwrap(), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }]), @@ -299,7 +299,7 @@ impl DatabaseApi<'_> { entity_type_ids: HashSet::from([VersionedUrl { base_url: BaseUrl::new(INVITATION_ENTITY_TYPE_BASE_URL.to_owned()).unwrap(), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }]), @@ -337,14 +337,14 @@ impl DatabaseApi<'_> { VersionedUrl { base_url: BaseUrl::new(USER_ENTITY_TYPE_BASE_URL.to_owned()).unwrap(), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }, VersionedUrl { base_url: BaseUrl::new(INVITATION_ENTITY_TYPE_BASE_URL.to_owned()).unwrap(), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }, @@ -1476,7 +1476,7 @@ impl DatabaseApi<'_> { entity_type_ids: HashSet::from([VersionedUrl { base_url: BaseUrl::new(USER_ENTITY_TYPE_BASE_URL.to_owned()).unwrap(), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }]), @@ -1516,7 +1516,7 @@ impl DatabaseApi<'_> { entity_type_ids: HashSet::from([VersionedUrl { base_url: BaseUrl::new(INVITATION_ENTITY_TYPE_BASE_URL.to_owned()).unwrap(), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }]), @@ -2120,7 +2120,7 @@ impl DatabaseApi<'_> { entity_type_ids: HashSet::from([VersionedUrl { base_url: BaseUrl::new(USER_ENTITY_TYPE_BASE_URL.to_owned()).unwrap(), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }]), @@ -2160,7 +2160,7 @@ impl DatabaseApi<'_> { entity_type_ids: HashSet::from([VersionedUrl { base_url: BaseUrl::new(SECRET_ENTITY_TYPE_BASE_URL.to_owned()).unwrap(), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }]), @@ -2568,7 +2568,7 @@ impl DatabaseApi<'_> { entity_type_ids: HashSet::from([VersionedUrl { base_url: BaseUrl::new(USER_ENTITY_TYPE_BASE_URL.to_owned()).unwrap(), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }]), @@ -3021,7 +3021,7 @@ async fn subgraph_traversal_masks_linked_user_email() { let org_type_id = VersionedUrl { base_url: BaseUrl::new(ORGANIZATION_TYPE_BASE_URL.to_owned()).unwrap(), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }; @@ -3029,7 +3029,7 @@ async fn subgraph_traversal_masks_linked_user_email() { let user_type_id = VersionedUrl { base_url: BaseUrl::new(USER_ENTITY_TYPE_BASE_URL.to_owned()).unwrap(), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }; @@ -3037,7 +3037,7 @@ async fn subgraph_traversal_masks_linked_user_email() { let link_type_id = VersionedUrl { base_url: BaseUrl::new(HAS_MEMBER_LINK_TYPE_BASE_URL.to_owned()).unwrap(), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }; diff --git a/tests/graph/integration/postgres/entity.rs b/tests/graph/integration/postgres/entity.rs index 4c9793da77a..f0d3ca56202 100644 --- a/tests/graph/integration/postgres/entity.rs +++ b/tests/graph/integration/postgres/entity.rs @@ -21,7 +21,7 @@ use type_system::{ PropertyPath, PropertyWithMetadata, }, }, - ontology::id::{BaseUrl, OntologyTypeVersion, VersionedUrl}, + ontology::id::{BaseUrl, OntologyTypeMajorVersion, OntologyTypeVersion, VersionedUrl}, principal::{actor::ActorType, actor_group::WebId}, provenance::{OriginProvenance, OriginType}, }; @@ -72,7 +72,7 @@ async fn insert() { ) .expect("couldn't construct Base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }]), @@ -146,7 +146,7 @@ async fn query() { ) .expect("couldn't construct Base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }]), @@ -218,7 +218,7 @@ async fn public_actor_reads_only_publicly_permitted_entities() { ) .expect("couldn't construct Base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }]), @@ -352,7 +352,7 @@ async fn update() { ) .expect("couldn't construct Base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }]), diff --git a/tests/graph/integration/postgres/links.rs b/tests/graph/integration/postgres/links.rs index ccd084ed9a6..ce30c3fccb1 100644 --- a/tests/graph/integration/postgres/links.rs +++ b/tests/graph/integration/postgres/links.rs @@ -19,7 +19,7 @@ use type_system::{ entity::{LinkData, provenance::ProvidedEntityEditionProvenance}, property::{PropertyObject, PropertyObjectWithMetadata, metadata::PropertyProvenance}, }, - ontology::id::{BaseUrl, OntologyTypeVersion, VersionedUrl}, + ontology::id::{BaseUrl, OntologyTypeMajorVersion, OntologyTypeVersion, VersionedUrl}, principal::{actor::ActorType, actor_group::WebId}, provenance::{OriginProvenance, OriginType}, }; @@ -65,7 +65,7 @@ async fn insert() { ) .expect("couldn't construct Base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }; @@ -126,7 +126,7 @@ async fn insert() { ) .expect("couldn't construct Base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }; @@ -299,7 +299,7 @@ async fn get_entity_links() { ) .expect("couldn't construct Base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }; @@ -310,7 +310,7 @@ async fn get_entity_links() { ) .expect("couldn't construct Base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }; @@ -321,7 +321,7 @@ async fn get_entity_links() { ) .expect("couldn't construct Base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }; @@ -567,7 +567,7 @@ async fn remove_link() { ) .expect("couldn't construct Base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }; @@ -578,7 +578,7 @@ async fn remove_link() { ) .expect("couldn't construct Base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }; diff --git a/tests/graph/integration/postgres/read_only.rs b/tests/graph/integration/postgres/read_only.rs index 04b7cbaf04f..f094939f1f3 100644 --- a/tests/graph/integration/postgres/read_only.rs +++ b/tests/graph/integration/postgres/read_only.rs @@ -18,7 +18,7 @@ use type_system::{ }, value::{ValueMetadata, metadata::ValueProvenance}, }, - ontology::id::{BaseUrl, OntologyTypeVersion, VersionedUrl}, + ontology::id::{BaseUrl, OntologyTypeMajorVersion, OntologyTypeVersion, VersionedUrl}, principal::{ actor::{ActorEntityUuid, ActorId, ActorType}, actor_group::WebId, @@ -143,7 +143,7 @@ async fn read_only_modification_matrix() { ) .expect("couldn't construct Base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }; diff --git a/tests/graph/integration/postgres/semantic_search.rs b/tests/graph/integration/postgres/semantic_search.rs index 3dff8474f33..e5094c73924 100644 --- a/tests/graph/integration/postgres/semantic_search.rs +++ b/tests/graph/integration/postgres/semantic_search.rs @@ -24,7 +24,7 @@ use type_system::{ entity::{EntityId, provenance::ProvidedEntityEditionProvenance}, property::{PropertyObject, PropertyObjectWithMetadata}, }, - ontology::id::{BaseUrl, OntologyTypeVersion, VersionedUrl}, + ontology::id::{BaseUrl, OntologyTypeMajorVersion, OntologyTypeVersion, VersionedUrl}, principal::{ actor::{ActorId, ActorType}, actor_group::WebId, @@ -69,7 +69,7 @@ fn person_entity_type_id() -> VersionedUrl { ) .expect("couldn't construct Base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, } diff --git a/tests/graph/integration/postgres/sorting.rs b/tests/graph/integration/postgres/sorting.rs index 8fa455d6fb3..6664ce40cf0 100644 --- a/tests/graph/integration/postgres/sorting.rs +++ b/tests/graph/integration/postgres/sorting.rs @@ -20,7 +20,7 @@ use type_system::{ }, ontology::{ VersionedUrl, - id::{BaseUrl, OntologyTypeVersion}, + id::{BaseUrl, OntologyTypeMajorVersion, OntologyTypeVersion}, }, principal::{actor::ActorType, actor_group::WebId}, provenance::{OriginProvenance, OriginType}, @@ -160,7 +160,7 @@ async fn insert(database: &mut DatabaseTestWrapper) -> DatabaseApi<'_> { ) .expect("couldn't construct Base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }; @@ -170,7 +170,7 @@ async fn insert(database: &mut DatabaseTestWrapper) -> DatabaseApi<'_> { ) .expect("couldn't construct Base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }; diff --git a/tests/graph/integration/postgres/table.rs b/tests/graph/integration/postgres/table.rs index a49a2632dfb..7309af59464 100644 --- a/tests/graph/integration/postgres/table.rs +++ b/tests/graph/integration/postgres/table.rs @@ -23,7 +23,7 @@ use type_system::{ }, ontology::{ VersionedUrl, - id::{BaseUrl, OntologyTypeVersion}, + id::{BaseUrl, OntologyTypeMajorVersion, OntologyTypeVersion}, }, principal::{actor::ActorType, actor_group::WebId}, provenance::{OriginProvenance, OriginType}, @@ -38,7 +38,7 @@ fn person_entity_type() -> VersionedUrl { ) .expect("the URL should be a valid base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, } @@ -51,7 +51,7 @@ fn page_entity_type() -> VersionedUrl { ) .expect("the URL should be a valid base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, } @@ -144,7 +144,7 @@ async fn link_rows_carry_their_endpoints() { ) .expect("the URL should be a valid base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }; @@ -325,7 +325,7 @@ async fn label_sort_pages_alphabetically() { ) .expect("the URL should be a valid base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }; @@ -614,7 +614,7 @@ async fn conversions_convert_row_property_values() { base_url: BaseUrl::new("http://localhost:3000/@alice/types/entity-type/line/".to_owned()) .expect("the URL should be a valid base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }; @@ -973,7 +973,7 @@ async fn link_endpoints_hide_entities_the_actor_cannot_view() { ) .expect("the URL should be a valid base URL"), version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }; diff --git a/tests/graph/test-data/rust/src/seeding/producer/data_type.rs b/tests/graph/test-data/rust/src/seeding/producer/data_type.rs index 201fb1c4e40..c8ed4a14764 100644 --- a/tests/graph/test-data/rust/src/seeding/producer/data_type.rs +++ b/tests/graph/test-data/rust/src/seeding/producer/data_type.rs @@ -11,7 +11,7 @@ use type_system::ontology::{ DataType, schema::{DataTypeReference, DataTypeSchemaTag, DataTypeTag, ValueLabel}, }, - id::{OntologyTypeVersion, ParseBaseUrlError}, + id::{OntologyTypeMajorVersion, OntologyTypeVersion, ParseBaseUrlError}, json_schema::{SingleValueConstraints, ValueConstraints}, provenance::{OntologyOwnership, ProvidedOntologyEditionProvenance}, }; @@ -216,7 +216,7 @@ impl Producer for DataTypePr slug_from_title(&title) ))?, version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }, diff --git a/tests/graph/test-data/rust/src/seeding/producer/entity_type.rs b/tests/graph/test-data/rust/src/seeding/producer/entity_type.rs index f4a2f05fca7..50b95c8bf3f 100644 --- a/tests/graph/test-data/rust/src/seeding/producer/entity_type.rs +++ b/tests/graph/test-data/rust/src/seeding/producer/entity_type.rs @@ -13,7 +13,7 @@ use type_system::ontology::{ InverseEntityTypeMetadata, }, }, - id::{OntologyTypeVersion, ParseBaseUrlError}, + id::{OntologyTypeMajorVersion, OntologyTypeVersion, ParseBaseUrlError}, json_schema::ObjectTypeTag, provenance::{OntologyOwnership, ProvidedOntologyEditionProvenance}, }; @@ -290,7 +290,7 @@ impl< slug_from_title(&title) ))?, version: OntologyTypeVersion { - major: 1, + major: OntologyTypeMajorVersion::MIN, pre_release: None, }, }, diff --git a/tests/graph/test-data/rust/src/seeding/producer/property_type.rs b/tests/graph/test-data/rust/src/seeding/producer/property_type.rs index 897d1b452c8..edc0e15f669 100644 --- a/tests/graph/test-data/rust/src/seeding/producer/property_type.rs +++ b/tests/graph/test-data/rust/src/seeding/producer/property_type.rs @@ -5,7 +5,7 @@ use hash_graph_store::{property_type::CreatePropertyTypeParams, query::ConflictB use rand::{distr::Distribution as _, seq::IndexedRandom as _}; use type_system::ontology::{ BaseUrl, VersionedUrl, - id::{OntologyTypeVersion, ParseBaseUrlError}, + id::{OntologyTypeMajorVersion, OntologyTypeVersion, ParseBaseUrlError}, property_type::{PropertyType, schema::PropertyTypeReference}, provenance::{OntologyOwnership, ProvidedOntologyEditionProvenance}, }; @@ -209,7 +209,7 @@ impl Producer