Add validator lib

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-11-03 08:19:09 +01:00
committed by Sacha Al Himdani
parent 288c59a5f2
commit f9216d30b2
102 changed files with 7409 additions and 1068 deletions

View File

@@ -1,7 +1,7 @@
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { Option } from "@probo/ui"; import { Option } from "@probo/ui";
import type { import type {
ProcessingActivitySpecialOrCriminalData, ProcessingActivitySpecialOrCriminalDatum,
ProcessingActivityLawfulBasis, ProcessingActivityLawfulBasis,
ProcessingActivityDataProtectionImpactAssessment, ProcessingActivityDataProtectionImpactAssessment,
ProcessingActivityTransferImpactAssessment, ProcessingActivityTransferImpactAssessment,
@@ -11,7 +11,7 @@ export function SpecialOrCriminalDataOptions() {
const { __ } = useTranslate(); const { __ } = useTranslate();
const options: Array<{ const options: Array<{
value: ProcessingActivitySpecialOrCriminalData; value: ProcessingActivitySpecialOrCriminalDatum;
label: string; label: string;
}> = [ }> = [
{ value: "YES", label: __("Yes") }, { value: "YES", label: __("Yes") },
@@ -56,16 +56,19 @@ export function LawfulBasisOptions() {
); );
} }
export function getLawfulBasisLabel(value: ProcessingActivityLawfulBasis | null | undefined, __: (key: string) => string): string { export function getLawfulBasisLabel(
value: ProcessingActivityLawfulBasis | null | undefined,
__: (key: string) => string
): string {
if (!value) return "-"; if (!value) return "-";
const labels = { const labels = {
"CONSENT": __("Consent"), CONSENT: __("Consent"),
"CONTRACTUAL_NECESSITY": __("Contractual Necessity"), CONTRACTUAL_NECESSITY: __("Contractual Necessity"),
"LEGAL_OBLIGATION": __("Legal Obligation"), LEGAL_OBLIGATION: __("Legal Obligation"),
"LEGITIMATE_INTEREST": __("Legitimate Interest"), LEGITIMATE_INTEREST: __("Legitimate Interest"),
"PUBLIC_TASK": __("Public Task"), PUBLIC_TASK: __("Public Task"),
"VITAL_INTERESTS": __("Vital Interests"), VITAL_INTERESTS: __("Vital Interests"),
}; };
return labels[value] || value; return labels[value] || value;
@@ -79,12 +82,18 @@ export function TransferSafeguardsOptions() {
label: string; label: string;
}> = [ }> = [
{ value: "__NONE__", label: __("None") }, { value: "__NONE__", label: __("None") },
{ value: "STANDARD_CONTRACTUAL_CLAUSES", label: __("Standard Contractual Clauses") }, {
value: "STANDARD_CONTRACTUAL_CLAUSES",
label: __("Standard Contractual Clauses"),
},
{ value: "BINDING_CORPORATE_RULES", label: __("Binding Corporate Rules") }, { value: "BINDING_CORPORATE_RULES", label: __("Binding Corporate Rules") },
{ value: "ADEQUACY_DECISION", label: __("Adequacy Decision") }, { value: "ADEQUACY_DECISION", label: __("Adequacy Decision") },
{ value: "DEROGATIONS", label: __("Derogations") }, { value: "DEROGATIONS", label: __("Derogations") },
{ value: "CODES_OF_CONDUCT", label: __("Codes of Conduct") }, { value: "CODES_OF_CONDUCT", label: __("Codes of Conduct") },
{ value: "CERTIFICATION_MECHANISMS", label: __("Certification Mechanisms") }, {
value: "CERTIFICATION_MECHANISMS",
label: __("Certification Mechanisms"),
},
]; ];
return ( return (

View File

@@ -107,6 +107,14 @@ export function useVendorForm(vendorKey: useVendorFormFragment$key) {
id: vendor.id, id: vendor.id,
...data, ...data,
description: data.description || null, description: data.description || null,
statusPageUrl: data.statusPageUrl || null,
termsOfServiceUrl: data.termsOfServiceUrl || null,
privacyPolicyUrl: data.privacyPolicyUrl || null,
serviceLevelAgreementUrl: data.serviceLevelAgreementUrl || null,
dataProcessingAgreementUrl: data.dataProcessingAgreementUrl || null,
websiteUrl: data.websiteUrl || null,
securityPageUrl: data.securityPageUrl || null,
trustPageUrl: data.trustPageUrl || null,
}, },
}, },
}).then(() => { }).then(() => {

View File

@@ -3,6 +3,7 @@ import { useMutation } from "react-relay";
import { useConfirm } from "@probo/ui"; import { useConfirm } from "@probo/ui";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { promisifyMutation, sprintf } from "@probo/helpers"; import { promisifyMutation, sprintf } from "@probo/helpers";
import { useMutationWithToasts } from "../useMutationWithToasts";
export const assetsQuery = graphql` export const assetsQuery = graphql`
query AssetGraphListQuery($organizationId: ID!, $snapshotId: ID) { query AssetGraphListQuery($organizationId: ID!, $snapshotId: ID) {
@@ -197,8 +198,11 @@ export const useCreateAsset = (connectionId: string) => {
}; };
export const useUpdateAsset = () => { export const useUpdateAsset = () => {
const [mutate] = useMutation(updateAssetMutation);
const { __ } = useTranslate(); const { __ } = useTranslate();
const [mutate] = useMutationWithToasts(updateAssetMutation, {
successMessage: __("Asset updated successfully"),
errorMessage: __("Failed to update asset"),
});
return (input: { return (input: {
id: string; id: string;
@@ -213,7 +217,7 @@ export const useUpdateAsset = () => {
return alert(__("Failed to update asset: asset ID is required")); return alert(__("Failed to update asset: asset ID is required"));
} }
return promisifyMutation(mutate)({ return mutate({
variables: { variables: {
input, input,
}, },

View File

@@ -151,7 +151,7 @@ export const useCreateAudit = (connectionId: string) => {
return (input: { return (input: {
organizationId: string; organizationId: string;
frameworkId: string; frameworkId: string;
name?: string; name?: string | null;
validFrom?: string; validFrom?: string;
validUntil?: string; validUntil?: string;
reportKey?: string; reportKey?: string;
@@ -187,7 +187,7 @@ export const useUpdateAudit = () => {
return (input: { return (input: {
id: string; id: string;
name?: string; name?: string | null;
validFrom?: string | null; validFrom?: string | null;
validUntil?: string | null; validUntil?: string | null;
state?: string; state?: string;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<d01ca9f47a4ce6e732cffd5deb9150c6>> * @generated SignedSource<<a804557b48f2dbc4749c8399e708b9fe>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -11,9 +11,9 @@
import { ConcreteRequest } from 'relay-runtime'; import { ConcreteRequest } from 'relay-runtime';
export type ProcessingActivityDataProtectionImpactAssessment = "NEEDED" | "NOT_NEEDED"; export type ProcessingActivityDataProtectionImpactAssessment = "NEEDED" | "NOT_NEEDED";
export type ProcessingActivityLawfulBasis = "CONSENT" | "CONTRACTUAL_NECESSITY" | "LEGAL_OBLIGATION" | "LEGITIMATE_INTEREST" | "PUBLIC_TASK" | "VITAL_INTERESTS"; export type ProcessingActivityLawfulBasis = "CONSENT" | "CONTRACTUAL_NECESSITY" | "LEGAL_OBLIGATION" | "LEGITIMATE_INTEREST" | "PUBLIC_TASK" | "VITAL_INTERESTS";
export type ProcessingActivitySpecialOrCriminalData = "NO" | "POSSIBLE" | "YES"; export type ProcessingActivitySpecialOrCriminalDatum = "NO" | "POSSIBLE" | "YES";
export type ProcessingActivityTransferImpactAssessment = "NEEDED" | "NOT_NEEDED"; export type ProcessingActivityTransferImpactAssessment = "NEEDED" | "NOT_NEEDED";
export type ProcessingActivityTransferSafeguards = "ADEQUACY_DECISION" | "BINDING_CORPORATE_RULES" | "CERTIFICATION_MECHANISMS" | "CODES_OF_CONDUCT" | "DEROGATIONS" | "STANDARD_CONTRACTUAL_CLAUSES"; export type ProcessingActivityTransferSafeguard = "ADEQUACY_DECISION" | "BINDING_CORPORATE_RULES" | "CERTIFICATION_MECHANISMS" | "CODES_OF_CONDUCT" | "DEROGATIONS" | "STANDARD_CONTRACTUAL_CLAUSES";
export type CreateProcessingActivityInput = { export type CreateProcessingActivityInput = {
consentEvidenceLink?: string | null | undefined; consentEvidenceLink?: string | null | undefined;
dataProtectionImpactAssessment: ProcessingActivityDataProtectionImpactAssessment; dataProtectionImpactAssessment: ProcessingActivityDataProtectionImpactAssessment;
@@ -28,9 +28,9 @@ export type CreateProcessingActivityInput = {
recipients?: string | null | undefined; recipients?: string | null | undefined;
retentionPeriod?: string | null | undefined; retentionPeriod?: string | null | undefined;
securityMeasures?: string | null | undefined; securityMeasures?: string | null | undefined;
specialOrCriminalData: ProcessingActivitySpecialOrCriminalData; specialOrCriminalData: ProcessingActivitySpecialOrCriminalDatum;
transferImpactAssessment: ProcessingActivityTransferImpactAssessment; transferImpactAssessment: ProcessingActivityTransferImpactAssessment;
transferSafeguards?: ProcessingActivityTransferSafeguards | null | undefined; transferSafeguards?: ProcessingActivityTransferSafeguard | null | undefined;
vendorIds?: ReadonlyArray<string> | null | undefined; vendorIds?: ReadonlyArray<string> | null | undefined;
}; };
export type ProcessingActivityGraphCreateMutation$variables = { export type ProcessingActivityGraphCreateMutation$variables = {
@@ -55,9 +55,9 @@ export type ProcessingActivityGraphCreateMutation$data = {
readonly recipients: string | null | undefined; readonly recipients: string | null | undefined;
readonly retentionPeriod: string | null | undefined; readonly retentionPeriod: string | null | undefined;
readonly securityMeasures: string | null | undefined; readonly securityMeasures: string | null | undefined;
readonly specialOrCriminalData: ProcessingActivitySpecialOrCriminalData; readonly specialOrCriminalData: ProcessingActivitySpecialOrCriminalDatum;
readonly transferImpactAssessment: ProcessingActivityTransferImpactAssessment; readonly transferImpactAssessment: ProcessingActivityTransferImpactAssessment;
readonly transferSafeguards: ProcessingActivityTransferSafeguards | null | undefined; readonly transferSafeguards: ProcessingActivityTransferSafeguard | null | undefined;
readonly vendors: { readonly vendors: {
readonly edges: ReadonlyArray<{ readonly edges: ReadonlyArray<{
readonly node: { readonly node: {

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<5dea7ccaf98a1223d9e4cc1ecdfbb0bc>> * @generated SignedSource<<736925258373b1116c286fc16d19ad7d>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -11,9 +11,9 @@
import { ConcreteRequest } from 'relay-runtime'; import { ConcreteRequest } from 'relay-runtime';
export type ProcessingActivityDataProtectionImpactAssessment = "NEEDED" | "NOT_NEEDED"; export type ProcessingActivityDataProtectionImpactAssessment = "NEEDED" | "NOT_NEEDED";
export type ProcessingActivityLawfulBasis = "CONSENT" | "CONTRACTUAL_NECESSITY" | "LEGAL_OBLIGATION" | "LEGITIMATE_INTEREST" | "PUBLIC_TASK" | "VITAL_INTERESTS"; export type ProcessingActivityLawfulBasis = "CONSENT" | "CONTRACTUAL_NECESSITY" | "LEGAL_OBLIGATION" | "LEGITIMATE_INTEREST" | "PUBLIC_TASK" | "VITAL_INTERESTS";
export type ProcessingActivitySpecialOrCriminalData = "NO" | "POSSIBLE" | "YES"; export type ProcessingActivitySpecialOrCriminalDatum = "NO" | "POSSIBLE" | "YES";
export type ProcessingActivityTransferImpactAssessment = "NEEDED" | "NOT_NEEDED"; export type ProcessingActivityTransferImpactAssessment = "NEEDED" | "NOT_NEEDED";
export type ProcessingActivityTransferSafeguards = "ADEQUACY_DECISION" | "BINDING_CORPORATE_RULES" | "CERTIFICATION_MECHANISMS" | "CODES_OF_CONDUCT" | "DEROGATIONS" | "STANDARD_CONTRACTUAL_CLAUSES"; export type ProcessingActivityTransferSafeguard = "ADEQUACY_DECISION" | "BINDING_CORPORATE_RULES" | "CERTIFICATION_MECHANISMS" | "CODES_OF_CONDUCT" | "DEROGATIONS" | "STANDARD_CONTRACTUAL_CLAUSES";
export type VendorCategory = "ANALYTICS" | "CLOUD_MONITORING" | "CLOUD_PROVIDER" | "COLLABORATION" | "CUSTOMER_SUPPORT" | "DATA_STORAGE_AND_PROCESSING" | "DOCUMENT_MANAGEMENT" | "EMPLOYEE_MANAGEMENT" | "ENGINEERING" | "FINANCE" | "IDENTITY_PROVIDER" | "IT" | "MARKETING" | "OFFICE_OPERATIONS" | "OTHER" | "PASSWORD_MANAGEMENT" | "PRODUCT_AND_DESIGN" | "PROFESSIONAL_SERVICES" | "RECRUITING" | "SALES" | "SECURITY" | "VERSION_CONTROL"; export type VendorCategory = "ANALYTICS" | "CLOUD_MONITORING" | "CLOUD_PROVIDER" | "COLLABORATION" | "CUSTOMER_SUPPORT" | "DATA_STORAGE_AND_PROCESSING" | "DOCUMENT_MANAGEMENT" | "EMPLOYEE_MANAGEMENT" | "ENGINEERING" | "FINANCE" | "IDENTITY_PROVIDER" | "IT" | "MARKETING" | "OFFICE_OPERATIONS" | "OTHER" | "PASSWORD_MANAGEMENT" | "PRODUCT_AND_DESIGN" | "PROFESSIONAL_SERVICES" | "RECRUITING" | "SALES" | "SECURITY" | "VERSION_CONTROL";
export type ProcessingActivityGraphNodeQuery$variables = { export type ProcessingActivityGraphNodeQuery$variables = {
processingActivityId: string; processingActivityId: string;
@@ -39,9 +39,9 @@ export type ProcessingActivityGraphNodeQuery$data = {
readonly retentionPeriod?: string | null | undefined; readonly retentionPeriod?: string | null | undefined;
readonly securityMeasures?: string | null | undefined; readonly securityMeasures?: string | null | undefined;
readonly snapshotId?: string | null | undefined; readonly snapshotId?: string | null | undefined;
readonly specialOrCriminalData?: ProcessingActivitySpecialOrCriminalData; readonly specialOrCriminalData?: ProcessingActivitySpecialOrCriminalDatum;
readonly transferImpactAssessment?: ProcessingActivityTransferImpactAssessment; readonly transferImpactAssessment?: ProcessingActivityTransferImpactAssessment;
readonly transferSafeguards?: ProcessingActivityTransferSafeguards | null | undefined; readonly transferSafeguards?: ProcessingActivityTransferSafeguard | null | undefined;
readonly updatedAt?: any; readonly updatedAt?: any;
readonly vendors?: { readonly vendors?: {
readonly edges: ReadonlyArray<{ readonly edges: ReadonlyArray<{

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<08099a83039838c6c79dbf1ca7c99034>> * @generated SignedSource<<7004a9f42c1d7e16eadf8adbdb613b2a>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -11,9 +11,9 @@
import { ConcreteRequest } from 'relay-runtime'; import { ConcreteRequest } from 'relay-runtime';
export type ProcessingActivityDataProtectionImpactAssessment = "NEEDED" | "NOT_NEEDED"; export type ProcessingActivityDataProtectionImpactAssessment = "NEEDED" | "NOT_NEEDED";
export type ProcessingActivityLawfulBasis = "CONSENT" | "CONTRACTUAL_NECESSITY" | "LEGAL_OBLIGATION" | "LEGITIMATE_INTEREST" | "PUBLIC_TASK" | "VITAL_INTERESTS"; export type ProcessingActivityLawfulBasis = "CONSENT" | "CONTRACTUAL_NECESSITY" | "LEGAL_OBLIGATION" | "LEGITIMATE_INTEREST" | "PUBLIC_TASK" | "VITAL_INTERESTS";
export type ProcessingActivitySpecialOrCriminalData = "NO" | "POSSIBLE" | "YES"; export type ProcessingActivitySpecialOrCriminalDatum = "NO" | "POSSIBLE" | "YES";
export type ProcessingActivityTransferImpactAssessment = "NEEDED" | "NOT_NEEDED"; export type ProcessingActivityTransferImpactAssessment = "NEEDED" | "NOT_NEEDED";
export type ProcessingActivityTransferSafeguards = "ADEQUACY_DECISION" | "BINDING_CORPORATE_RULES" | "CERTIFICATION_MECHANISMS" | "CODES_OF_CONDUCT" | "DEROGATIONS" | "STANDARD_CONTRACTUAL_CLAUSES"; export type ProcessingActivityTransferSafeguard = "ADEQUACY_DECISION" | "BINDING_CORPORATE_RULES" | "CERTIFICATION_MECHANISMS" | "CODES_OF_CONDUCT" | "DEROGATIONS" | "STANDARD_CONTRACTUAL_CLAUSES";
export type UpdateProcessingActivityInput = { export type UpdateProcessingActivityInput = {
consentEvidenceLink?: string | null | undefined; consentEvidenceLink?: string | null | undefined;
dataProtectionImpactAssessment?: ProcessingActivityDataProtectionImpactAssessment | null | undefined; dataProtectionImpactAssessment?: ProcessingActivityDataProtectionImpactAssessment | null | undefined;
@@ -28,9 +28,9 @@ export type UpdateProcessingActivityInput = {
recipients?: string | null | undefined; recipients?: string | null | undefined;
retentionPeriod?: string | null | undefined; retentionPeriod?: string | null | undefined;
securityMeasures?: string | null | undefined; securityMeasures?: string | null | undefined;
specialOrCriminalData?: ProcessingActivitySpecialOrCriminalData | null | undefined; specialOrCriminalData?: ProcessingActivitySpecialOrCriminalDatum | null | undefined;
transferImpactAssessment?: ProcessingActivityTransferImpactAssessment | null | undefined; transferImpactAssessment?: ProcessingActivityTransferImpactAssessment | null | undefined;
transferSafeguards?: ProcessingActivityTransferSafeguards | null | undefined; transferSafeguards?: ProcessingActivityTransferSafeguard | null | undefined;
vendorIds?: ReadonlyArray<string> | null | undefined; vendorIds?: ReadonlyArray<string> | null | undefined;
}; };
export type ProcessingActivityGraphUpdateMutation$variables = { export type ProcessingActivityGraphUpdateMutation$variables = {
@@ -52,9 +52,9 @@ export type ProcessingActivityGraphUpdateMutation$data = {
readonly recipients: string | null | undefined; readonly recipients: string | null | undefined;
readonly retentionPeriod: string | null | undefined; readonly retentionPeriod: string | null | undefined;
readonly securityMeasures: string | null | undefined; readonly securityMeasures: string | null | undefined;
readonly specialOrCriminalData: ProcessingActivitySpecialOrCriminalData; readonly specialOrCriminalData: ProcessingActivitySpecialOrCriminalDatum;
readonly transferImpactAssessment: ProcessingActivityTransferImpactAssessment; readonly transferImpactAssessment: ProcessingActivityTransferImpactAssessment;
readonly transferSafeguards: ProcessingActivityTransferSafeguards | null | undefined; readonly transferSafeguards: ProcessingActivityTransferSafeguard | null | undefined;
readonly updatedAt: any; readonly updatedAt: any;
readonly vendors: { readonly vendors: {
readonly edges: ReadonlyArray<{ readonly edges: ReadonlyArray<{

View File

@@ -80,15 +80,11 @@ export default function AssetDetailsPage(props: Props) {
const updateAsset = useUpdateAsset(); const updateAsset = useUpdateAsset();
const onSubmit = handleSubmit(async (formData) => { const onSubmit = handleSubmit(async (formData) => {
try { await updateAsset({
await updateAsset({ id: assetEntry?.id,
id: assetEntry?.id, ...formData,
...formData, });
}); reset(formData);
reset(formData);
} catch (error) {
console.error("Failed to update asset:", error);
}
}); });
const breadcrumbAssetsUrl = isSnapshotMode && snapshotId const breadcrumbAssetsUrl = isSnapshotMode && snapshotId

View File

@@ -36,7 +36,7 @@ import { getAuditStateLabel, getAuditStateVariant, auditStates, fileSize, sprint
import type { AuditGraphNodeQuery } from "/hooks/graph/__generated__/AuditGraphNodeQuery.graphql"; import type { AuditGraphNodeQuery } from "/hooks/graph/__generated__/AuditGraphNodeQuery.graphql";
const updateAuditSchema = z.object({ const updateAuditSchema = z.object({
name: z.string().optional(), name: z.string().nullable().optional(),
validFrom: z.string().optional(), validFrom: z.string().optional(),
validUntil: z.string().optional(), validUntil: z.string().optional(),
state: z.enum(["NOT_STARTED", "IN_PROGRESS", "COMPLETED", "REJECTED", "OUTDATED"]), state: z.enum(["NOT_STARTED", "IN_PROGRESS", "COMPLETED", "REJECTED", "OUTDATED"]),
@@ -63,7 +63,7 @@ export default function AuditDetailsPage(props: Props) {
const { control, formState, handleSubmit, register, reset } = useFormWithSchema(updateAuditSchema, { const { control, formState, handleSubmit, register, reset } = useFormWithSchema(updateAuditSchema, {
defaultValues: { defaultValues: {
name: auditEntry.name || "", name: auditEntry.name || null,
validFrom: auditEntry.validFrom?.split('T')[0] || "", validFrom: auditEntry.validFrom?.split('T')[0] || "",
validUntil: auditEntry.validUntil?.split('T')[0] || "", validUntil: auditEntry.validUntil?.split('T')[0] || "",
state: auditEntry.state || "NOT_STARTED", state: auditEntry.state || "NOT_STARTED",
@@ -82,7 +82,7 @@ export default function AuditDetailsPage(props: Props) {
try { try {
await updateAudit({ await updateAudit({
id: auditEntry.id, id: auditEntry.id,
name: formData.name, name: formData.name || null,
validFrom: formatDatetime(formData.validFrom) ?? null, validFrom: formatDatetime(formData.validFrom) ?? null,
validUntil: formatDatetime(formData.validUntil) ?? null, validUntil: formatDatetime(formData.validUntil) ?? null,
state: formData.state, state: formData.state,

View File

@@ -79,7 +79,7 @@ export function CreateAuditDialog({
await createAudit({ await createAudit({
organizationId, organizationId,
frameworkId: data.frameworkId, frameworkId: data.frameworkId,
name: data.name, name: data.name || null,
validFrom: formatDatetime(data.validFrom), validFrom: formatDatetime(data.validFrom),
validUntil: formatDatetime(data.validUntil), validUntil: formatDatetime(data.validUntil),
state: data.state, state: data.state,

View File

@@ -28,6 +28,13 @@ const (
AssetTypeVirtual AssetType = "VIRTUAL" AssetTypeVirtual AssetType = "VIRTUAL"
) )
func AssetTypes() []AssetType {
return []AssetType{
AssetTypePhysical,
AssetTypeVirtual,
}
}
func (at AssetType) MarshalText() ([]byte, error) { func (at AssetType) MarshalText() ([]byte, error) {
return []byte(at.String()), nil return []byte(at.String()), nil
} }

View File

@@ -29,6 +29,16 @@ const (
AuditStateOutdated AuditState = "OUTDATED" AuditStateOutdated AuditState = "OUTDATED"
) )
func AuditStates() []AuditState {
return []AuditState{
AuditStateNotStarted,
AuditStateInProgress,
AuditStateCompleted,
AuditStateRejected,
AuditStateOutdated,
}
}
func (as AuditState) String() string { func (as AuditState) String() string {
return string(as) return string(as)
} }

View File

@@ -29,6 +29,15 @@ const (
BusinessImpactCritical BusinessImpact = "CRITICAL" BusinessImpactCritical BusinessImpact = "CRITICAL"
) )
func BusinessImpacts() []BusinessImpact {
return []BusinessImpact{
BusinessImpactLow,
BusinessImpactMedium,
BusinessImpactHigh,
BusinessImpactCritical,
}
}
func (i BusinessImpact) String() string { func (i BusinessImpact) String() string {
return string(i) return string(i)
} }

View File

@@ -25,6 +25,12 @@ const (
ConnectorProtocolOAuth2 ConnectorProtocol = "OAUTH2" ConnectorProtocolOAuth2 ConnectorProtocol = "OAUTH2"
) )
func ConnectorProtocols() []ConnectorProtocol {
return []ConnectorProtocol{
ConnectorProtocolOAuth2,
}
}
func (cp ConnectorProtocol) String() string { func (cp ConnectorProtocol) String() string {
return string(cp) return string(cp)
} }

View File

@@ -25,6 +25,12 @@ const (
ConnectorProviderSlack ConnectorProvider = "SLACK" ConnectorProviderSlack ConnectorProvider = "SLACK"
) )
func ConnectorProviders() []ConnectorProvider {
return []ConnectorProvider{
ConnectorProviderSlack,
}
}
func (cp ConnectorProvider) String() string { func (cp ConnectorProvider) String() string {
return string(cp) return string(cp)
} }

View File

@@ -27,6 +27,14 @@ const (
ContinualImprovementPriorityHigh ContinualImprovementPriority = "HIGH" ContinualImprovementPriorityHigh ContinualImprovementPriority = "HIGH"
) )
func ContinualImprovementPriorities() []ContinualImprovementPriority {
return []ContinualImprovementPriority{
ContinualImprovementPriorityLow,
ContinualImprovementPriorityMedium,
ContinualImprovementPriorityHigh,
}
}
func (cip ContinualImprovementPriority) String() string { func (cip ContinualImprovementPriority) String() string {
return string(cip) return string(cip)
} }

View File

@@ -27,6 +27,14 @@ const (
ContinualImprovementStatusClosed ContinualImprovementStatus = "CLOSED" ContinualImprovementStatusClosed ContinualImprovementStatus = "CLOSED"
) )
func ContinualImprovementStatuses() []ContinualImprovementStatus {
return []ContinualImprovementStatus{
ContinualImprovementStatusOpen,
ContinualImprovementStatusInProgress,
ContinualImprovementStatusClosed,
}
}
func (cis ContinualImprovementStatus) String() string { func (cis ContinualImprovementStatus) String() string {
return string(cis) return string(cis)
} }

View File

@@ -26,6 +26,13 @@ const (
ControlStatusExcluded ControlStatus = "EXCLUDED" ControlStatusExcluded ControlStatus = "EXCLUDED"
) )
func ControlStatuses() []ControlStatus {
return []ControlStatus{
ControlStatusIncluded,
ControlStatusExcluded,
}
}
func (cs ControlStatus) String() string { func (cs ControlStatus) String() string {
return string(cs) return string(cs)
} }

View File

@@ -22,3 +22,12 @@ const (
DataClassificationConfidential DataClassification = "CONFIDENTIAL" DataClassificationConfidential DataClassification = "CONFIDENTIAL"
DataClassificationSecret DataClassification = "SECRET" DataClassificationSecret DataClassification = "SECRET"
) )
func DataClassifications() []DataClassification {
return []DataClassification{
DataClassificationPublic,
DataClassificationInternal,
DataClassificationConfidential,
DataClassificationSecret,
}
}

View File

@@ -30,6 +30,16 @@ const (
DataSensitivityCritical DataSensitivity = "CRITICAL" DataSensitivityCritical DataSensitivity = "CRITICAL"
) )
func DataSensitivities() []DataSensitivity {
return []DataSensitivity{
DataSensitivityNone,
DataSensitivityLow,
DataSensitivityMedium,
DataSensitivityHigh,
DataSensitivityCritical,
}
}
func (i DataSensitivity) String() string { func (i DataSensitivity) String() string {
return string(i) return string(i)
} }

View File

@@ -14,6 +14,15 @@ const (
DocumentClassificationSecret DocumentClassification = "SECRET" DocumentClassificationSecret DocumentClassification = "SECRET"
) )
func DocumentClassifications() []DocumentClassification {
return []DocumentClassification{
DocumentClassificationPublic,
DocumentClassificationInternal,
DocumentClassificationConfidential,
DocumentClassificationSecret,
}
}
func (dc DocumentClassification) String() string { func (dc DocumentClassification) String() string {
switch dc { switch dc {
case DocumentClassificationPublic: case DocumentClassificationPublic:

View File

@@ -30,6 +30,15 @@ const (
DocumentTypeProcedure DocumentType = "PROCEDURE" DocumentTypeProcedure DocumentType = "PROCEDURE"
) )
func DocumentTypes() []DocumentType {
return []DocumentType{
DocumentTypeOther,
DocumentTypeISMS,
DocumentTypePolicy,
DocumentTypeProcedure,
}
}
func (dt DocumentType) MarshalText() ([]byte, error) { func (dt DocumentType) MarshalText() ([]byte, error) {
return []byte(dt.String()), nil return []byte(dt.String()), nil
} }

View File

@@ -21,49 +21,49 @@ var (
) )
const ( const (
OrganizationEntityType uint16 = iota OrganizationEntityType uint16 = 0
FrameworkEntityType FrameworkEntityType uint16 = 1
MeasureEntityType MeasureEntityType uint16 = 2
TaskEntityType TaskEntityType uint16 = 3
EvidenceEntityType EvidenceEntityType uint16 = 4
ConnectorEntityType ConnectorEntityType uint16 = 5
VendorRiskAssessmentEntityType VendorRiskAssessmentEntityType uint16 = 6
VendorEntityType VendorEntityType uint16 = 7
PeopleEntityType PeopleEntityType uint16 = 8
VendorComplianceReportEntityType VendorComplianceReportEntityType uint16 = 9
DocumentEntityType DocumentEntityType uint16 = 10
UserEntityType UserEntityType uint16 = 11
SessionEntityType SessionEntityType uint16 = 12
EmailEntityType EmailEntityType uint16 = 13
ControlEntityType ControlEntityType uint16 = 14
RiskEntityType RiskEntityType uint16 = 15
DocumentVersionEntityType DocumentVersionEntityType uint16 = 16
DocumentVersionSignatureEntityType DocumentVersionSignatureEntityType uint16 = 17
AssetEntityType AssetEntityType uint16 = 18
DatumEntityType DatumEntityType uint16 = 19
AuditEntityType AuditEntityType uint16 = 20
ReportEntityType ReportEntityType uint16 = 21
TrustCenterEntityType TrustCenterEntityType uint16 = 22
TrustCenterAccessEntityType TrustCenterAccessEntityType uint16 = 23
VendorBusinessAssociateAgreementEntityType VendorBusinessAssociateAgreementEntityType uint16 = 24
FileEntityType FileEntityType uint16 = 25
VendorContactEntityType VendorContactEntityType uint16 = 26
VendorDataPrivacyAgreementEntityType VendorDataPrivacyAgreementEntityType uint16 = 27
NonconformityEntityType NonconformityEntityType uint16 = 28
ObligationEntityType ObligationEntityType uint16 = 29
VendorServiceEntityType VendorServiceEntityType uint16 = 30
SnapshotEntityType SnapshotEntityType uint16 = 31
ContinualImprovementEntityType ContinualImprovementEntityType uint16 = 32
ProcessingActivityEntityType ProcessingActivityEntityType uint16 = 33
ExportJobEntityType ExportJobEntityType uint16 = 34
TrustCenterReferenceEntityType TrustCenterReferenceEntityType uint16 = 35
TrustCenterDocumentAccessEntityType TrustCenterDocumentAccessEntityType uint16 = 36
CustomDomainEntityType CustomDomainEntityType uint16 = 37
InvitationEntityType InvitationEntityType uint16 = 38
MembershipEntityType MembershipEntityType uint16 = 39
SlackMessageEntityType SlackMessageEntityType uint16 = 40
TrustCenterFileEntityType TrustCenterFileEntityType uint16 = 41
SAMLConfigurationEntityType SAMLConfigurationEntityType uint16 = 42
UserAPIKeyEntityType UserAPIKeyEntityType uint16 = 43
UserAPIKeyMembershipEntityType UserAPIKeyMembershipEntityType uint16 = 44
) )

View File

@@ -30,6 +30,15 @@ const (
MeasureStateImplemented MeasureStateImplemented
) )
func MeasureStates() []MeasureState {
return []MeasureState{
MeasureStateNotStarted,
MeasureStateInProgress,
MeasureStateNotApplicable,
MeasureStateImplemented,
}
}
func (ms MeasureState) MarshalText() ([]byte, error) { func (ms MeasureState) MarshalText() ([]byte, error) {
return []byte(ms.String()), nil return []byte(ms.String()), nil
} }

View File

@@ -27,6 +27,14 @@ const (
NonconformityStatusClosed NonconformityStatus = "CLOSED" NonconformityStatusClosed NonconformityStatus = "CLOSED"
) )
func NonconformityStatuses() []NonconformityStatus {
return []NonconformityStatus{
NonconformityStatusOpen,
NonconformityStatusInProgress,
NonconformityStatusClosed,
}
}
func (ncs NonconformityStatus) String() string { func (ncs NonconformityStatus) String() string {
return string(ncs) return string(ncs)
} }

View File

@@ -27,6 +27,14 @@ const (
ObligationStatusCompliant ObligationStatus = "COMPLIANT" ObligationStatusCompliant ObligationStatus = "COMPLIANT"
) )
func ObligationStatuses() []ObligationStatus {
return []ObligationStatus{
ObligationStatusNonCompliant,
ObligationStatusPartiallyCompliant,
ObligationStatusCompliant,
}
}
func (os ObligationStatus) String() string { func (os ObligationStatus) String() string {
return string(os) return string(os)
} }

View File

@@ -29,6 +29,14 @@ const (
PeopleKindServiceAccount PeopleKindServiceAccount
) )
func PeopleKinds() []PeopleKind {
return []PeopleKind{
PeopleKindEmployee,
PeopleKindContractor,
PeopleKindServiceAccount,
}
}
func (ps PeopleKind) MarshalText() ([]byte, error) { func (ps PeopleKind) MarshalText() ([]byte, error) {
return []byte(ps.String()), nil return []byte(ps.String()), nil
} }

View File

@@ -20,10 +20,10 @@ import (
"maps" "maps"
"time" "time"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg" "go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
) )
type ( type (
@@ -36,13 +36,13 @@ type (
Purpose *string `db:"purpose"` Purpose *string `db:"purpose"`
DataSubjectCategory *string `db:"data_subject_category"` DataSubjectCategory *string `db:"data_subject_category"`
PersonalDataCategory *string `db:"personal_data_category"` PersonalDataCategory *string `db:"personal_data_category"`
SpecialOrCriminalData ProcessingActivitySpecialOrCriminalData `db:"special_or_criminal_data"` SpecialOrCriminalData ProcessingActivitySpecialOrCriminalDatum `db:"special_or_criminal_data"`
ConsentEvidenceLink *string `db:"consent_evidence_link"` ConsentEvidenceLink *string `db:"consent_evidence_link"`
LawfulBasis ProcessingActivityLawfulBasis `db:"lawful_basis"` LawfulBasis ProcessingActivityLawfulBasis `db:"lawful_basis"`
Recipients *string `db:"recipients"` Recipients *string `db:"recipients"`
Location *string `db:"location"` Location *string `db:"location"`
InternationalTransfers bool `db:"international_transfers"` InternationalTransfers bool `db:"international_transfers"`
TransferSafeguards *ProcessingActivityTransferSafeguards `db:"transfer_safeguards"` TransferSafeguard *ProcessingActivityTransferSafeguard `db:"transfer_safeguards"`
RetentionPeriod *string `db:"retention_period"` RetentionPeriod *string `db:"retention_period"`
SecurityMeasures *string `db:"security_measures"` SecurityMeasures *string `db:"security_measures"`
DataProtectionImpactAssessment ProcessingActivityDataProtectionImpactAssessment `db:"data_protection_impact_assessment"` DataProtectionImpactAssessment ProcessingActivityDataProtectionImpactAssessment `db:"data_protection_impact_assessment"`
@@ -290,7 +290,7 @@ INSERT INTO processing_activities (
"recipients": p.Recipients, "recipients": p.Recipients,
"location": p.Location, "location": p.Location,
"international_transfers": p.InternationalTransfers, "international_transfers": p.InternationalTransfers,
"transfer_safeguards": p.TransferSafeguards, "transfer_safeguards": p.TransferSafeguard,
"retention_period": p.RetentionPeriod, "retention_period": p.RetentionPeriod,
"security_measures": p.SecurityMeasures, "security_measures": p.SecurityMeasures,
"data_protection_impact_assessment": p.DataProtectionImpactAssessment, "data_protection_impact_assessment": p.DataProtectionImpactAssessment,
@@ -351,7 +351,7 @@ WHERE
"recipients": p.Recipients, "recipients": p.Recipients,
"location": p.Location, "location": p.Location,
"international_transfers": p.InternationalTransfers, "international_transfers": p.InternationalTransfers,
"transfer_safeguards": p.TransferSafeguards, "transfer_safeguards": p.TransferSafeguard,
"retention_period": p.RetentionPeriod, "retention_period": p.RetentionPeriod,
"security_measures": p.SecurityMeasures, "security_measures": p.SecurityMeasures,
"data_protection_impact_assessment": p.DataProtectionImpactAssessment, "data_protection_impact_assessment": p.DataProtectionImpactAssessment,

View File

@@ -26,6 +26,13 @@ const (
ProcessingActivityDataProtectionImpactAssessmentNotNeeded ProcessingActivityDataProtectionImpactAssessment = "NOT_NEEDED" ProcessingActivityDataProtectionImpactAssessmentNotNeeded ProcessingActivityDataProtectionImpactAssessment = "NOT_NEEDED"
) )
func ProcessingActivityDataProtectionImpactAssessments() []ProcessingActivityDataProtectionImpactAssessment {
return []ProcessingActivityDataProtectionImpactAssessment{
ProcessingActivityDataProtectionImpactAssessmentNeeded,
ProcessingActivityDataProtectionImpactAssessmentNotNeeded,
}
}
func (p ProcessingActivityDataProtectionImpactAssessment) String() string { func (p ProcessingActivityDataProtectionImpactAssessment) String() string {
return string(p) return string(p)
} }

View File

@@ -30,6 +30,17 @@ const (
ProcessingActivityLawfulBasisPublicTask ProcessingActivityLawfulBasis = "PUBLIC_TASK" ProcessingActivityLawfulBasisPublicTask ProcessingActivityLawfulBasis = "PUBLIC_TASK"
) )
func ProcessingActivityLawfulBases() []ProcessingActivityLawfulBasis {
return []ProcessingActivityLawfulBasis{
ProcessingActivityLawfulBasisLegitimateInterest,
ProcessingActivityLawfulBasisConsent,
ProcessingActivityLawfulBasisContractualNecessity,
ProcessingActivityLawfulBasisLegalObligation,
ProcessingActivityLawfulBasisVitalInterests,
ProcessingActivityLawfulBasisPublicTask,
}
}
func (p ProcessingActivityLawfulBasis) String() string { func (p ProcessingActivityLawfulBasis) String() string {
return string(p) return string(p)
} }

View File

@@ -19,19 +19,27 @@ import (
"fmt" "fmt"
) )
type ProcessingActivitySpecialOrCriminalData string type ProcessingActivitySpecialOrCriminalDatum string
const ( const (
ProcessingActivitySpecialOrCriminalDataYes ProcessingActivitySpecialOrCriminalData = "YES" ProcessingActivitySpecialOrCriminalDatumYes ProcessingActivitySpecialOrCriminalDatum = "YES"
ProcessingActivitySpecialOrCriminalDataNo ProcessingActivitySpecialOrCriminalData = "NO" ProcessingActivitySpecialOrCriminalDatumNo ProcessingActivitySpecialOrCriminalDatum = "NO"
ProcessingActivitySpecialOrCriminalDataPossible ProcessingActivitySpecialOrCriminalData = "POSSIBLE" ProcessingActivitySpecialOrCriminalDatumPossible ProcessingActivitySpecialOrCriminalDatum = "POSSIBLE"
) )
func (p ProcessingActivitySpecialOrCriminalData) String() string { func ProcessingActivitySpecialOrCriminalData() []ProcessingActivitySpecialOrCriminalDatum {
return []ProcessingActivitySpecialOrCriminalDatum{
ProcessingActivitySpecialOrCriminalDatumYes,
ProcessingActivitySpecialOrCriminalDatumNo,
ProcessingActivitySpecialOrCriminalDatumPossible,
}
}
func (p ProcessingActivitySpecialOrCriminalDatum) String() string {
return string(p) return string(p)
} }
func (p *ProcessingActivitySpecialOrCriminalData) Scan(value any) error { func (p *ProcessingActivitySpecialOrCriminalDatum) Scan(value any) error {
var s string var s string
switch v := value.(type) { switch v := value.(type) {
case string: case string:
@@ -39,22 +47,22 @@ func (p *ProcessingActivitySpecialOrCriminalData) Scan(value any) error {
case []byte: case []byte:
s = string(v) s = string(v)
default: default:
return fmt.Errorf("unsupported type for ProcessingActivitySpecialOrCriminalData: %T", value) return fmt.Errorf("unsupported type for ProcessingActivitySpecialOrCriminalDatum: %T", value)
} }
switch s { switch s {
case "YES": case "YES":
*p = ProcessingActivitySpecialOrCriminalDataYes *p = ProcessingActivitySpecialOrCriminalDatumYes
case "NO": case "NO":
*p = ProcessingActivitySpecialOrCriminalDataNo *p = ProcessingActivitySpecialOrCriminalDatumNo
case "POSSIBLE": case "POSSIBLE":
*p = ProcessingActivitySpecialOrCriminalDataPossible *p = ProcessingActivitySpecialOrCriminalDatumPossible
default: default:
return fmt.Errorf("invalid ProcessingActivitySpecialOrCriminalData value: %q", s) return fmt.Errorf("invalid ProcessingActivitySpecialOrCriminalDatum value: %q", s)
} }
return nil return nil
} }
func (p ProcessingActivitySpecialOrCriminalData) Value() (driver.Value, error) { func (p ProcessingActivitySpecialOrCriminalDatum) Value() (driver.Value, error) {
return p.String(), nil return p.String(), nil
} }

View File

@@ -26,6 +26,13 @@ const (
ProcessingActivityTransferImpactAssessmentNotNeeded ProcessingActivityTransferImpactAssessment = "NOT_NEEDED" ProcessingActivityTransferImpactAssessmentNotNeeded ProcessingActivityTransferImpactAssessment = "NOT_NEEDED"
) )
func ProcessingActivityTransferImpactAssessments() []ProcessingActivityTransferImpactAssessment {
return []ProcessingActivityTransferImpactAssessment{
ProcessingActivityTransferImpactAssessmentNeeded,
ProcessingActivityTransferImpactAssessmentNotNeeded,
}
}
func (p ProcessingActivityTransferImpactAssessment) String() string { func (p ProcessingActivityTransferImpactAssessment) String() string {
return string(p) return string(p)
} }

View File

@@ -19,22 +19,33 @@ import (
"fmt" "fmt"
) )
type ProcessingActivityTransferSafeguards string type ProcessingActivityTransferSafeguard string
const ( const (
ProcessingActivityTransferSafeguardsStandardContractualClauses ProcessingActivityTransferSafeguards = "STANDARD_CONTRACTUAL_CLAUSES" ProcessingActivityTransferSafeguardStandardContractualClauses ProcessingActivityTransferSafeguard = "STANDARD_CONTRACTUAL_CLAUSES"
ProcessingActivityTransferSafeguardsBindingCorporateRules ProcessingActivityTransferSafeguards = "BINDING_CORPORATE_RULES" ProcessingActivityTransferSafeguardBindingCorporateRules ProcessingActivityTransferSafeguard = "BINDING_CORPORATE_RULES"
ProcessingActivityTransferSafeguardsAdequacyDecision ProcessingActivityTransferSafeguards = "ADEQUACY_DECISION" ProcessingActivityTransferSafeguardAdequacyDecision ProcessingActivityTransferSafeguard = "ADEQUACY_DECISION"
ProcessingActivityTransferSafeguardsDerogations ProcessingActivityTransferSafeguards = "DEROGATIONS" ProcessingActivityTransferSafeguardDerogations ProcessingActivityTransferSafeguard = "DEROGATIONS"
ProcessingActivityTransferSafeguardsCodesOfConduct ProcessingActivityTransferSafeguards = "CODES_OF_CONDUCT" ProcessingActivityTransferSafeguardCodesOfConduct ProcessingActivityTransferSafeguard = "CODES_OF_CONDUCT"
ProcessingActivityTransferSafeguardsCertificationMechanisms ProcessingActivityTransferSafeguards = "CERTIFICATION_MECHANISMS" ProcessingActivityTransferSafeguardCertificationMechanisms ProcessingActivityTransferSafeguard = "CERTIFICATION_MECHANISMS"
) )
func (p ProcessingActivityTransferSafeguards) String() string { func ProcessingActivityTransferSafeguards() []ProcessingActivityTransferSafeguard {
return []ProcessingActivityTransferSafeguard{
ProcessingActivityTransferSafeguardStandardContractualClauses,
ProcessingActivityTransferSafeguardBindingCorporateRules,
ProcessingActivityTransferSafeguardAdequacyDecision,
ProcessingActivityTransferSafeguardDerogations,
ProcessingActivityTransferSafeguardCodesOfConduct,
ProcessingActivityTransferSafeguardCertificationMechanisms,
}
}
func (p ProcessingActivityTransferSafeguard) String() string {
return string(p) return string(p)
} }
func (p *ProcessingActivityTransferSafeguards) Scan(value any) error { func (p *ProcessingActivityTransferSafeguard) Scan(value any) error {
var s string var s string
switch v := value.(type) { switch v := value.(type) {
case string: case string:
@@ -42,28 +53,28 @@ func (p *ProcessingActivityTransferSafeguards) Scan(value any) error {
case []byte: case []byte:
s = string(v) s = string(v)
default: default:
return fmt.Errorf("unsupported type for ProcessingActivityTransferSafeguards: %T", value) return fmt.Errorf("unsupported type for ProcessingActivityTransferSafeguard: %T", value)
} }
switch s { switch s {
case "STANDARD_CONTRACTUAL_CLAUSES": case "STANDARD_CONTRACTUAL_CLAUSES":
*p = ProcessingActivityTransferSafeguardsStandardContractualClauses *p = ProcessingActivityTransferSafeguardStandardContractualClauses
case "BINDING_CORPORATE_RULES": case "BINDING_CORPORATE_RULES":
*p = ProcessingActivityTransferSafeguardsBindingCorporateRules *p = ProcessingActivityTransferSafeguardBindingCorporateRules
case "ADEQUACY_DECISION": case "ADEQUACY_DECISION":
*p = ProcessingActivityTransferSafeguardsAdequacyDecision *p = ProcessingActivityTransferSafeguardAdequacyDecision
case "DEROGATIONS": case "DEROGATIONS":
*p = ProcessingActivityTransferSafeguardsDerogations *p = ProcessingActivityTransferSafeguardDerogations
case "CODES_OF_CONDUCT": case "CODES_OF_CONDUCT":
*p = ProcessingActivityTransferSafeguardsCodesOfConduct *p = ProcessingActivityTransferSafeguardCodesOfConduct
case "CERTIFICATION_MECHANISMS": case "CERTIFICATION_MECHANISMS":
*p = ProcessingActivityTransferSafeguardsCertificationMechanisms *p = ProcessingActivityTransferSafeguardCertificationMechanisms
default: default:
return fmt.Errorf("invalid ProcessingActivityTransferSafeguards value: %q", s) return fmt.Errorf("invalid ProcessingActivityTransferSafeguard value: %q", s)
} }
return nil return nil
} }
func (p ProcessingActivityTransferSafeguards) Value() (driver.Value, error) { func (p ProcessingActivityTransferSafeguard) Value() (driver.Value, error) {
return p.String(), nil return p.String(), nil
} }

View File

@@ -30,6 +30,15 @@ const (
RiskTreatmentTransferred RiskTreatment = "TRANSFERRED" RiskTreatmentTransferred RiskTreatment = "TRANSFERRED"
) )
func RiskTreatments() []RiskTreatment {
return []RiskTreatment{
RiskTreatmentMitigated,
RiskTreatmentAccepted,
RiskTreatmentAvoided,
RiskTreatmentTransferred,
}
}
func (rt RiskTreatment) MarshalText() ([]byte, error) { func (rt RiskTreatment) MarshalText() ([]byte, error) {
return []byte(rt.String()), nil return []byte(rt.String()), nil
} }

View File

@@ -34,6 +34,16 @@ const (
SnapshotsTypeProcessingActivities SnapshotsType = "PROCESSING_ACTIVITIES" SnapshotsTypeProcessingActivities SnapshotsType = "PROCESSING_ACTIVITIES"
) )
func SnapshotsTypes() []SnapshotsType {
return []SnapshotsType{
SnapshotsTypeRisks,
SnapshotsTypeVendors,
SnapshotsTypeAssets,
SnapshotsTypeData,
SnapshotsTypeNonconformities,
}
}
func (st SnapshotsType) String() string { func (st SnapshotsType) String() string {
return string(st) return string(st)
} }

View File

@@ -28,6 +28,13 @@ const (
TaskStateDone TaskStateDone
) )
func TaskStates() []TaskState {
return []TaskState{
TaskStateTodo,
TaskStateDone,
}
}
func (ts TaskState) MarshalText() ([]byte, error) { func (ts TaskState) MarshalText() ([]byte, error) {
return []byte(ts.String()), nil return []byte(ts.String()), nil
} }

View File

@@ -27,6 +27,14 @@ const (
TrustCenterVisibilityPublic TrustCenterVisibility = "PUBLIC" TrustCenterVisibilityPublic TrustCenterVisibility = "PUBLIC"
) )
func TrustCenterVisibilities() []TrustCenterVisibility {
return []TrustCenterVisibility{
TrustCenterVisibilityNone,
TrustCenterVisibilityPrivate,
TrustCenterVisibilityPublic,
}
}
func (tcv TrustCenterVisibility) String() string { func (tcv TrustCenterVisibility) String() string {
return string(tcv) return string(tcv)
} }

View File

@@ -47,6 +47,33 @@ const (
VendorCategoryVersionControl VendorCategory = "VERSION_CONTROL" VendorCategoryVersionControl VendorCategory = "VERSION_CONTROL"
) )
func VendorCategories() []VendorCategory {
return []VendorCategory{
VendorCategoryAnalytics,
VendorCategoryCloudMonitoring,
VendorCategoryCloudProvider,
VendorCategoryCollaboration,
VendorCategoryCustomerSupport,
VendorCategoryDataStorageAndProcessing,
VendorCategoryDocumentManagement,
VendorCategoryEmployeeManagement,
VendorCategoryEngineering,
VendorCategoryFinance,
VendorCategoryIdentityProvider,
VendorCategoryIT,
VendorCategoryMarketing,
VendorCategoryOfficeOperations,
VendorCategoryOther,
VendorCategoryPasswordManagement,
VendorCategoryProductAndDesign,
VendorCategoryProfessionalServices,
VendorCategoryRecruiting,
VendorCategorySales,
VendorCategorySecurity,
VendorCategoryVersionControl,
}
}
func (i VendorCategory) String() string { func (i VendorCategory) String() string {
return string(i) return string(i)
} }

View File

@@ -5,10 +5,11 @@ import (
"fmt" "fmt"
"time" "time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/validator"
) )
type AssetService struct { type AssetService struct {
@@ -35,6 +36,38 @@ type UpdateAssetRequest struct {
VendorIDs []gid.GID VendorIDs []gid.GID
} }
func (car *CreateAssetRequest) Validate() error {
v := validator.New()
v.Check(car.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(car.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(car.Amount, "amount", validator.Required(), validator.Min(1))
v.Check(car.OwnerID, "owner_id", validator.Required(), validator.GID(coredata.PeopleEntityType))
v.Check(car.AssetType, "asset_type", validator.Required(), validator.OneOfSlice(coredata.AssetTypes()))
v.Check(car.DataTypesStored, "data_types_stored", validator.Required(), validator.SafeText(ContentMaxLength))
v.CheckEach(car.VendorIDs, "vendor_ids", func(index int, item any) {
v.Check(item, fmt.Sprintf("vendor_ids[%d]", index), validator.Required(), validator.GID(coredata.VendorEntityType))
})
return v.Error()
}
func (uar *UpdateAssetRequest) Validate() error {
v := validator.New()
v.Check(uar.ID, "id", validator.Required(), validator.GID(coredata.AssetEntityType))
v.Check(uar.Name, "name", validator.SafeText(NameMaxLength))
v.Check(uar.Amount, "amount", validator.Min(1))
v.Check(uar.OwnerID, "owner_id", validator.GID(coredata.PeopleEntityType))
v.Check(uar.AssetType, "asset_type", validator.OneOfSlice(coredata.AssetTypes()))
v.Check(uar.DataTypesStored, "data_types_stored", validator.SafeText(ContentMaxLength))
v.CheckEach(uar.VendorIDs, "vendor_ids", func(index int, item any) {
v.Check(item, fmt.Sprintf("vendor_ids[%d]", index), validator.GID(coredata.VendorEntityType))
})
return v.Error()
}
func (s AssetService) Get( func (s AssetService) Get(
ctx context.Context, ctx context.Context,
assetID gid.GID, assetID gid.GID,
@@ -135,6 +168,10 @@ func (s AssetService) Update(
ctx context.Context, ctx context.Context,
req UpdateAssetRequest, req UpdateAssetRequest,
) (*coredata.Asset, error) { ) (*coredata.Asset, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now() now := time.Now()
asset := &coredata.Asset{ID: req.ID} asset := &coredata.Asset{ID: req.ID}
assetVendors := &coredata.AssetVendors{} assetVendors := &coredata.AssetVendors{}
@@ -185,6 +222,10 @@ func (s AssetService) Create(
ctx context.Context, ctx context.Context,
req CreateAssetRequest, req CreateAssetRequest,
) (*coredata.Asset, error) { ) (*coredata.Asset, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now() now := time.Now()
assetID := gid.New(s.svc.scope.GetTenantID(), coredata.AssetEntityType) assetID := gid.New(s.svc.scope.GetTenantID(), coredata.AssetEntityType)
assetVendors := &coredata.AssetVendors{} assetVendors := &coredata.AssetVendors{}

View File

@@ -21,11 +21,12 @@ import (
"github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.gearno.de/crypto/uuid" "go.probo.inc/probo/pkg/validator"
"go.gearno.de/kit/pg"
) )
type AuditService struct { type AuditService struct {
@@ -52,21 +53,45 @@ type (
TrustCenterVisibility *coredata.TrustCenterVisibility TrustCenterVisibility *coredata.TrustCenterVisibility
} }
UpdateAuditStateRequest struct {
ID gid.GID
State coredata.AuditState
}
UploadAuditReportRequest struct { UploadAuditReportRequest struct {
AuditID gid.GID AuditID gid.GID
File File File File
} }
DeleteAuditReportRequest struct {
ID gid.GID
}
) )
func (car *CreateAuditRequest) Validate() error {
v := validator.New()
v.Check(car.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(car.FrameworkID, "framework_id", validator.Required(), validator.GID(coredata.FrameworkEntityType))
v.Check(car.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(car.ValidUntil, "valid_until", validator.After(car.ValidFrom))
v.Check(car.State, "state", validator.OneOfSlice(coredata.AuditStates()))
v.Check(car.TrustCenterVisibility, "trust_center_visibility", validator.OneOfSlice(coredata.TrustCenterVisibilities()))
return v.Error()
}
func (uar *UpdateAuditRequest) Validate() error {
v := validator.New()
v.Check(uar.ID, "id", validator.Required(), validator.GID(coredata.AuditEntityType))
v.Check(uar.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(uar.ValidUntil, "valid_until", validator.After(uar.ValidFrom))
v.Check(uar.State, "state", validator.OneOfSlice(coredata.AuditStates()))
v.Check(uar.TrustCenterVisibility, "trust_center_visibility", validator.OneOfSlice(coredata.TrustCenterVisibilities()))
return v.Error()
}
func (uarr *UploadAuditReportRequest) Validate() error {
v := validator.New()
v.Check(uarr.AuditID, "audit_id", validator.Required(), validator.GID(coredata.AuditEntityType))
return v.Error()
}
func (s AuditService) Get( func (s AuditService) Get(
ctx context.Context, ctx context.Context,
auditID gid.GID, auditID gid.GID,
@@ -111,8 +136,11 @@ func (s *AuditService) Create(
ctx context.Context, ctx context.Context,
req *CreateAuditRequest, req *CreateAuditRequest,
) (*coredata.Audit, error) { ) (*coredata.Audit, error) {
now := time.Now() if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now()
audit := &coredata.Audit{ audit := &coredata.Audit{
ID: gid.New(s.svc.scope.GetTenantID(), coredata.AuditEntityType), ID: gid.New(s.svc.scope.GetTenantID(), coredata.AuditEntityType),
Name: req.Name, Name: req.Name,
@@ -166,8 +194,11 @@ func (s *AuditService) Update(
ctx context.Context, ctx context.Context,
req *UpdateAuditRequest, req *UpdateAuditRequest,
) (*coredata.Audit, error) { ) (*coredata.Audit, error) {
audit := &coredata.Audit{} if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
audit := &coredata.Audit{}
err := s.svc.pg.WithTx( err := s.svc.pg.WithTx(
ctx, ctx,
func(conn pg.Conn) error { func(conn pg.Conn) error {

View File

@@ -22,11 +22,12 @@ import (
"text/template" "text/template"
"time" "time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/connector" "go.probo.inc/probo/pkg/connector"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/validator"
) )
var ( var (
@@ -55,6 +56,15 @@ type (
} }
) )
func (car *CreateConnectorRequest) Validate() error {
v := validator.New()
v.Check(car.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(car.Provider, "provider", validator.Required(), validator.OneOfSlice(coredata.ConnectorProviders()))
v.Check(car.Protocol, "protocol", validator.Required(), validator.OneOfSlice(coredata.ConnectorProtocols()))
v.Check(car.Connection, "connection", validator.Required())
return v.Error()
}
func (s *ConnectorService) ListForOrganizationID( func (s *ConnectorService) ListForOrganizationID(
ctx context.Context, ctx context.Context,
organizationID gid.GID, organizationID gid.GID,
@@ -88,20 +98,8 @@ func (s *ConnectorService) Create(
ctx context.Context, ctx context.Context,
req CreateConnectorRequest, req CreateConnectorRequest,
) (*coredata.Connector, error) { ) (*coredata.Connector, error) {
if req.OrganizationID == gid.Nil { if err := req.Validate(); err != nil {
return nil, fmt.Errorf("organization ID is required") return nil, fmt.Errorf("invalid request: %w", err)
}
if req.Provider == "" {
return nil, fmt.Errorf("connector provider is required")
}
if req.Protocol == "" {
return nil, fmt.Errorf("connector protocol is required")
}
if req.Connection == nil {
return nil, fmt.Errorf("connection configuration is required")
} }
id := gid.New(s.svc.scope.GetTenantID(), coredata.ConnectorEntityType) id := gid.New(s.svc.scope.GetTenantID(), coredata.ConnectorEntityType)

View File

@@ -19,10 +19,11 @@ import (
"fmt" "fmt"
"time" "time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/validator"
) )
type ContinualImprovementService struct { type ContinualImprovementService struct {
@@ -53,6 +54,34 @@ type (
} }
) )
func (ccir *CreateContinualImprovementRequest) Validate() error {
v := validator.New()
v.Check(ccir.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(ccir.ReferenceID, "reference_id", validator.SafeText(NameMaxLength))
v.Check(ccir.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(ccir.Source, "source", validator.SafeText(ContentMaxLength))
v.Check(ccir.OwnerID, "owner_id", validator.Required(), validator.GID(coredata.PeopleEntityType))
v.Check(ccir.Status, "status", validator.OneOfSlice(coredata.ContinualImprovementStatuses()))
v.Check(ccir.Priority, "priority", validator.OneOfSlice(coredata.ContinualImprovementPriorities()))
return v.Error()
}
func (ucir *UpdateContinualImprovementRequest) Validate() error {
v := validator.New()
v.Check(ucir.ID, "id", validator.Required(), validator.GID(coredata.ContinualImprovementEntityType))
v.Check(ucir.ReferenceID, "reference_id", validator.SafeText(NameMaxLength))
v.Check(ucir.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(ucir.Source, "source", validator.SafeText(ContentMaxLength))
v.Check(ucir.OwnerID, "owner_id", validator.GID(coredata.PeopleEntityType))
v.Check(ucir.Status, "status", validator.OneOfSlice(coredata.ContinualImprovementStatuses()))
v.Check(ucir.Priority, "priority", validator.OneOfSlice(coredata.ContinualImprovementPriorities()))
return v.Error()
}
func (s ContinualImprovementService) Get( func (s ContinualImprovementService) Get(
ctx context.Context, ctx context.Context,
continualImprovementID gid.GID, continualImprovementID gid.GID,
@@ -81,6 +110,10 @@ func (s *ContinualImprovementService) Create(
ctx context.Context, ctx context.Context,
req *CreateContinualImprovementRequest, req *CreateContinualImprovementRequest,
) (*coredata.ContinualImprovement, error) { ) (*coredata.ContinualImprovement, error) {
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now() now := time.Now()
improvement := &coredata.ContinualImprovement{ improvement := &coredata.ContinualImprovement{

View File

@@ -23,6 +23,7 @@ import (
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/validator"
) )
type ( type (
@@ -48,18 +49,35 @@ type (
Status *coredata.ControlStatus Status *coredata.ControlStatus
ExclusionJustification *string ExclusionJustification *string
} }
ConnectControlToMitigationRequest struct {
ControlID gid.GID
MitigationID gid.GID
}
DisconnectControlFromMitigationRequest struct {
ControlID gid.GID
MitigationID gid.GID
}
) )
func (ccr *CreateControlRequest) Validate() error {
v := validator.New()
v.Check(ccr.ID, "id", validator.Required(), validator.GID(coredata.ControlEntityType))
v.Check(ccr.FrameworkID, "framework_id", validator.Required(), validator.GID(coredata.FrameworkEntityType))
v.Check(ccr.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(ccr.Description, "description", validator.Required(), validator.SafeText(ContentMaxLength))
v.Check(ccr.SectionTitle, "section_title", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(ccr.Status, "status", validator.Required(), validator.OneOfSlice(coredata.ControlStatuses()))
v.Check(ccr.ExclusionJustification, "exclusion_justification", validator.Required(), validator.SafeText(TitleMaxLength))
return v.Error()
}
func (ucr *UpdateControlRequest) Validate() error {
v := validator.New()
v.Check(ucr.ID, "id", validator.Required(), validator.GID(coredata.ControlEntityType))
v.Check(ucr.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(ucr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(ucr.SectionTitle, "section_title", validator.SafeText(TitleMaxLength))
v.Check(ucr.Status, "status", validator.OneOfSlice(coredata.ControlStatuses()))
v.Check(ucr.ExclusionJustification, "exclusion_justification", validator.SafeText(TitleMaxLength))
return v.Error()
}
func (s ControlService) CountForDocumentID( func (s ControlService) CountForDocumentID(
ctx context.Context, ctx context.Context,
documentID gid.GID, documentID gid.GID,
@@ -706,6 +724,10 @@ func (s ControlService) Create(
ctx context.Context, ctx context.Context,
req CreateControlRequest, req CreateControlRequest,
) (*coredata.Control, error) { ) (*coredata.Control, error) {
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now() now := time.Now()
framework := &coredata.Framework{} framework := &coredata.Framework{}
@@ -765,6 +787,10 @@ func (s ControlService) Update(
ctx context.Context, ctx context.Context,
req UpdateControlRequest, req UpdateControlRequest,
) (*coredata.Control, error) { ) (*coredata.Control, error) {
if err := req.Validate(); err != nil {
return nil, err
}
control := &coredata.Control{ID: req.ID} control := &coredata.Control{ID: req.ID}
err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error { err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error {

View File

@@ -18,12 +18,13 @@ import (
"context" "context"
"fmt" "fmt"
"go.gearno.de/kit/log"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/certmanager" "go.probo.inc/probo/pkg/certmanager"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/crypto/cipher" "go.probo.inc/probo/pkg/crypto/cipher"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.gearno.de/kit/log" "go.probo.inc/probo/pkg/validator"
"go.gearno.de/kit/pg"
) )
type ( type (
@@ -40,6 +41,15 @@ type (
} }
) )
func (ccdr *CreateCustomDomainRequest) Validate() error {
v := validator.New()
v.Check(ccdr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(ccdr.Domain, "domain", validator.Required(), validator.NotEmpty(), validator.Domain())
return v.Error()
}
func NewCustomDomainService( func NewCustomDomainService(
svc *TenantService, svc *TenantService,
acmeService *certmanager.ACMEService, acmeService *certmanager.ACMEService,
@@ -58,6 +68,10 @@ func (s *CustomDomainService) CreateCustomDomain(
ctx context.Context, ctx context.Context,
req CreateCustomDomainRequest, req CreateCustomDomainRequest,
) (*coredata.CustomDomain, error) { ) (*coredata.CustomDomain, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
var domain *coredata.CustomDomain var domain *coredata.CustomDomain
err := s.svc.pg.WithTx( err := s.svc.pg.WithTx(

View File

@@ -19,30 +19,61 @@ import (
"fmt" "fmt"
"time" "time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/validator"
) )
type DatumService struct { type (
svc *TenantService DatumService struct {
svc *TenantService
}
CreateDatumRequest struct {
OrganizationID gid.GID
Name string
DataClassification coredata.DataClassification
OwnerID gid.GID
VendorIDs []gid.GID
}
UpdateDatumRequest struct {
ID gid.GID
Name *string
DataClassification *coredata.DataClassification
OwnerID *gid.GID
VendorIDs []gid.GID
}
)
func (cdr *CreateDatumRequest) Validate() error {
v := validator.New()
v.Check(cdr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(cdr.Name, "name", validator.Required(), validator.SafeText(NameMaxLength))
v.Check(cdr.DataClassification, "data_classification", validator.Required(), validator.OneOfSlice(coredata.DataClassifications()))
v.Check(cdr.OwnerID, "owner_id", validator.Required(), validator.GID(coredata.PeopleEntityType))
v.CheckEach(cdr.VendorIDs, "vendor_ids", func(index int, item any) {
v.Check(item, fmt.Sprintf("vendor_ids[%d]", index), validator.Required(), validator.GID(coredata.VendorEntityType))
})
return v.Error()
} }
type CreateDatumRequest struct { func (udr *UpdateDatumRequest) Validate() error {
OrganizationID gid.GID v := validator.New()
Name string
DataClassification coredata.DataClassification
OwnerID gid.GID
VendorIDs []gid.GID
}
type UpdateDatumRequest struct { v.Check(udr.ID, "id", validator.Required(), validator.GID(coredata.DatumEntityType))
ID gid.GID v.Check(udr.Name, "name", validator.SafeText(NameMaxLength))
Name *string v.Check(udr.DataClassification, "data_classification", validator.OneOfSlice(coredata.DataClassifications()))
DataClassification *coredata.DataClassification v.Check(udr.OwnerID, "owner_id", validator.GID(coredata.PeopleEntityType))
OwnerID *gid.GID v.CheckEach(udr.VendorIDs, "vendor_ids", func(index int, item any) {
VendorIDs []gid.GID v.Check(item, fmt.Sprintf("vendor_ids[%d]", index), validator.Required(), validator.GID(coredata.VendorEntityType))
})
return v.Error()
} }
func (s DatumService) Get( func (s DatumService) Get(
@@ -145,6 +176,10 @@ func (s DatumService) Update(
ctx context.Context, ctx context.Context,
req UpdateDatumRequest, req UpdateDatumRequest,
) (*coredata.Datum, error) { ) (*coredata.Datum, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now() now := time.Now()
datum := &coredata.Datum{} datum := &coredata.Datum{}
datumVendors := &coredata.DatumVendors{} datumVendors := &coredata.DatumVendors{}
@@ -189,6 +224,10 @@ func (s DatumService) Create(
ctx context.Context, ctx context.Context,
req CreateDatumRequest, req CreateDatumRequest,
) (*coredata.Datum, error) { ) (*coredata.Datum, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now() now := time.Now()
datumID := gid.New(s.svc.scope.GetTenantID(), coredata.DatumEntityType) datumID := gid.New(s.svc.scope.GetTenantID(), coredata.DatumEntityType)
datumVendors := &coredata.DatumVendors{} datumVendors := &coredata.DatumVendors{}

View File

@@ -26,6 +26,7 @@ import (
"go.probo.inc/probo/pkg/html2pdf" "go.probo.inc/probo/pkg/html2pdf"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/statelesstoken" "go.probo.inc/probo/pkg/statelesstoken"
"go.probo.inc/probo/pkg/validator"
"go.probo.inc/probo/pkg/watermarkpdf" "go.probo.inc/probo/pkg/watermarkpdf"
) )
@@ -86,6 +87,42 @@ type (
} }
) )
func (cdr *CreateDocumentRequest) Validate() error {
v := validator.New()
v.Check(cdr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(cdr.Title, "title", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(cdr.Content, "content", validator.Required(), validator.SafeText(ContentMaxLength))
v.Check(cdr.OwnerID, "owner_id", validator.Required(), validator.GID(coredata.PeopleEntityType))
v.Check(cdr.Classification, "classification", validator.Required(), validator.OneOfSlice(coredata.DocumentClassifications()))
v.Check(cdr.DocumentType, "document_type", validator.Required(), validator.OneOfSlice(coredata.DocumentTypes()))
v.Check(cdr.TrustCenterVisibility, "trust_center_visibility", validator.OneOfSlice(coredata.TrustCenterVisibilities()))
return v.Error()
}
func (udr *UpdateDocumentRequest) Validate() error {
v := validator.New()
v.Check(udr.DocumentID, "document_id", validator.Required(), validator.GID(coredata.DocumentEntityType))
v.Check(udr.Title, "title", validator.SafeText(TitleMaxLength))
v.Check(udr.OwnerID, "owner_id", validator.GID(coredata.PeopleEntityType))
v.Check(udr.Classification, "classification", validator.OneOfSlice(coredata.DocumentClassifications()))
v.Check(udr.DocumentType, "document_type", validator.OneOfSlice(coredata.DocumentTypes()))
v.Check(udr.TrustCenterVisibility, "trust_center_visibility", validator.OneOfSlice(coredata.TrustCenterVisibilities()))
return v.Error()
}
func (udvr *UpdateDocumentVersionRequest) Validate() error {
v := validator.New()
v.Check(udvr.ID, "id", validator.Required(), validator.GID(coredata.DocumentVersionEntityType))
v.Check(udvr.Content, "content", validator.Required(), validator.SafeText(ContentMaxLength))
return v.Error()
}
const ( const (
TokenTypeSigningRequest = "signing_request" TokenTypeSigningRequest = "signing_request"
@@ -308,6 +345,10 @@ func (s *DocumentService) Create(
ctx context.Context, ctx context.Context,
req CreateDocumentRequest, req CreateDocumentRequest,
) (*coredata.Document, *coredata.DocumentVersion, error) { ) (*coredata.Document, *coredata.DocumentVersion, error) {
if err := req.Validate(); err != nil {
return nil, nil, err
}
now := time.Now() now := time.Now()
documentID := gid.New(s.svc.scope.GetTenantID(), coredata.DocumentEntityType) documentID := gid.New(s.svc.scope.GetTenantID(), coredata.DocumentEntityType)
documentVersionID := gid.New(s.svc.scope.GetTenantID(), coredata.DocumentVersionEntityType) documentVersionID := gid.New(s.svc.scope.GetTenantID(), coredata.DocumentVersionEntityType)
@@ -1131,6 +1172,10 @@ func (s *DocumentService) Update(
ctx context.Context, ctx context.Context,
req UpdateDocumentRequest, req UpdateDocumentRequest,
) (*coredata.Document, error) { ) (*coredata.Document, error) {
if err := req.Validate(); err != nil {
return nil, err
}
document := &coredata.Document{} document := &coredata.Document{}
people := &coredata.People{} people := &coredata.People{}
now := time.Now() now := time.Now()

View File

@@ -19,12 +19,13 @@ import (
"fmt" "fmt"
"time" "time"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/filevalidation" "go.probo.inc/probo/pkg/filevalidation"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.gearno.de/crypto/uuid" "go.probo.inc/probo/pkg/validator"
"go.gearno.de/kit/pg"
) )
type ( type (
@@ -40,6 +41,16 @@ type (
} }
) )
func (umer *UploadMeasureEvidenceRequest) Validate() error {
v := validator.New()
v.Check(umer.MeasureID, "measure_id", validator.Required(), validator.GID(coredata.MeasureEntityType))
v.Check(umer.URL, "url", validator.URL())
v.Check(umer.File, "file", validator.Required())
return v.Error()
}
func (s EvidenceService) Get( func (s EvidenceService) Get(
ctx context.Context, ctx context.Context,
evidenceID gid.GID, evidenceID gid.GID,
@@ -68,6 +79,10 @@ func (s EvidenceService) UploadMeasureEvidence(
ctx context.Context, ctx context.Context,
req UploadMeasureEvidenceRequest, req UploadMeasureEvidenceRequest,
) (*coredata.Evidence, error) { ) (*coredata.Evidence, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now() now := time.Now()
evidenceID := gid.New(s.svc.scope.GetTenantID(), coredata.EvidenceEntityType) evidenceID := gid.New(s.svc.scope.GetTenantID(), coredata.EvidenceEntityType)

View File

@@ -23,11 +23,11 @@ import (
"github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/filevalidation" "go.probo.inc/probo/pkg/filevalidation"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
) )
type ( type (

View File

@@ -35,6 +35,7 @@ import (
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/slug" "go.probo.inc/probo/pkg/slug"
"go.probo.inc/probo/pkg/soagen" "go.probo.inc/probo/pkg/soagen"
"go.probo.inc/probo/pkg/validator"
) )
const ( const (
@@ -73,6 +74,26 @@ type (
} }
) )
func (cfr *CreateFrameworkRequest) Validate() error {
v := validator.New()
v.Check(cfr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(cfr.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(cfr.Description, "description", validator.SafeText(ContentMaxLength))
return v.Error()
}
func (ufr *UpdateFrameworkRequest) Validate() error {
v := validator.New()
v.Check(ufr.ID, "id", validator.Required(), validator.GID(coredata.FrameworkEntityType))
v.Check(ufr.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(ufr.Description, "description", validator.SafeText(ContentMaxLength))
return v.Error()
}
func (s FrameworkService) RequestExport( func (s FrameworkService) RequestExport(
ctx context.Context, ctx context.Context,
frameworkID gid.GID, frameworkID gid.GID,
@@ -310,6 +331,10 @@ func (s FrameworkService) Create(
ctx context.Context, ctx context.Context,
req CreateFrameworkRequest, req CreateFrameworkRequest,
) (*coredata.Framework, error) { ) (*coredata.Framework, error) {
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now() now := time.Now()
organization := &coredata.Organization{} organization := &coredata.Organization{}
@@ -420,6 +445,10 @@ func (s FrameworkService) Update(
ctx context.Context, ctx context.Context,
req UpdateFrameworkRequest, req UpdateFrameworkRequest,
) (*coredata.Framework, error) { ) (*coredata.Framework, error) {
if err := req.Validate(); err != nil {
return nil, err
}
framework := &coredata.Framework{ID: req.ID} framework := &coredata.Framework{ID: req.ID}
err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error { err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error {

View File

@@ -24,6 +24,7 @@ import (
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/validator"
) )
type ( type (
@@ -69,6 +70,29 @@ type (
} }
) )
func (cmr *CreateMeasureRequest) Validate() error {
v := validator.New()
v.Check(cmr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(cmr.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(cmr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(cmr.Category, "category", validator.Required(), validator.SafeText(TitleMaxLength))
return v.Error()
}
func (umr *UpdateMeasureRequest) Validate() error {
v := validator.New()
v.Check(umr.ID, "id", validator.Required(), validator.GID(coredata.MeasureEntityType))
v.Check(umr.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(umr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(umr.Category, "category", validator.SafeText(TitleMaxLength))
v.Check(umr.State, "state", validator.OneOfSlice(coredata.MeasureStates()))
return v.Error()
}
func (s MeasureService) CountForRiskID( func (s MeasureService) CountForRiskID(
ctx context.Context, ctx context.Context,
riskID gid.GID, riskID gid.GID,
@@ -399,6 +423,10 @@ func (s MeasureService) Update(
ctx context.Context, ctx context.Context,
req UpdateMeasureRequest, req UpdateMeasureRequest,
) (*coredata.Measure, error) { ) (*coredata.Measure, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
measure := &coredata.Measure{ID: req.ID} measure := &coredata.Measure{ID: req.ID}
err := s.svc.pg.WithTx( err := s.svc.pg.WithTx(
@@ -444,6 +472,10 @@ func (s MeasureService) Create(
ctx context.Context, ctx context.Context,
req CreateMeasureRequest, req CreateMeasureRequest,
) (*coredata.Measure, error) { ) (*coredata.Measure, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now() now := time.Now()
var measure *coredata.Measure var measure *coredata.Measure
organization := &coredata.Organization{} organization := &coredata.Organization{}

View File

@@ -19,10 +19,11 @@ import (
"fmt" "fmt"
"time" "time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/validator"
) )
type NonconformityService struct { type NonconformityService struct {
@@ -59,6 +60,36 @@ type (
} }
) )
func (cnr *CreateNonconformityRequest) Validate() error {
v := validator.New()
v.Check(cnr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(cnr.ReferenceID, "reference_id", validator.Required(), validator.SafeText(NameMaxLength))
v.Check(cnr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(cnr.AuditID, "audit_id", validator.Required(), validator.GID(coredata.AuditEntityType))
v.Check(cnr.RootCause, "root_cause", validator.Required(), validator.SafeText(ContentMaxLength))
v.Check(cnr.CorrectiveAction, "corrective_action", validator.SafeText(ContentMaxLength))
v.Check(cnr.OwnerID, "owner_id", validator.Required(), validator.GID(coredata.PeopleEntityType))
v.Check(cnr.Status, "status", validator.OneOfSlice(coredata.NonconformityStatuses()))
v.Check(cnr.EffectivenessCheck, "effectiveness_check", validator.SafeText(ContentMaxLength))
return v.Error()
}
func (unr *UpdateNonconformityRequest) Validate() error {
v := validator.New()
v.Check(unr.ID, "id", validator.Required(), validator.GID(coredata.NonconformityEntityType))
v.Check(unr.ReferenceID, "reference_id", validator.SafeText(NameMaxLength))
v.Check(unr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(unr.RootCause, "root_cause", validator.SafeText(ContentMaxLength))
v.Check(unr.CorrectiveAction, "corrective_action", validator.SafeText(ContentMaxLength))
v.Check(unr.OwnerID, "owner_id", validator.GID(coredata.PeopleEntityType))
v.Check(unr.Status, "status", validator.OneOfSlice(coredata.NonconformityStatuses()))
v.Check(unr.EffectivenessCheck, "effectiveness_check", validator.SafeText(ContentMaxLength))
return v.Error()
}
func (s NonconformityService) Get( func (s NonconformityService) Get(
ctx context.Context, ctx context.Context,
nonconformityID gid.GID, nonconformityID gid.GID,
@@ -83,6 +114,10 @@ func (s *NonconformityService) Create(
ctx context.Context, ctx context.Context,
req *CreateNonconformityRequest, req *CreateNonconformityRequest,
) (*coredata.Nonconformity, error) { ) (*coredata.Nonconformity, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now() now := time.Now()
nonconformity := &coredata.Nonconformity{ nonconformity := &coredata.Nonconformity{
@@ -143,6 +178,10 @@ func (s *NonconformityService) Update(
ctx context.Context, ctx context.Context,
req *UpdateNonconformityRequest, req *UpdateNonconformityRequest,
) (*coredata.Nonconformity, error) { ) (*coredata.Nonconformity, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
nonconformity := &coredata.Nonconformity{} nonconformity := &coredata.Nonconformity{}
err := s.svc.pg.WithTx( err := s.svc.pg.WithTx(

View File

@@ -19,10 +19,11 @@ import (
"fmt" "fmt"
"time" "time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/validator"
) )
type ObligationService struct { type ObligationService struct {
@@ -57,6 +58,36 @@ type (
} }
) )
func (cor *CreateObligationRequest) Validate() error {
v := validator.New()
v.Check(cor.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(cor.Area, "area", validator.SafeText(TitleMaxLength))
v.Check(cor.Source, "source", validator.SafeText(TitleMaxLength))
v.Check(cor.Requirement, "requirement", validator.SafeText(TitleMaxLength))
v.Check(cor.ActionsToBeImplemented, "actions_to_be_implemented", validator.SafeText(TitleMaxLength))
v.Check(cor.Regulator, "regulator", validator.SafeText(TitleMaxLength))
v.Check(cor.OwnerID, "owner_id", validator.Required(), validator.GID(coredata.PeopleEntityType))
v.Check(cor.Status, "status", validator.OneOfSlice(coredata.ObligationStatuses()))
return v.Error()
}
func (uor *UpdateObligationRequest) Validate() error {
v := validator.New()
v.Check(uor.ID, "id", validator.Required(), validator.GID(coredata.ObligationEntityType))
v.Check(uor.Area, "area", validator.SafeText(NameMaxLength))
v.Check(uor.Source, "source", validator.SafeText(NameMaxLength))
v.Check(uor.Requirement, "requirement", validator.SafeText(NameMaxLength))
v.Check(uor.ActionsToBeImplemented, "actions_to_be_implemented", validator.SafeText(NameMaxLength))
v.Check(uor.Regulator, "regulator", validator.SafeText(NameMaxLength))
v.Check(uor.OwnerID, "owner_id", validator.GID(coredata.PeopleEntityType))
v.Check(uor.Status, "status", validator.OneOfSlice(coredata.ObligationStatuses()))
return v.Error()
}
func (s ObligationService) Get( func (s ObligationService) Get(
ctx context.Context, ctx context.Context,
obligationID gid.GID, obligationID gid.GID,
@@ -85,6 +116,10 @@ func (s *ObligationService) Create(
ctx context.Context, ctx context.Context,
req *CreateObligationRequest, req *CreateObligationRequest,
) (*coredata.Obligation, error) { ) (*coredata.Obligation, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now() now := time.Now()
obligation := &coredata.Obligation{ obligation := &coredata.Obligation{
@@ -135,6 +170,10 @@ func (s *ObligationService) Update(
ctx context.Context, ctx context.Context,
req *UpdateObligationRequest, req *UpdateObligationRequest,
) (*coredata.Obligation, error) { ) (*coredata.Obligation, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
obligation := &coredata.Obligation{} obligation := &coredata.Obligation{}
err := s.svc.pg.WithTx( err := s.svc.pg.WithTx(

View File

@@ -22,12 +22,13 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/filevalidation" "go.probo.inc/probo/pkg/filevalidation"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/slug" "go.probo.inc/probo/pkg/slug"
"go.gearno.de/crypto/uuid" "go.probo.inc/probo/pkg/validator"
"go.gearno.de/kit/pg"
) )
var ( var (
@@ -74,10 +75,37 @@ type (
} }
) )
func (cor *CreateOrganizationRequest) Validate() error {
v := validator.New()
v.Check(cor.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
return v.Error()
}
func (uor *UpdateOrganizationRequest) Validate() error {
v := validator.New()
v.Check(uor.ID, "id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(uor.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(uor.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(uor.WebsiteURL, "website_url", validator.SafeText(2048))
v.Check(uor.Email, "email", validator.SafeText(255))
v.Check(uor.HeadquarterAddress, "headquarter_address", validator.SafeText(2048))
v.Check(uor.File, "file", validator.NotEmpty())
v.Check(uor.HorizontalLogoFile, "horizontal_logo_file", validator.NotEmpty())
return v.Error()
}
func (s OrganizationService) Create( func (s OrganizationService) Create(
ctx context.Context, ctx context.Context,
req CreateOrganizationRequest, req CreateOrganizationRequest,
) (*coredata.Organization, error) { ) (*coredata.Organization, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now() now := time.Now()
organizationID := gid.New(s.svc.scope.GetTenantID(), coredata.OrganizationEntityType) organizationID := gid.New(s.svc.scope.GetTenantID(), coredata.OrganizationEntityType)
@@ -154,6 +182,10 @@ func (s OrganizationService) Update(
ctx context.Context, ctx context.Context,
req UpdateOrganizationRequest, req UpdateOrganizationRequest,
) (*coredata.Organization, error) { ) (*coredata.Organization, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
organization := &coredata.Organization{} organization := &coredata.Organization{}
err := s.svc.pg.WithTx( err := s.svc.pg.WithTx(

View File

@@ -19,10 +19,11 @@ import (
"fmt" "fmt"
"time" "time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/validator"
) )
type ( type (
@@ -30,17 +31,6 @@ type (
svc *TenantService svc *TenantService
} }
UpdatePeopleRequest struct {
ID gid.GID
Kind *coredata.PeopleKind
FullName *string
PrimaryEmailAddress *string
AdditionalEmailAddresses *[]string
Position **string
ContractStartDate **time.Time
ContractEndDate **time.Time
}
CreatePeopleRequest struct { CreatePeopleRequest struct {
OrganizationID gid.GID OrganizationID gid.GID
FullName string FullName string
@@ -51,8 +41,53 @@ type (
ContractStartDate *time.Time ContractStartDate *time.Time
ContractEndDate *time.Time ContractEndDate *time.Time
} }
UpdatePeopleRequest struct {
ID gid.GID
Kind *coredata.PeopleKind
FullName *string
PrimaryEmailAddress *string
AdditionalEmailAddresses *[]string
Position **string
ContractStartDate **time.Time
ContractEndDate **time.Time
}
) )
func (cpr *CreatePeopleRequest) Validate() error {
v := validator.New()
v.Check(cpr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(cpr.FullName, "full_name", validator.Required(), validator.SafeText(NameMaxLength))
v.Check(cpr.PrimaryEmailAddress, "primary_email_address", validator.Required(), validator.NotEmpty(), validator.Email())
v.CheckEach(cpr.AdditionalEmailAddresses, "additional_email_addresses", func(index int, item any) {
v.Check(item, fmt.Sprintf("additional_email_addresses[%d]", index), validator.Required(), validator.NotEmpty(), validator.Email())
})
v.Check(cpr.Kind, "kind", validator.Required(), validator.OneOfSlice(coredata.PeopleKinds()))
v.Check(cpr.Position, "position", validator.SafeText(TitleMaxLength))
v.Check(cpr.ContractStartDate, "contract_start_date", validator.Before(cpr.ContractEndDate))
v.Check(cpr.ContractEndDate, "contract_end_date", validator.After(cpr.ContractStartDate))
return v.Error()
}
func (upr *UpdatePeopleRequest) Validate() error {
v := validator.New()
v.Check(upr.ID, "id", validator.Required(), validator.GID(coredata.PeopleEntityType))
v.Check(upr.Kind, "kind", validator.OneOfSlice(coredata.PeopleKinds()))
v.Check(upr.FullName, "full_name", validator.Required(), validator.SafeText(NameMaxLength))
v.Check(upr.PrimaryEmailAddress, "primary_email_address", validator.NotEmpty(), validator.Email())
v.CheckEach(upr.AdditionalEmailAddresses, "additional_email_addresses", func(index int, item any) {
v.Check(item, fmt.Sprintf("additional_email_addresses[%d]", index), validator.Required(), validator.NotEmpty(), validator.Email())
})
v.Check(upr.Position, "position", validator.SafeText(TitleMaxLength))
v.Check(upr.ContractStartDate, "contract_start_date", validator.Before(upr.ContractEndDate))
v.Check(upr.ContractEndDate, "contract_end_date", validator.After(upr.ContractStartDate))
return v.Error()
}
func (s PeopleService) Get( func (s PeopleService) Get(
ctx context.Context, ctx context.Context,
peopleID gid.GID, peopleID gid.GID,
@@ -133,6 +168,10 @@ func (s PeopleService) Update(
ctx context.Context, ctx context.Context,
req UpdatePeopleRequest, req UpdatePeopleRequest,
) (*coredata.People, error) { ) (*coredata.People, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
people := &coredata.People{} people := &coredata.People{}
err := s.svc.pg.WithTx( err := s.svc.pg.WithTx(
@@ -191,10 +230,8 @@ func (s PeopleService) Create(
ctx context.Context, ctx context.Context,
req CreatePeopleRequest, req CreatePeopleRequest,
) (*coredata.People, error) { ) (*coredata.People, error) {
if req.ContractStartDate != nil && req.ContractEndDate != nil { if err := req.Validate(); err != nil {
if req.ContractEndDate.Before(*req.ContractStartDate) { return nil, fmt.Errorf("invalid request: %w", err)
return nil, fmt.Errorf("contract end date must be after or equal to start date")
}
} }
now := time.Now() now := time.Now()

View File

@@ -19,10 +19,11 @@ import (
"fmt" "fmt"
"time" "time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/validator"
) )
type ProcessingActivityService struct { type ProcessingActivityService struct {
@@ -36,13 +37,13 @@ type (
Purpose *string Purpose *string
DataSubjectCategory *string DataSubjectCategory *string
PersonalDataCategory *string PersonalDataCategory *string
SpecialOrCriminalData coredata.ProcessingActivitySpecialOrCriminalData SpecialOrCriminalData coredata.ProcessingActivitySpecialOrCriminalDatum
ConsentEvidenceLink *string ConsentEvidenceLink *string
LawfulBasis coredata.ProcessingActivityLawfulBasis LawfulBasis coredata.ProcessingActivityLawfulBasis
Recipients *string Recipients *string
Location *string Location *string
InternationalTransfers bool InternationalTransfers bool
TransferSafeguards *coredata.ProcessingActivityTransferSafeguards TransferSafeguard *coredata.ProcessingActivityTransferSafeguard
RetentionPeriod *string RetentionPeriod *string
SecurityMeasures *string SecurityMeasures *string
DataProtectionImpactAssessment coredata.ProcessingActivityDataProtectionImpactAssessment DataProtectionImpactAssessment coredata.ProcessingActivityDataProtectionImpactAssessment
@@ -56,13 +57,13 @@ type (
Purpose **string Purpose **string
DataSubjectCategory **string DataSubjectCategory **string
PersonalDataCategory **string PersonalDataCategory **string
SpecialOrCriminalData *coredata.ProcessingActivitySpecialOrCriminalData SpecialOrCriminalData *coredata.ProcessingActivitySpecialOrCriminalDatum
ConsentEvidenceLink **string ConsentEvidenceLink **string
LawfulBasis *coredata.ProcessingActivityLawfulBasis LawfulBasis *coredata.ProcessingActivityLawfulBasis
Recipients **string Recipients **string
Location **string Location **string
InternationalTransfers *bool InternationalTransfers *bool
TransferSafeguards **coredata.ProcessingActivityTransferSafeguards TransferSafeguard **coredata.ProcessingActivityTransferSafeguard
RetentionPeriod **string RetentionPeriod **string
SecurityMeasures **string SecurityMeasures **string
DataProtectionImpactAssessment *coredata.ProcessingActivityDataProtectionImpactAssessment DataProtectionImpactAssessment *coredata.ProcessingActivityDataProtectionImpactAssessment
@@ -71,6 +72,57 @@ type (
} }
) )
func (cpar *CreateProcessingActivityRequest) Validate() error {
v := validator.New()
v.Check(cpar.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(cpar.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(cpar.Purpose, "purpose", validator.SafeText(TitleMaxLength))
v.Check(cpar.DataSubjectCategory, "data_subject_category", validator.SafeText(TitleMaxLength))
v.Check(cpar.PersonalDataCategory, "personal_data_category", validator.SafeText(TitleMaxLength))
v.Check(cpar.SpecialOrCriminalData, "special_or_criminal_data", validator.Required(), validator.OneOfSlice(coredata.ProcessingActivitySpecialOrCriminalData()))
v.Check(cpar.ConsentEvidenceLink, "consent_evidence_link", validator.SafeText(2048))
v.Check(cpar.LawfulBasis, "lawful_basis", validator.Required(), validator.OneOfSlice(coredata.ProcessingActivityLawfulBases()))
v.Check(cpar.Recipients, "recipients", validator.SafeText(TitleMaxLength))
v.Check(cpar.Location, "location", validator.SafeText(TitleMaxLength))
v.Check(cpar.InternationalTransfers, "international_transfers", validator.Required())
v.Check(cpar.TransferSafeguard, "transfer_safeguard", validator.OneOfSlice(coredata.ProcessingActivityTransferSafeguards()))
v.Check(cpar.RetentionPeriod, "retention_period", validator.SafeText(TitleMaxLength))
v.Check(cpar.SecurityMeasures, "security_measures", validator.SafeText(TitleMaxLength))
v.Check(cpar.DataProtectionImpactAssessment, "data_protection_impact_assessment", validator.Required(), validator.OneOfSlice(coredata.ProcessingActivityDataProtectionImpactAssessments()))
v.Check(cpar.TransferImpactAssessment, "transfer_impact_assessment", validator.Required(), validator.OneOfSlice(coredata.ProcessingActivityTransferImpactAssessments()))
v.CheckEach(cpar.VendorIDs, "vendor_ids", func(index int, item any) {
v.Check(item, fmt.Sprintf("vendor_ids[%d]", index), validator.Required(), validator.GID(coredata.VendorEntityType))
})
return v.Error()
}
func (upar *UpdateProcessingActivityRequest) Validate() error {
v := validator.New()
v.Check(upar.ID, "id", validator.Required(), validator.GID(coredata.ProcessingActivityEntityType))
v.Check(upar.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(upar.Purpose, "purpose", validator.SafeText(TitleMaxLength))
v.Check(upar.DataSubjectCategory, "data_subject_category", validator.SafeText(TitleMaxLength))
v.Check(upar.PersonalDataCategory, "personal_data_category", validator.SafeText(TitleMaxLength))
v.Check(upar.SpecialOrCriminalData, "special_or_criminal_data", validator.OneOfSlice(coredata.ProcessingActivitySpecialOrCriminalData()))
v.Check(upar.ConsentEvidenceLink, "consent_evidence_link", validator.SafeText(2048))
v.Check(upar.LawfulBasis, "lawful_basis", validator.OneOfSlice(coredata.ProcessingActivityLawfulBases()))
v.Check(upar.Recipients, "recipients", validator.SafeText(TitleMaxLength))
v.Check(upar.Location, "location", validator.SafeText(TitleMaxLength))
v.Check(upar.TransferSafeguard, "transfer_safeguards", validator.OneOfSlice(coredata.ProcessingActivityTransferSafeguards()))
v.Check(upar.RetentionPeriod, "retention_period", validator.SafeText(TitleMaxLength))
v.Check(upar.SecurityMeasures, "security_measures", validator.SafeText(TitleMaxLength))
v.Check(upar.DataProtectionImpactAssessment, "data_protection_impact_assessment", validator.OneOfSlice(coredata.ProcessingActivityDataProtectionImpactAssessments()))
v.Check(upar.TransferImpactAssessment, "transfer_impact_assessment", validator.OneOfSlice(coredata.ProcessingActivityTransferImpactAssessments()))
v.CheckEach(upar.VendorIDs, "vendor_ids", func(index int, item any) {
v.Check(item, fmt.Sprintf("vendor_ids[%d]", index), validator.GID(coredata.VendorEntityType))
})
return v.Error()
}
func (s ProcessingActivityService) Get( func (s ProcessingActivityService) Get(
ctx context.Context, ctx context.Context,
processingActivityID gid.GID, processingActivityID gid.GID,
@@ -111,7 +163,7 @@ func (s *ProcessingActivityService) Create(
Recipients: req.Recipients, Recipients: req.Recipients,
Location: req.Location, Location: req.Location,
InternationalTransfers: req.InternationalTransfers, InternationalTransfers: req.InternationalTransfers,
TransferSafeguards: req.TransferSafeguards, TransferSafeguard: req.TransferSafeguard,
RetentionPeriod: req.RetentionPeriod, RetentionPeriod: req.RetentionPeriod,
SecurityMeasures: req.SecurityMeasures, SecurityMeasures: req.SecurityMeasures,
DataProtectionImpactAssessment: req.DataProtectionImpactAssessment, DataProtectionImpactAssessment: req.DataProtectionImpactAssessment,
@@ -193,8 +245,8 @@ func (s *ProcessingActivityService) Update(
if req.InternationalTransfers != nil { if req.InternationalTransfers != nil {
processingActivity.InternationalTransfers = *req.InternationalTransfers processingActivity.InternationalTransfers = *req.InternationalTransfers
} }
if req.TransferSafeguards != nil { if req.TransferSafeguard != nil {
processingActivity.TransferSafeguards = *req.TransferSafeguards processingActivity.TransferSafeguard = *req.TransferSafeguard
} }
if req.RetentionPeriod != nil { if req.RetentionPeriod != nil {
processingActivity.RetentionPeriod = *req.RetentionPeriod processingActivity.RetentionPeriod = *req.RetentionPeriod

View File

@@ -23,6 +23,7 @@ import (
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/validator"
) )
type ( type (
@@ -59,6 +60,42 @@ type (
} }
) )
func (crr *CreateRiskRequest) Validate() error {
v := validator.New()
v.Check(crr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(crr.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(crr.Description, "description", validator.Required(), validator.SafeText(ContentMaxLength))
v.Check(crr.Category, "category", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(crr.Treatment, "treatment", validator.Required(), validator.OneOfSlice(coredata.RiskTreatments()))
v.Check(crr.OwnerID, "owner_id", validator.GID(coredata.PeopleEntityType))
v.Check(crr.InherentLikelihood, "inherent_likelihood", validator.Required(), validator.Min(1), validator.Max(5))
v.Check(crr.InherentImpact, "inherent_impact", validator.Required(), validator.Min(1), validator.Max(5))
v.Check(crr.ResidualLikelihood, "residual_likelihood", validator.Min(1), validator.Max(5))
v.Check(crr.ResidualImpact, "residual_impact", validator.Min(1), validator.Max(5))
v.Check(crr.Note, "note", validator.SafeText(TitleMaxLength))
return v.Error()
}
func (urr *UpdateRiskRequest) Validate() error {
v := validator.New()
v.Check(urr.ID, "id", validator.Required(), validator.GID(coredata.RiskEntityType))
v.Check(urr.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(urr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(urr.Category, "category", validator.SafeText(TitleMaxLength))
v.Check(urr.Treatment, "treatment", validator.OneOfSlice(coredata.RiskTreatments()))
v.Check(urr.OwnerID, "owner_id", validator.GID(coredata.PeopleEntityType))
v.Check(urr.InherentLikelihood, "inherent_likelihood", validator.Min(1), validator.Max(5))
v.Check(urr.InherentImpact, "inherent_impact", validator.Min(1), validator.Max(5))
v.Check(urr.ResidualLikelihood, "residual_likelihood", validator.Min(1), validator.Max(5))
v.Check(urr.ResidualImpact, "residual_impact", validator.Min(1), validator.Max(5))
v.Check(urr.Note, "note", validator.SafeText(TitleMaxLength))
return v.Error()
}
func (s RiskService) CountForMeasureID( func (s RiskService) CountForMeasureID(
ctx context.Context, ctx context.Context,
measureID gid.GID, measureID gid.GID,

View File

@@ -20,6 +20,9 @@ import (
"time" "time"
"github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3"
"go.gearno.de/kit/log"
"go.gearno.de/kit/pg"
"go.gearno.de/x/ref"
"go.probo.inc/probo/pkg/agents" "go.probo.inc/probo/pkg/agents"
"go.probo.inc/probo/pkg/auth" "go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/authz" "go.probo.inc/probo/pkg/authz"
@@ -30,9 +33,12 @@ import (
"go.probo.inc/probo/pkg/filevalidation" "go.probo.inc/probo/pkg/filevalidation"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/html2pdf" "go.probo.inc/probo/pkg/html2pdf"
"go.gearno.de/kit/log" )
"go.gearno.de/kit/pg"
"go.gearno.de/x/ref" const (
NameMaxLength = 100
TitleMaxLength = 1000
ContentMaxLength = 5000
) )
type ExportService interface { type ExportService interface {

View File

@@ -19,10 +19,11 @@ import (
"fmt" "fmt"
"time" "time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/validator"
) )
type SnapshotService struct { type SnapshotService struct {
@@ -45,6 +46,28 @@ type (
} }
) )
func (csr *CreateSnapshotRequest) Validate() error {
v := validator.New()
v.Check(csr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(csr.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(csr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(csr.Type, "type", validator.Required(), validator.OneOfSlice(coredata.SnapshotsTypes()))
return v.Error()
}
func (usr *UpdateSnapshotRequest) Validate() error {
v := validator.New()
v.Check(usr.ID, "id", validator.Required(), validator.GID(coredata.SnapshotEntityType))
v.Check(usr.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(usr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(usr.Type, "type", validator.OneOfSlice(coredata.SnapshotsTypes()))
return v.Error()
}
func (s *SnapshotService) Get( func (s *SnapshotService) Get(
ctx context.Context, ctx context.Context,
snapshotID gid.GID, snapshotID gid.GID,

View File

@@ -24,6 +24,7 @@ import (
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/validator"
) )
type ( type (
@@ -51,10 +52,39 @@ type (
} }
) )
func (ctr *CreateTaskRequest) Validate() error {
v := validator.New()
v.Check(ctr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(ctr.MeasureID, "measure_id", validator.GID(coredata.MeasureEntityType))
v.Check(ctr.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(ctr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(ctr.TimeEstimate, "time_estimate", validator.RangeDuration(0, 1000*time.Hour))
v.Check(ctr.AssignedToID, "assigned_to_id", validator.GID(coredata.PeopleEntityType))
return v.Error()
}
func (utr *UpdateTaskRequest) Validate() error {
v := validator.New()
v.Check(utr.TaskID, "task_id", validator.Required(), validator.GID(coredata.TaskEntityType))
v.Check(utr.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(utr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(utr.TimeEstimate, "time_estimate", validator.RangeDuration(0, 1000*time.Hour))
v.Check(utr.State, "state", validator.OneOfSlice(coredata.TaskStates()))
return v.Error()
}
func (s TaskService) Create( func (s TaskService) Create(
ctx context.Context, ctx context.Context,
req CreateTaskRequest, req CreateTaskRequest,
) (*coredata.Task, error) { ) (*coredata.Task, error) {
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now() now := time.Now()
taskID := gid.New(s.svc.scope.GetTenantID(), coredata.TaskEntityType) taskID := gid.New(s.svc.scope.GetTenantID(), coredata.TaskEntityType)
@@ -180,6 +210,9 @@ func (s TaskService) Update(
ctx context.Context, ctx context.Context,
req UpdateTaskRequest, req UpdateTaskRequest,
) (*coredata.Task, error) { ) (*coredata.Task, error) {
if err := req.Validate(); err != nil {
return nil, err
}
task := &coredata.Task{} task := &coredata.Task{}

View File

@@ -17,7 +17,6 @@ package probo
import ( import (
"context" "context"
"fmt" "fmt"
"net/mail"
"net/url" "net/url"
"time" "time"
@@ -27,6 +26,7 @@ import (
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/statelesstoken" "go.probo.inc/probo/pkg/statelesstoken"
"go.probo.inc/probo/pkg/validator"
) )
type ( type (
@@ -49,16 +49,40 @@ type (
TrustCenterFileIDs []gid.GID TrustCenterFileIDs []gid.GID
} }
DeleteTrustCenterAccessRequest struct {
ID gid.GID
}
TrustCenterAccessData struct { TrustCenterAccessData struct {
TrustCenterID gid.GID `json:"trust_center_id"` TrustCenterID gid.GID `json:"trust_center_id"`
Email string `json:"email"` Email string `json:"email"`
} }
) )
func (ctcar *CreateTrustCenterAccessRequest) Validate() error {
v := validator.New()
v.Check(ctcar.TrustCenterID, "trust_center_id", validator.Required(), validator.GID(coredata.TrustCenterEntityType))
v.Check(ctcar.Email, "email", validator.Required(), validator.Email())
v.Check(ctcar.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
return v.Error()
}
func (utcar *UpdateTrustCenterAccessRequest) Validate() error {
v := validator.New()
v.Check(utcar.ID, "id", validator.Required(), validator.GID(coredata.TrustCenterAccessEntityType))
v.Check(utcar.Name, "name", validator.SafeText(TitleMaxLength))
v.CheckEach(utcar.DocumentIDs, "document_ids", func(index int, item any) {
v.Check(item, fmt.Sprintf("document_ids[%d]", index), validator.Required(), validator.GID(coredata.DocumentEntityType))
})
v.CheckEach(utcar.ReportIDs, "report_ids", func(index int, item any) {
v.Check(item, fmt.Sprintf("report_ids[%d]", index), validator.Required(), validator.GID(coredata.ReportEntityType))
})
v.CheckEach(utcar.TrustCenterFileIDs, "trust_center_file_ids", func(index int, item any) {
v.Check(item, fmt.Sprintf("trust_center_file_ids[%d]", index), validator.Required(), validator.GID(coredata.TrustCenterFileEntityType))
})
return v.Error()
}
func (s TrustCenterAccessService) ListForTrustCenterID( func (s TrustCenterAccessService) ListForTrustCenterID(
ctx context.Context, ctx context.Context,
trustCenterID gid.GID, trustCenterID gid.GID,
@@ -239,18 +263,12 @@ func (s TrustCenterAccessService) Create(
ctx context.Context, ctx context.Context,
req *CreateTrustCenterAccessRequest, req *CreateTrustCenterAccessRequest,
) (*coredata.TrustCenterAccess, error) { ) (*coredata.TrustCenterAccess, error) {
if _, err := mail.ParseAddress(req.Email); err != nil { if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid email address") return nil, err
}
if req.Name == "" {
return nil, fmt.Errorf("name is required")
} }
now := time.Now() now := time.Now()
var access *coredata.TrustCenterAccess var access *coredata.TrustCenterAccess
err := s.svc.pg.WithTx( err := s.svc.pg.WithTx(
ctx, ctx,
func(tx pg.Conn) error { func(tx pg.Conn) error {
@@ -285,14 +303,13 @@ func (s TrustCenterAccessService) Update(
ctx context.Context, ctx context.Context,
req *UpdateTrustCenterAccessRequest, req *UpdateTrustCenterAccessRequest,
) (*coredata.TrustCenterAccess, error) { ) (*coredata.TrustCenterAccess, error) {
now := time.Now()
var access *coredata.TrustCenterAccess if err := req.Validate(); err != nil {
return nil, err
if req.Name != nil && *req.Name == "" {
return nil, fmt.Errorf("name is required")
} }
now := time.Now()
var access *coredata.TrustCenterAccess
err := s.svc.pg.WithTx( err := s.svc.pg.WithTx(
ctx, ctx,
func(tx pg.Conn) error { func(tx pg.Conn) error {
@@ -344,14 +361,14 @@ func (s TrustCenterAccessService) Update(
func (s TrustCenterAccessService) Delete( func (s TrustCenterAccessService) Delete(
ctx context.Context, ctx context.Context,
req *DeleteTrustCenterAccessRequest, trustCenterAccessID gid.GID,
) error { ) error {
err := s.svc.pg.WithTx( err := s.svc.pg.WithTx(
ctx, ctx,
func(tx pg.Conn) error { func(tx pg.Conn) error {
access := &coredata.TrustCenterAccess{} access := &coredata.TrustCenterAccess{}
if err := access.LoadByID(ctx, tx, s.svc.scope, req.ID); err != nil { if err := access.LoadByID(ctx, tx, s.svc.scope, trustCenterAccessID); err != nil {
return fmt.Errorf("cannot load trust center access: %w", err) return fmt.Errorf("cannot load trust center access: %w", err)
} }

View File

@@ -25,12 +25,13 @@ import (
"github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/filevalidation" "go.probo.inc/probo/pkg/filevalidation"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.gearno.de/crypto/uuid" "go.probo.inc/probo/pkg/validator"
"go.gearno.de/kit/pg"
) )
type ( type (
@@ -53,12 +54,31 @@ type (
Category *string Category *string
TrustCenterVisibility *coredata.TrustCenterVisibility TrustCenterVisibility *coredata.TrustCenterVisibility
} }
DeleteTrustCenterFileRequest struct {
ID gid.GID
}
) )
func (ctcfr *CreateTrustCenterFileRequest) Validate() error {
v := validator.New()
v.Check(ctcfr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(ctcfr.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(ctcfr.Category, "category", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(ctcfr.File, "file", validator.Required())
v.Check(ctcfr.TrustCenterVisibility, "trust_center_visibility", validator.Required(), validator.OneOfSlice(coredata.TrustCenterVisibilities()))
return v.Error()
}
func (utcfr *UpdateTrustCenterFileRequest) Validate() error {
v := validator.New()
v.Check(utcfr.ID, "id", validator.Required(), validator.GID(coredata.TrustCenterFileEntityType))
v.Check(utcfr.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(utcfr.Category, "category", validator.SafeText(TitleMaxLength))
v.Check(utcfr.TrustCenterVisibility, "trust_center_visibility", validator.OneOfSlice(coredata.TrustCenterVisibilities()))
return v.Error()
}
func (s TrustCenterFileService) ListForOrganizationID( func (s TrustCenterFileService) ListForOrganizationID(
ctx context.Context, ctx context.Context,
organizationID gid.GID, organizationID gid.GID,
@@ -134,8 +154,8 @@ func (s TrustCenterFileService) Create(
ctx context.Context, ctx context.Context,
req *CreateTrustCenterFileRequest, req *CreateTrustCenterFileRequest,
) (*coredata.TrustCenterFile, error) { ) (*coredata.TrustCenterFile, error) {
if req.Name == "" { if err := req.Validate(); err != nil {
return nil, fmt.Errorf("name is required") return nil, err
} }
// Validate file // Validate file
@@ -197,14 +217,14 @@ func (s TrustCenterFileService) Update(
ctx context.Context, ctx context.Context,
req *UpdateTrustCenterFileRequest, req *UpdateTrustCenterFileRequest,
) (*coredata.TrustCenterFile, error) { ) (*coredata.TrustCenterFile, error) {
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now() now := time.Now()
var file *coredata.TrustCenterFile var file *coredata.TrustCenterFile
if req.Name != nil && *req.Name == "" {
return nil, fmt.Errorf("name is required")
}
err := s.svc.pg.WithTx( err := s.svc.pg.WithTx(
ctx, ctx,
func(tx pg.Conn) error { func(tx pg.Conn) error {
@@ -242,14 +262,14 @@ func (s TrustCenterFileService) Update(
func (s TrustCenterFileService) Delete( func (s TrustCenterFileService) Delete(
ctx context.Context, ctx context.Context,
req *DeleteTrustCenterFileRequest, trustCenterFileID gid.GID,
) error { ) error {
err := s.svc.pg.WithTx( err := s.svc.pg.WithTx(
ctx, ctx,
func(tx pg.Conn) error { func(tx pg.Conn) error {
file := &coredata.TrustCenterFile{} file := &coredata.TrustCenterFile{}
if err := file.LoadByID(ctx, tx, s.svc.scope, req.ID); err != nil { if err := file.LoadByID(ctx, tx, s.svc.scope, trustCenterFileID); err != nil {
return fmt.Errorf("cannot load trust center file: %w", err) return fmt.Errorf("cannot load trust center file: %w", err)
} }

View File

@@ -31,6 +31,7 @@ import (
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/validator"
) )
type ( type (
@@ -54,12 +55,30 @@ type (
LogoFile *File LogoFile *File
Rank *int Rank *int
} }
DeleteTrustCenterReferenceRequest struct {
ID gid.GID
}
) )
func (ctcrr *CreateTrustCenterReferenceRequest) Validate() error {
v := validator.New()
v.Check(ctcrr.TrustCenterID, "trust_center_id", validator.Required(), validator.GID(coredata.TrustCenterEntityType))
v.Check(ctcrr.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(ctcrr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(ctcrr.WebsiteURL, "website_url", validator.Required(), validator.SafeText(2048))
return v.Error()
}
func (utcrr *UpdateTrustCenterReferenceRequest) Validate() error {
v := validator.New()
v.Check(utcrr.ID, "id", validator.Required(), validator.GID(coredata.TrustCenterReferenceEntityType))
v.Check(utcrr.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(utcrr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(utcrr.WebsiteURL, "website_url", validator.SafeText(2048))
return v.Error()
}
func (s TrustCenterReferenceService) ListForTrustCenterID( func (s TrustCenterReferenceService) ListForTrustCenterID(
ctx context.Context, ctx context.Context,
trustCenterID gid.GID, trustCenterID gid.GID,
@@ -132,12 +151,8 @@ func (s TrustCenterReferenceService) Create(
ctx context.Context, ctx context.Context,
req *CreateTrustCenterReferenceRequest, req *CreateTrustCenterReferenceRequest,
) (*coredata.TrustCenterReference, error) { ) (*coredata.TrustCenterReference, error) {
if req.Name == "" { if err := req.Validate(); err != nil {
return nil, fmt.Errorf("name is required") return nil, err
}
if req.WebsiteURL == "" {
return nil, fmt.Errorf("website URL is required")
} }
now := time.Now() now := time.Now()
@@ -185,19 +200,14 @@ func (s TrustCenterReferenceService) Update(
ctx context.Context, ctx context.Context,
req *UpdateTrustCenterReferenceRequest, req *UpdateTrustCenterReferenceRequest,
) (*coredata.TrustCenterReference, error) { ) (*coredata.TrustCenterReference, error) {
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now() now := time.Now()
var reference *coredata.TrustCenterReference var reference *coredata.TrustCenterReference
var newFileID *gid.GID var newFileID *gid.GID
if req.Name != nil && *req.Name == "" {
return nil, fmt.Errorf("name is required")
}
if req.WebsiteURL != nil && *req.WebsiteURL == "" {
return nil, fmt.Errorf("website URL is required")
}
var logoKey string var logoKey string
err := s.svc.pg.WithTx(ctx, func(tx pg.Conn) error { err := s.svc.pg.WithTx(ctx, func(tx pg.Conn) error {
@@ -254,12 +264,12 @@ func (s TrustCenterReferenceService) Update(
func (s TrustCenterReferenceService) Delete( func (s TrustCenterReferenceService) Delete(
ctx context.Context, ctx context.Context,
req *DeleteTrustCenterReferenceRequest, trustCenterReferenceID gid.GID,
) error { ) error {
err := s.svc.pg.WithTx(ctx, func(tx pg.Conn) error { err := s.svc.pg.WithTx(ctx, func(tx pg.Conn) error {
reference := &coredata.TrustCenterReference{} reference := &coredata.TrustCenterReference{}
if err := reference.LoadByID(ctx, tx, s.svc.scope, req.ID); err != nil { if err := reference.LoadByID(ctx, tx, s.svc.scope, trustCenterReferenceID); err != nil {
return fmt.Errorf("cannot load trust center reference: %w", err) return fmt.Errorf("cannot load trust center reference: %w", err)
} }

View File

@@ -25,10 +25,11 @@ import (
"github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.gearno.de/crypto/uuid" "go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg" "go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/validator"
) )
type ( type (
@@ -48,12 +49,27 @@ type (
File io.Reader File io.Reader
FileName string FileName string
} }
DeleteTrustCenterNDARequest struct {
TrustCenterID gid.GID
}
) )
func (utcr *UpdateTrustCenterRequest) Validate() error {
v := validator.New()
v.Check(utcr.ID, "id", validator.Required(), validator.GID(coredata.TrustCenterEntityType))
v.Check(utcr.Slug, "slug", validator.SafeText(NameMaxLength))
v.Check(utcr.NonDisclosureAgreementFileID, "non_disclosure_agreement_file_id", validator.GID(coredata.FileEntityType))
return v.Error()
}
func (utcndar *UploadTrustCenterNDARequest) Validate() error {
v := validator.New()
v.Check(utcndar.TrustCenterID, "trust_center_id", validator.Required(), validator.GID(coredata.TrustCenterEntityType))
v.Check(utcndar.FileName, "file_name", validator.Required(), validator.SafeText(TitleMaxLength))
return v.Error()
}
func (s TrustCenterService) Get( func (s TrustCenterService) Get(
ctx context.Context, ctx context.Context,
trustCenterID gid.GID, trustCenterID gid.GID,
@@ -124,6 +140,10 @@ func (s TrustCenterService) Update(
ctx context.Context, ctx context.Context,
req *UpdateTrustCenterRequest, req *UpdateTrustCenterRequest,
) (*coredata.TrustCenter, *coredata.File, error) { ) (*coredata.TrustCenter, *coredata.File, error) {
if err := req.Validate(); err != nil {
return nil, nil, err
}
var trustCenter *coredata.TrustCenter var trustCenter *coredata.TrustCenter
var file *coredata.File var file *coredata.File
@@ -170,6 +190,10 @@ func (s TrustCenterService) UploadNDA(
ctx context.Context, ctx context.Context,
req *UploadTrustCenterNDARequest, req *UploadTrustCenterNDARequest,
) (*coredata.TrustCenter, *coredata.File, error) { ) (*coredata.TrustCenter, *coredata.File, error) {
if err := req.Validate(); err != nil {
return nil, nil, err
}
objectKey, err := uuid.NewV7() objectKey, err := uuid.NewV7()
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("cannot generate object key: %w", err) return nil, nil, fmt.Errorf("cannot generate object key: %w", err)
@@ -249,7 +273,7 @@ func (s TrustCenterService) UploadNDA(
func (s TrustCenterService) DeleteNDA( func (s TrustCenterService) DeleteNDA(
ctx context.Context, ctx context.Context,
req *DeleteTrustCenterNDARequest, trustCenterID gid.GID,
) (*coredata.TrustCenter, *coredata.File, error) { ) (*coredata.TrustCenter, *coredata.File, error) {
var trustCenter *coredata.TrustCenter var trustCenter *coredata.TrustCenter
@@ -257,7 +281,7 @@ func (s TrustCenterService) DeleteNDA(
ctx, ctx,
func(conn pg.Conn) error { func(conn pg.Conn) error {
trustCenter = &coredata.TrustCenter{} trustCenter = &coredata.TrustCenter{}
if err := trustCenter.LoadByID(ctx, conn, s.svc.scope, req.TrustCenterID); err != nil { if err := trustCenter.LoadByID(ctx, conn, s.svc.scope, trustCenterID); err != nil {
return fmt.Errorf("cannot load trust center: %w", err) return fmt.Errorf("cannot load trust center: %w", err)
} }
@@ -276,7 +300,7 @@ func (s TrustCenterService) DeleteNDA(
return nil, nil, err return nil, nil, err
} }
return trustCenter, nil, nil // File is nil after deletion return trustCenter, nil, nil
} }
func (s TrustCenterService) GenerateNDAFileURL( func (s TrustCenterService) GenerateNDAFileURL(

View File

@@ -25,10 +25,11 @@ import (
"github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.gearno.de/crypto/uuid" "go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg" "go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/validator"
) )
type ( type (
@@ -49,6 +50,23 @@ type (
} }
) )
func (vbaacr *VendorBusinessAssociateAgreementCreateRequest) Validate() error {
v := validator.New()
v.Check(vbaacr.FileName, "file_name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(vbaacr.ValidUntil, "valid_until", validator.After(vbaacr.ValidFrom))
return v.Error()
}
func (vbaaur *VendorBusinessAssociateAgreementUpdateRequest) Validate() error {
v := validator.New()
v.Check(vbaaur.ValidUntil, "valid_until", validator.After(vbaaur.ValidFrom))
return v.Error()
}
func (s VendorBusinessAssociateAgreementService) GetByVendorID( func (s VendorBusinessAssociateAgreementService) GetByVendorID(
ctx context.Context, ctx context.Context,
vendorID gid.GID, vendorID gid.GID,
@@ -85,6 +103,10 @@ func (s VendorBusinessAssociateAgreementService) Upload(
vendorID gid.GID, vendorID gid.GID,
req *VendorBusinessAssociateAgreementCreateRequest, req *VendorBusinessAssociateAgreementCreateRequest,
) (*coredata.VendorBusinessAssociateAgreement, *coredata.File, error) { ) (*coredata.VendorBusinessAssociateAgreement, *coredata.File, error) {
if err := req.Validate(); err != nil {
return nil, nil, err
}
objectKey, err := uuid.NewV7() objectKey, err := uuid.NewV7()
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("cannot generate object key: %w", err) return nil, nil, fmt.Errorf("cannot generate object key: %w", err)
@@ -255,6 +277,10 @@ func (s VendorBusinessAssociateAgreementService) Update(
vendorID gid.GID, vendorID gid.GID,
req *VendorBusinessAssociateAgreementUpdateRequest, req *VendorBusinessAssociateAgreementUpdateRequest,
) (*coredata.VendorBusinessAssociateAgreement, *coredata.File, error) { ) (*coredata.VendorBusinessAssociateAgreement, *coredata.File, error) {
if err := req.Validate(); err != nil {
return nil, nil, err
}
existingAgreement := &coredata.VendorBusinessAssociateAgreement{} existingAgreement := &coredata.VendorBusinessAssociateAgreement{}
file := &coredata.File{} file := &coredata.File{}

View File

@@ -19,11 +19,12 @@ import (
"fmt" "fmt"
"time" "time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/filevalidation" "go.probo.inc/probo/pkg/filevalidation"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/validator"
) )
type ( type (
@@ -40,6 +41,14 @@ type (
} }
) )
func (vcrcr *VendorComplianceReportCreateRequest) Validate() error {
v := validator.New()
v.Check(vcrcr.ReportName, "report_name", validator.Required(), validator.SafeText(TitleMaxLength))
return v.Error()
}
func (s VendorComplianceReportService) ListForVendorID( func (s VendorComplianceReportService) ListForVendorID(
ctx context.Context, ctx context.Context,
vendorID gid.GID, vendorID gid.GID,
@@ -66,6 +75,10 @@ func (s VendorComplianceReportService) Upload(
vendorID gid.GID, vendorID gid.GID,
req *VendorComplianceReportCreateRequest, req *VendorComplianceReportCreateRequest,
) (*coredata.VendorComplianceReport, error) { ) (*coredata.VendorComplianceReport, error) {
if err := req.Validate(); err != nil {
return nil, err
}
vendor, err := s.svc.Vendors.Get(ctx, vendorID) vendor, err := s.svc.Vendors.Get(ctx, vendorID)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot get vendor: %w", err) return nil, fmt.Errorf("cannot get vendor: %w", err)

View File

@@ -19,10 +19,11 @@ import (
"fmt" "fmt"
"time" "time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/validator"
) )
type ( type (
@@ -47,6 +48,30 @@ type (
} }
) )
func (cvcr *CreateVendorContactRequest) Validate() error {
v := validator.New()
v.Check(cvcr.VendorID, "vendor_id", validator.Required(), validator.GID(coredata.VendorEntityType))
v.Check(cvcr.FullName, "full_name", validator.SafeText(TitleMaxLength))
v.Check(cvcr.Email, "email", validator.Email())
v.Check(cvcr.Phone, "phone", validator.SafeText(NameMaxLength))
v.Check(cvcr.Role, "role", validator.SafeText(TitleMaxLength))
return v.Error()
}
func (uvcr *UpdateVendorContactRequest) Validate() error {
v := validator.New()
v.Check(uvcr.ID, "id", validator.Required(), validator.GID(coredata.VendorContactEntityType))
v.Check(uvcr.FullName, "full_name", validator.SafeText(TitleMaxLength))
v.Check(uvcr.Email, "email", validator.Email())
v.Check(uvcr.Phone, "phone", validator.SafeText(NameMaxLength))
v.Check(uvcr.Role, "role", validator.SafeText(TitleMaxLength))
return v.Error()
}
func (s VendorContactService) Get( func (s VendorContactService) Get(
ctx context.Context, ctx context.Context,
vendorContactID gid.GID, vendorContactID gid.GID,
@@ -102,6 +127,10 @@ func (s VendorContactService) Create(
ctx context.Context, ctx context.Context,
req CreateVendorContactRequest, req CreateVendorContactRequest,
) (*coredata.VendorContact, error) { ) (*coredata.VendorContact, error) {
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now() now := time.Now()
vendorContact := &coredata.VendorContact{ vendorContact := &coredata.VendorContact{
ID: gid.New(s.svc.scope.GetTenantID(), coredata.VendorContactEntityType), ID: gid.New(s.svc.scope.GetTenantID(), coredata.VendorContactEntityType),
@@ -136,6 +165,10 @@ func (s VendorContactService) Update(
ctx context.Context, ctx context.Context,
req UpdateVendorContactRequest, req UpdateVendorContactRequest,
) (*coredata.VendorContact, error) { ) (*coredata.VendorContact, error) {
if err := req.Validate(); err != nil {
return nil, err
}
vendorContact := &coredata.VendorContact{} vendorContact := &coredata.VendorContact{}
err := s.svc.pg.WithTx( err := s.svc.pg.WithTx(

View File

@@ -25,10 +25,11 @@ import (
"github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.gearno.de/crypto/uuid" "go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg" "go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/validator"
) )
type ( type (
@@ -49,6 +50,23 @@ type (
} }
) )
func (vdpacr *VendorDataPrivacyAgreementCreateRequest) Validate() error {
v := validator.New()
v.Check(vdpacr.FileName, "file_name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(vdpacr.ValidUntil, "valid_until", validator.After(vdpacr.ValidFrom))
return v.Error()
}
func (vdpaur *VendorDataPrivacyAgreementUpdateRequest) Validate() error {
v := validator.New()
v.Check(vdpaur.ValidUntil, "valid_until", validator.After(vdpaur.ValidFrom))
return v.Error()
}
func (s VendorDataPrivacyAgreementService) GetByVendorID( func (s VendorDataPrivacyAgreementService) GetByVendorID(
ctx context.Context, ctx context.Context,
vendorID gid.GID, vendorID gid.GID,
@@ -85,6 +103,10 @@ func (s VendorDataPrivacyAgreementService) Upload(
vendorID gid.GID, vendorID gid.GID,
req *VendorDataPrivacyAgreementCreateRequest, req *VendorDataPrivacyAgreementCreateRequest,
) (*coredata.VendorDataPrivacyAgreement, *coredata.File, error) { ) (*coredata.VendorDataPrivacyAgreement, *coredata.File, error) {
if err := req.Validate(); err != nil {
return nil, nil, err
}
objectKey, err := uuid.NewV7() objectKey, err := uuid.NewV7()
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("cannot generate object key: %w", err) return nil, nil, fmt.Errorf("cannot generate object key: %w", err)
@@ -253,6 +275,10 @@ func (s VendorDataPrivacyAgreementService) Update(
vendorID gid.GID, vendorID gid.GID,
req *VendorDataPrivacyAgreementUpdateRequest, req *VendorDataPrivacyAgreementUpdateRequest,
) (*coredata.VendorDataPrivacyAgreement, *coredata.File, error) { ) (*coredata.VendorDataPrivacyAgreement, *coredata.File, error) {
if err := req.Validate(); err != nil {
return nil, nil, err
}
existingAgreement := &coredata.VendorDataPrivacyAgreement{} existingAgreement := &coredata.VendorDataPrivacyAgreement{}
file := &coredata.File{} file := &coredata.File{}

View File

@@ -19,10 +19,11 @@ import (
"fmt" "fmt"
"time" "time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/validator"
) )
type ( type (
@@ -91,6 +92,67 @@ type (
} }
) )
func (cvr *CreateVendorRequest) Validate() error {
v := validator.New()
v.Check(cvr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(cvr.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(cvr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(cvr.HeadquarterAddress, "headquarter_address", validator.SafeText(ContentMaxLength))
v.Check(cvr.LegalName, "legal_name", validator.SafeText(TitleMaxLength))
v.Check(cvr.WebsiteURL, "website_url", validator.SafeText(2048))
v.Check(cvr.Category, "category", validator.OneOfSlice(coredata.VendorCategories()))
v.Check(cvr.PrivacyPolicyURL, "privacy_policy_url", validator.SafeText(2048))
v.Check(cvr.ServiceLevelAgreementURL, "service_level_agreement_url", validator.SafeText(2048))
v.Check(cvr.DataProcessingAgreementURL, "data_processing_agreement_url", validator.SafeText(2048))
v.Check(cvr.BusinessAssociateAgreementURL, "business_associate_agreement_url", validator.SafeText(2048))
v.Check(cvr.SubprocessorsListURL, "subprocessors_list_url", validator.SafeText(2048))
v.Check(cvr.SecurityPageURL, "security_page_url", validator.SafeText(2048))
v.Check(cvr.TrustPageURL, "trust_page_url", validator.SafeText(2048))
v.Check(cvr.TermsOfServiceURL, "terms_of_service_url", validator.SafeText(2048))
v.Check(cvr.StatusPageURL, "status_page_url", validator.SafeText(2048))
v.Check(cvr.BusinessOwnerID, "business_owner_id", validator.GID(coredata.PeopleEntityType))
v.Check(cvr.SecurityOwnerID, "security_owner_id", validator.GID(coredata.PeopleEntityType))
return v.Error()
}
func (uvr *UpdateVendorRequest) Validate() error {
v := validator.New()
v.Check(uvr.ID, "id", validator.Required(), validator.GID(coredata.VendorEntityType))
v.Check(uvr.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(uvr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(uvr.HeadquarterAddress, "headquarter_address", validator.SafeText(ContentMaxLength))
v.Check(uvr.LegalName, "legal_name", validator.SafeText(TitleMaxLength))
v.Check(uvr.WebsiteURL, "website_url", validator.SafeText(2048))
v.Check(uvr.Category, "category", validator.OneOfSlice(coredata.VendorCategories()))
v.Check(uvr.PrivacyPolicyURL, "privacy_policy_url", validator.SafeText(2048))
v.Check(uvr.ServiceLevelAgreementURL, "service_level_agreement_url", validator.SafeText(2048))
v.Check(uvr.DataProcessingAgreementURL, "data_processing_agreement_url", validator.SafeText(2048))
v.Check(uvr.BusinessAssociateAgreementURL, "business_associate_agreement_url", validator.SafeText(2048))
v.Check(uvr.SubprocessorsListURL, "subprocessors_list_url", validator.SafeText(2048))
v.Check(uvr.SecurityPageURL, "security_page_url", validator.SafeText(2048))
v.Check(uvr.TrustPageURL, "trust_page_url", validator.SafeText(2048))
v.Check(uvr.TermsOfServiceURL, "terms_of_service_url", validator.SafeText(2048))
v.Check(uvr.StatusPageURL, "status_page_url", validator.SafeText(2048))
v.Check(uvr.BusinessOwnerID, "business_owner_id", validator.GID(coredata.PeopleEntityType))
v.Check(uvr.SecurityOwnerID, "security_owner_id", validator.GID(coredata.PeopleEntityType))
return v.Error()
}
func (cvrar *CreateVendorRiskAssessmentRequest) Validate() error {
v := validator.New()
v.Check(cvrar.VendorID, "vendor_id", validator.Required(), validator.GID(coredata.VendorEntityType))
v.Check(cvrar.DataSensitivity, "data_sensitivity", validator.Required(), validator.OneOfSlice(coredata.DataSensitivities()))
v.Check(cvrar.BusinessImpact, "business_impact", validator.Required(), validator.OneOfSlice(coredata.BusinessImpacts()))
v.Check(cvrar.Notes, "notes", validator.SafeText(ContentMaxLength))
return v.Error()
}
func (s VendorService) CountForOrganizationID( func (s VendorService) CountForOrganizationID(
ctx context.Context, ctx context.Context,
organizationID gid.GID, organizationID gid.GID,
@@ -208,6 +270,10 @@ func (s VendorService) Update(
ctx context.Context, ctx context.Context,
req UpdateVendorRequest, req UpdateVendorRequest,
) (*coredata.Vendor, error) { ) (*coredata.Vendor, error) {
if err := req.Validate(); err != nil {
return nil, err
}
vendor := &coredata.Vendor{} vendor := &coredata.Vendor{}
err := s.svc.pg.WithTx( err := s.svc.pg.WithTx(
@@ -373,6 +439,10 @@ func (s VendorService) Create(
ctx context.Context, ctx context.Context,
req CreateVendorRequest, req CreateVendorRequest,
) (*coredata.Vendor, error) { ) (*coredata.Vendor, error) {
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now() now := time.Now()
vendor := &coredata.Vendor{ vendor := &coredata.Vendor{
ID: gid.New(s.svc.scope.GetTenantID(), coredata.VendorEntityType), ID: gid.New(s.svc.scope.GetTenantID(), coredata.VendorEntityType),
@@ -542,6 +612,10 @@ func (s VendorService) CreateRiskAssessment(
ctx context.Context, ctx context.Context,
req CreateVendorRiskAssessmentRequest, req CreateVendorRiskAssessmentRequest,
) (*coredata.VendorRiskAssessment, error) { ) (*coredata.VendorRiskAssessment, error) {
if err := req.Validate(); err != nil {
return nil, err
}
vendorRiskAssessmentID := gid.New(s.svc.scope.GetTenantID(), coredata.VendorRiskAssessmentEntityType) vendorRiskAssessmentID := gid.New(s.svc.scope.GetTenantID(), coredata.VendorRiskAssessmentEntityType)
now := time.Now() now := time.Now()

View File

@@ -19,10 +19,11 @@ import (
"fmt" "fmt"
"time" "time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/validator"
) )
type ( type (
@@ -43,6 +44,26 @@ type (
} }
) )
func (cvsr *CreateVendorServiceRequest) Validate() error {
v := validator.New()
v.Check(cvsr.VendorID, "vendor_id", validator.Required(), validator.GID(coredata.VendorEntityType))
v.Check(cvsr.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(cvsr.Description, "description", validator.SafeText(ContentMaxLength))
return v.Error()
}
func (uvsr *UpdateVendorServiceRequest) Validate() error {
v := validator.New()
v.Check(uvsr.ID, "id", validator.Required(), validator.GID(coredata.VendorServiceEntityType))
v.Check(uvsr.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(uvsr.Description, "description", validator.SafeText(ContentMaxLength))
return v.Error()
}
func (s VendorServiceService) Get( func (s VendorServiceService) Get(
ctx context.Context, ctx context.Context,
vendorServiceID gid.GID, vendorServiceID gid.GID,
@@ -98,6 +119,10 @@ func (s VendorServiceService) Create(
ctx context.Context, ctx context.Context,
req CreateVendorServiceRequest, req CreateVendorServiceRequest,
) (*coredata.VendorService, error) { ) (*coredata.VendorService, error) {
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now() now := time.Now()
vendorService := &coredata.VendorService{ vendorService := &coredata.VendorService{
ID: gid.New(s.svc.scope.GetTenantID(), coredata.VendorServiceEntityType), ID: gid.New(s.svc.scope.GetTenantID(), coredata.VendorServiceEntityType),
@@ -130,6 +155,10 @@ func (s VendorServiceService) Update(
ctx context.Context, ctx context.Context,
req UpdateVendorServiceRequest, req UpdateVendorServiceRequest,
) (*coredata.VendorService, error) { ) (*coredata.VendorService, error) {
if err := req.Validate(); err != nil {
return nil, err
}
vendorService := &coredata.VendorService{} vendorService := &coredata.VendorService{}
err := s.svc.pg.WithTx( err := s.svc.pg.WithTx(

View File

@@ -43,25 +43,16 @@ enum OrderDirection
enum MeasureState enum MeasureState
@goModel(model: "go.probo.inc/probo/pkg/coredata.MeasureState") { @goModel(model: "go.probo.inc/probo/pkg/coredata.MeasureState") {
NOT_STARTED NOT_STARTED
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.MeasureStateNotStarted")
value: "go.probo.inc/probo/pkg/coredata.MeasureStateNotStarted"
)
IN_PROGRESS IN_PROGRESS
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.MeasureStateInProgress")
value: "go.probo.inc/probo/pkg/coredata.MeasureStateInProgress"
)
NOT_APPLICABLE NOT_APPLICABLE
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.MeasureStateNotApplicable")
value: "go.probo.inc/probo/pkg/coredata.MeasureStateNotApplicable"
)
IMPLEMENTED IMPLEMENTED
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.MeasureStateImplemented")
value: "go.probo.inc/probo/pkg/coredata.MeasureStateImplemented"
)
} }
enum TaskState enum TaskState @goModel(model: "go.probo.inc/probo/pkg/coredata.TaskState") {
@goModel(model: "go.probo.inc/probo/pkg/coredata.TaskState") {
TODO @goEnum(value: "go.probo.inc/probo/pkg/coredata.TaskStateTodo") TODO @goEnum(value: "go.probo.inc/probo/pkg/coredata.TaskStateTodo")
DONE @goEnum(value: "go.probo.inc/probo/pkg/coredata.TaskStateDone") DONE @goEnum(value: "go.probo.inc/probo/pkg/coredata.TaskStateDone")
} }
@@ -69,43 +60,27 @@ enum TaskState
enum EvidenceState enum EvidenceState
@goModel(model: "go.probo.inc/probo/pkg/coredata.EvidenceState") { @goModel(model: "go.probo.inc/probo/pkg/coredata.EvidenceState") {
FULFILLED FULFILLED
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.EvidenceStateFulfilled")
value: "go.probo.inc/probo/pkg/coredata.EvidenceStateFulfilled"
)
REQUESTED REQUESTED
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.EvidenceStateRequested")
value: "go.probo.inc/probo/pkg/coredata.EvidenceStateRequested"
)
} }
enum PeopleKind enum PeopleKind @goModel(model: "go.probo.inc/probo/pkg/coredata.PeopleKind") {
@goModel(model: "go.probo.inc/probo/pkg/coredata.PeopleKind") { EMPLOYEE @goEnum(value: "go.probo.inc/probo/pkg/coredata.PeopleKindEmployee")
EMPLOYEE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.PeopleKindEmployee")
CONTRACTOR CONTRACTOR
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.PeopleKindContractor")
value: "go.probo.inc/probo/pkg/coredata.PeopleKindContractor"
)
SERVICE_ACCOUNT SERVICE_ACCOUNT
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.PeopleKindServiceAccount")
value: "go.probo.inc/probo/pkg/coredata.PeopleKindServiceAccount"
)
} }
enum InvitationStatus enum InvitationStatus
@goModel(model: "go.probo.inc/probo/pkg/coredata.InvitationStatus") { @goModel(model: "go.probo.inc/probo/pkg/coredata.InvitationStatus") {
PENDING PENDING
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationStatusPending")
value: "go.probo.inc/probo/pkg/coredata.InvitationStatusPending"
)
ACCEPTED ACCEPTED
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationStatusAccepted")
value: "go.probo.inc/probo/pkg/coredata.InvitationStatusAccepted"
)
EXPIRED EXPIRED
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationStatusExpired")
value: "go.probo.inc/probo/pkg/coredata.InvitationStatusExpired"
)
} }
enum Role @goModel(model: "go.probo.inc/probo/pkg/coredata.Role") { enum Role @goModel(model: "go.probo.inc/probo/pkg/coredata.Role") {
@@ -117,12 +92,9 @@ enum Role @goModel(model: "go.probo.inc/probo/pkg/coredata.Role") {
enum DocumentStatus enum DocumentStatus
@goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentStatus") { @goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentStatus") {
DRAFT DRAFT @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentStatusDraft")
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentStatusDraft")
PUBLISHED PUBLISHED
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentStatusPublished")
value: "go.probo.inc/probo/pkg/coredata.DocumentStatusPublished"
)
} }
enum EvidenceType enum EvidenceType
@@ -134,49 +106,28 @@ enum EvidenceType
enum RiskTreatment enum RiskTreatment
@goModel(model: "go.probo.inc/probo/pkg/coredata.RiskTreatment") { @goModel(model: "go.probo.inc/probo/pkg/coredata.RiskTreatment") {
MITIGATED MITIGATED
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.RiskTreatmentMitigated")
value: "go.probo.inc/probo/pkg/coredata.RiskTreatmentMitigated"
)
ACCEPTED ACCEPTED
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.RiskTreatmentAccepted")
value: "go.probo.inc/probo/pkg/coredata.RiskTreatmentAccepted" AVOIDED @goEnum(value: "go.probo.inc/probo/pkg/coredata.RiskTreatmentAvoided")
)
AVOIDED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.RiskTreatmentAvoided"
)
TRANSFERRED TRANSFERRED
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.RiskTreatmentTransferred")
value: "go.probo.inc/probo/pkg/coredata.RiskTreatmentTransferred"
)
} }
enum AuditState enum AuditState @goModel(model: "go.probo.inc/probo/pkg/coredata.AuditState") {
@goModel(model: "go.probo.inc/probo/pkg/coredata.AuditState") {
NOT_STARTED NOT_STARTED
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditStateNotStarted")
value: "go.probo.inc/probo/pkg/coredata.AuditStateNotStarted"
)
IN_PROGRESS IN_PROGRESS
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditStateInProgress")
value: "go.probo.inc/probo/pkg/coredata.AuditStateInProgress"
)
COMPLETED COMPLETED
@goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditStateCompleted") @goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditStateCompleted")
REJECTED REJECTED @goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditStateRejected")
@goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditStateRejected") OUTDATED @goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditStateOutdated")
OUTDATED
@goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditStateOutdated")
} }
enum SAMLEnforcementPolicy enum SAMLEnforcementPolicy
@goModel( @goModel(model: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicy") {
model: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicy" OFF @goEnum(value: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicyOff")
) {
OFF
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicyOff"
)
OPTIONAL OPTIONAL
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicyOptional" value: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicyOptional"
@@ -190,21 +141,14 @@ enum SAMLEnforcementPolicy
enum UserAuthMethod enum UserAuthMethod
@goModel(model: "go.probo.inc/probo/pkg/coredata.UserAuthMethod") { @goModel(model: "go.probo.inc/probo/pkg/coredata.UserAuthMethod") {
PASSWORD PASSWORD
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.UserAuthMethodPassword")
value: "go.probo.inc/probo/pkg/coredata.UserAuthMethodPassword" SAML @goEnum(value: "go.probo.inc/probo/pkg/coredata.UserAuthMethodSAML")
)
SAML
@goEnum(value: "go.probo.inc/probo/pkg/coredata.UserAuthMethodSAML")
} }
enum TrustCenterVisibility enum TrustCenterVisibility
@goModel( @goModel(model: "go.probo.inc/probo/pkg/coredata.TrustCenterVisibility") {
model: "go.probo.inc/probo/pkg/coredata.TrustCenterVisibility"
) {
NONE NONE
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.TrustCenterVisibilityNone")
value: "go.probo.inc/probo/pkg/coredata.TrustCenterVisibilityNone"
)
PRIVATE PRIVATE
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrustCenterVisibilityPrivate" value: "go.probo.inc/probo/pkg/coredata.TrustCenterVisibilityPrivate"
@@ -216,21 +160,14 @@ enum TrustCenterVisibility
} }
enum NonconformityStatus enum NonconformityStatus
@goModel( @goModel(model: "go.probo.inc/probo/pkg/coredata.NonconformityStatus") {
model: "go.probo.inc/probo/pkg/coredata.NonconformityStatus" OPEN @goEnum(value: "go.probo.inc/probo/pkg/coredata.NonconformityStatusOpen")
) {
OPEN
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.NonconformityStatusOpen"
)
IN_PROGRESS IN_PROGRESS
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.NonconformityStatusInProgress" value: "go.probo.inc/probo/pkg/coredata.NonconformityStatusInProgress"
) )
CLOSED CLOSED
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.NonconformityStatusClosed")
value: "go.probo.inc/probo/pkg/coredata.NonconformityStatusClosed"
)
} }
enum ObligationStatus enum ObligationStatus
@@ -244,9 +181,7 @@ enum ObligationStatus
value: "go.probo.inc/probo/pkg/coredata.ObligationStatusPartiallyCompliant" value: "go.probo.inc/probo/pkg/coredata.ObligationStatusPartiallyCompliant"
) )
COMPLIANT COMPLIANT
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.ObligationStatusCompliant")
value: "go.probo.inc/probo/pkg/coredata.ObligationStatusCompliant"
)
} }
enum ContinualImprovementStatus enum ContinualImprovementStatus
@@ -285,21 +220,21 @@ enum ContinualImprovementPriority
) )
} }
enum ProcessingActivitySpecialOrCriminalData enum ProcessingActivitySpecialOrCriminalDatum
@goModel( @goModel(
model: "go.probo.inc/probo/pkg/coredata.ProcessingActivitySpecialOrCriminalData" model: "go.probo.inc/probo/pkg/coredata.ProcessingActivitySpecialOrCriminalDatum"
) { ) {
YES YES
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivitySpecialOrCriminalDataYes" value: "go.probo.inc/probo/pkg/coredata.ProcessingActivitySpecialOrCriminalDatumYes"
) )
NO NO
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivitySpecialOrCriminalDataNo" value: "go.probo.inc/probo/pkg/coredata.ProcessingActivitySpecialOrCriminalDatumNo"
) )
POSSIBLE POSSIBLE
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivitySpecialOrCriminalDataPossible" value: "go.probo.inc/probo/pkg/coredata.ProcessingActivitySpecialOrCriminalDatumPossible"
) )
} }
@@ -333,33 +268,33 @@ enum ProcessingActivityLawfulBasis
) )
} }
enum ProcessingActivityTransferSafeguards enum ProcessingActivityTransferSafeguard
@goModel( @goModel(
model: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguards" model: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguard"
) { ) {
STANDARD_CONTRACTUAL_CLAUSES STANDARD_CONTRACTUAL_CLAUSES
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardsStandardContractualClauses" value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardStandardContractualClauses"
) )
BINDING_CORPORATE_RULES BINDING_CORPORATE_RULES
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardsBindingCorporateRules" value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardBindingCorporateRules"
) )
ADEQUACY_DECISION ADEQUACY_DECISION
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardsAdequacyDecision" value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardAdequacyDecision"
) )
DEROGATIONS DEROGATIONS
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardsDerogations" value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardDerogations"
) )
CODES_OF_CONDUCT CODES_OF_CONDUCT
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardsCodesOfConduct" value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardCodesOfConduct"
) )
CERTIFICATION_MECHANISMS CERTIFICATION_MECHANISMS
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardsCertificationMechanisms" value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardCertificationMechanisms"
) )
} }
@@ -395,47 +330,29 @@ enum ProcessingActivityTransferImpactAssessment
enum UserOrderField enum UserOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.UserOrderField") { @goModel(model: "go.probo.inc/probo/pkg/coredata.UserOrderField") {
CREATED_AT CREATED_AT
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.UserOrderFieldCreatedAt")
value: "go.probo.inc/probo/pkg/coredata.UserOrderFieldCreatedAt"
)
} }
enum PeopleOrderField enum PeopleOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.PeopleOrderField") { @goModel(model: "go.probo.inc/probo/pkg/coredata.PeopleOrderField") {
FULL_NAME FULL_NAME
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.PeopleOrderFieldFullName")
value: "go.probo.inc/probo/pkg/coredata.PeopleOrderFieldFullName"
)
CREATED_AT CREATED_AT
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.PeopleOrderFieldCreatedAt")
value: "go.probo.inc/probo/pkg/coredata.PeopleOrderFieldCreatedAt" KIND @goEnum(value: "go.probo.inc/probo/pkg/coredata.PeopleOrderFieldKind")
)
KIND
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.PeopleOrderFieldKind"
)
} }
enum VendorOrderField enum VendorOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.VendorOrderField") { @goModel(model: "go.probo.inc/probo/pkg/coredata.VendorOrderField") {
NAME NAME @goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorOrderFieldName")
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorOrderFieldName"
)
CREATED_AT CREATED_AT
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorOrderFieldCreatedAt")
value: "go.probo.inc/probo/pkg/coredata.VendorOrderFieldCreatedAt"
)
UPDATED_AT UPDATED_AT
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorOrderFieldUpdatedAt")
value: "go.probo.inc/probo/pkg/coredata.VendorOrderFieldUpdatedAt"
)
} }
enum FrameworkOrderField enum FrameworkOrderField
@goModel( @goModel(model: "go.probo.inc/probo/pkg/coredata.FrameworkOrderField") {
model: "go.probo.inc/probo/pkg/coredata.FrameworkOrderField"
) {
CREATED_AT CREATED_AT
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.FrameworkOrderFieldCreatedAt" value: "go.probo.inc/probo/pkg/coredata.FrameworkOrderFieldCreatedAt"
@@ -445,9 +362,7 @@ enum FrameworkOrderField
enum ControlOrderField enum ControlOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.ControlOrderField") { @goModel(model: "go.probo.inc/probo/pkg/coredata.ControlOrderField") {
CREATED_AT CREATED_AT
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.ControlOrderFieldCreatedAt")
value: "go.probo.inc/probo/pkg/coredata.ControlOrderFieldCreatedAt"
)
SECTION_TITLE SECTION_TITLE
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.ControlOrderFieldSectionTitle" value: "go.probo.inc/probo/pkg/coredata.ControlOrderFieldSectionTitle"
@@ -457,13 +372,8 @@ enum ControlOrderField
enum MeasureOrderField enum MeasureOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.MeasureOrderField") { @goModel(model: "go.probo.inc/probo/pkg/coredata.MeasureOrderField") {
CREATED_AT CREATED_AT
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.MeasureOrderFieldCreatedAt")
value: "go.probo.inc/probo/pkg/coredata.MeasureOrderFieldCreatedAt" NAME @goEnum(value: "go.probo.inc/probo/pkg/coredata.MeasureOrderFieldName")
)
NAME
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MeasureOrderFieldName"
)
} }
enum TaskOrderField enum TaskOrderField
@@ -474,9 +384,7 @@ enum TaskOrderField
enum DocumentOrderField enum DocumentOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentOrderField") { @goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentOrderField") {
TITLE TITLE
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentOrderFieldTitle")
value: "go.probo.inc/probo/pkg/coredata.DocumentOrderFieldTitle"
)
CREATED_AT CREATED_AT
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.DocumentOrderFieldCreatedAt" value: "go.probo.inc/probo/pkg/coredata.DocumentOrderFieldCreatedAt"
@@ -490,23 +398,14 @@ enum DocumentOrderField
enum RiskOrderField enum RiskOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.RiskOrderField") { @goModel(model: "go.probo.inc/probo/pkg/coredata.RiskOrderField") {
CREATED_AT CREATED_AT
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldCreatedAt")
value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldCreatedAt"
)
UPDATED_AT UPDATED_AT
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldUpdatedAt")
value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldUpdatedAt" NAME @goEnum(value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldName")
)
NAME
@goEnum(value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldName")
CATEGORY CATEGORY
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldCategory")
value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldCategory"
)
TREATMENT TREATMENT
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldTreatment")
value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldTreatment"
)
INHERENT_RISK_SCORE INHERENT_RISK_SCORE
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldInherentRiskScore" value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldInherentRiskScore"
@@ -541,9 +440,7 @@ enum VendorComplianceReportOrderField
} }
enum VendorContactOrderField enum VendorContactOrderField
@goModel( @goModel(model: "go.probo.inc/probo/pkg/coredata.VendorContactOrderField") {
model: "go.probo.inc/probo/pkg/coredata.VendorContactOrderField"
) {
CREATED_AT CREATED_AT
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorContactOrderFieldCreatedAt" value: "go.probo.inc/probo/pkg/coredata.VendorContactOrderFieldCreatedAt"
@@ -559,9 +456,7 @@ enum VendorContactOrderField
} }
enum VendorServiceOrderField enum VendorServiceOrderField
@goModel( @goModel(model: "go.probo.inc/probo/pkg/coredata.VendorServiceOrderField") {
model: "go.probo.inc/probo/pkg/coredata.VendorServiceOrderField"
) {
CREATED_AT CREATED_AT
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorServiceOrderFieldCreatedAt" value: "go.probo.inc/probo/pkg/coredata.VendorServiceOrderFieldCreatedAt"
@@ -573,13 +468,9 @@ enum VendorServiceOrderField
} }
enum OrganizationOrderField enum OrganizationOrderField
@goModel( @goModel(model: "go.probo.inc/probo/pkg/coredata.OrganizationOrderField") {
model: "go.probo.inc/probo/pkg/coredata.OrganizationOrderField"
) {
NAME NAME
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.OrganizationOrderFieldName")
value: "go.probo.inc/probo/pkg/coredata.OrganizationOrderFieldName"
)
CREATED_AT CREATED_AT
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.OrganizationOrderFieldCreatedAt" value: "go.probo.inc/probo/pkg/coredata.OrganizationOrderFieldCreatedAt"
@@ -592,41 +483,25 @@ enum OrganizationOrderField
enum DataSensitivity enum DataSensitivity
@goModel(model: "go.probo.inc/probo/pkg/coredata.DataSensitivity") { @goModel(model: "go.probo.inc/probo/pkg/coredata.DataSensitivity") {
NONE NONE @goEnum(value: "go.probo.inc/probo/pkg/coredata.DataSensitivityNone")
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DataSensitivityNone") LOW @goEnum(value: "go.probo.inc/probo/pkg/coredata.DataSensitivityLow")
LOW MEDIUM @goEnum(value: "go.probo.inc/probo/pkg/coredata.DataSensitivityMedium")
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DataSensitivityLow") HIGH @goEnum(value: "go.probo.inc/probo/pkg/coredata.DataSensitivityHigh")
MEDIUM
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DataSensitivityMedium"
)
HIGH
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DataSensitivityHigh")
CRITICAL CRITICAL
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.DataSensitivityCritical")
value: "go.probo.inc/probo/pkg/coredata.DataSensitivityCritical"
)
} }
enum BusinessImpact enum BusinessImpact
@goModel(model: "go.probo.inc/probo/pkg/coredata.BusinessImpact") { @goModel(model: "go.probo.inc/probo/pkg/coredata.BusinessImpact") {
LOW @goEnum(value: "go.probo.inc/probo/pkg/coredata.BusinessImpactLow") LOW @goEnum(value: "go.probo.inc/probo/pkg/coredata.BusinessImpactLow")
MEDIUM MEDIUM @goEnum(value: "go.probo.inc/probo/pkg/coredata.BusinessImpactMedium")
@goEnum( HIGH @goEnum(value: "go.probo.inc/probo/pkg/coredata.BusinessImpactHigh")
value: "go.probo.inc/probo/pkg/coredata.BusinessImpactMedium"
)
HIGH
@goEnum(value: "go.probo.inc/probo/pkg/coredata.BusinessImpactHigh")
CRITICAL CRITICAL
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.BusinessImpactCritical")
value: "go.probo.inc/probo/pkg/coredata.BusinessImpactCritical"
)
} }
enum DocumentVersionOrderField enum DocumentVersionOrderField
@goModel( @goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentVersionOrderField") {
model: "go.probo.inc/probo/pkg/coredata.DocumentVersionOrderField"
) {
VERSION VERSION
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.DocumentVersionOrderFieldVersion" value: "go.probo.inc/probo/pkg/coredata.DocumentVersionOrderFieldVersion"
@@ -893,9 +768,7 @@ enum CountryCode
enum VendorCategory enum VendorCategory
@goModel(model: "go.probo.inc/probo/pkg/coredata.VendorCategory") { @goModel(model: "go.probo.inc/probo/pkg/coredata.VendorCategory") {
ANALYTICS ANALYTICS
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategoryAnalytics")
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryAnalytics"
)
CLOUD_MONITORING CLOUD_MONITORING
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryCloudMonitoring" value: "go.probo.inc/probo/pkg/coredata.VendorCategoryCloudMonitoring"
@@ -925,28 +798,21 @@ enum VendorCategory
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryEmployeeManagement" value: "go.probo.inc/probo/pkg/coredata.VendorCategoryEmployeeManagement"
) )
ENGINEERING ENGINEERING
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategoryEngineering")
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryEngineering"
)
FINANCE FINANCE
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategoryFinance")
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryFinance"
)
IDENTITY_PROVIDER IDENTITY_PROVIDER
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryIdentityProvider" value: "go.probo.inc/probo/pkg/coredata.VendorCategoryIdentityProvider"
) )
IT @goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategoryIT") IT @goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategoryIT")
MARKETING MARKETING
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategoryMarketing")
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryMarketing"
)
OFFICE_OPERATIONS OFFICE_OPERATIONS
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryOfficeOperations" value: "go.probo.inc/probo/pkg/coredata.VendorCategoryOfficeOperations"
) )
OTHER OTHER @goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategoryOther")
@goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategoryOther")
PASSWORD_MANAGEMENT PASSWORD_MANAGEMENT
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryPasswordManagement" value: "go.probo.inc/probo/pkg/coredata.VendorCategoryPasswordManagement"
@@ -960,15 +826,10 @@ enum VendorCategory
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryProfessionalServices" value: "go.probo.inc/probo/pkg/coredata.VendorCategoryProfessionalServices"
) )
RECRUITING RECRUITING
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategoryRecruiting")
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryRecruiting" SALES @goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategorySales")
)
SALES
@goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategorySales")
SECURITY SECURITY
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategorySecurity")
value: "go.probo.inc/probo/pkg/coredata.VendorCategorySecurity"
)
VERSION_CONTROL VERSION_CONTROL
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryVersionControl" value: "go.probo.inc/probo/pkg/coredata.VendorCategoryVersionControl"
@@ -977,21 +838,15 @@ enum VendorCategory
enum DocumentType enum DocumentType
@goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentType") { @goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentType") {
OTHER OTHER @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeOther")
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeOther")
ISMS @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeISMS") ISMS @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeISMS")
POLICY POLICY @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypePolicy")
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypePolicy")
PROCEDURE PROCEDURE
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeProcedure")
value: "go.probo.inc/probo/pkg/coredata.DocumentTypeProcedure"
)
} }
enum DocumentClassification enum DocumentClassification
@goModel( @goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentClassification") {
model: "go.probo.inc/probo/pkg/coredata.DocumentClassification"
) {
PUBLIC PUBLIC
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.DocumentClassificationPublic" value: "go.probo.inc/probo/pkg/coredata.DocumentClassificationPublic"
@@ -1010,34 +865,23 @@ enum DocumentClassification
) )
} }
enum AssetType enum AssetType @goModel(model: "go.probo.inc/probo/pkg/coredata.AssetType") {
@goModel(model: "go.probo.inc/probo/pkg/coredata.AssetType") { PHYSICAL @goEnum(value: "go.probo.inc/probo/pkg/coredata.AssetTypePhysical")
PHYSICAL VIRTUAL @goEnum(value: "go.probo.inc/probo/pkg/coredata.AssetTypeVirtual")
@goEnum(value: "go.probo.inc/probo/pkg/coredata.AssetTypePhysical")
VIRTUAL
@goEnum(value: "go.probo.inc/probo/pkg/coredata.AssetTypeVirtual")
} }
enum AssetOrderField enum AssetOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.AssetOrderField") { @goModel(model: "go.probo.inc/probo/pkg/coredata.AssetOrderField") {
CREATED_AT CREATED_AT
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.AssetOrderFieldCreatedAt")
value: "go.probo.inc/probo/pkg/coredata.AssetOrderFieldCreatedAt" AMOUNT @goEnum(value: "go.probo.inc/probo/pkg/coredata.AssetOrderFieldAmount")
)
AMOUNT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AssetOrderFieldAmount"
)
} }
enum DatumOrderField enum DatumOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.DatumOrderField") { @goModel(model: "go.probo.inc/probo/pkg/coredata.DatumOrderField") {
CREATED_AT CREATED_AT
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.DatumOrderFieldCreatedAt")
value: "go.probo.inc/probo/pkg/coredata.DatumOrderFieldCreatedAt" NAME @goEnum(value: "go.probo.inc/probo/pkg/coredata.DatumOrderFieldName")
)
NAME
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DatumOrderFieldName")
DATA_CLASSIFICATION DATA_CLASSIFICATION
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.DatumOrderFieldDataClassification" value: "go.probo.inc/probo/pkg/coredata.DatumOrderFieldDataClassification"
@@ -1047,59 +891,38 @@ enum DatumOrderField
enum DataClassification enum DataClassification
@goModel(model: "go.probo.inc/probo/pkg/coredata.DataClassification") { @goModel(model: "go.probo.inc/probo/pkg/coredata.DataClassification") {
PUBLIC PUBLIC
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.DataClassificationPublic")
value: "go.probo.inc/probo/pkg/coredata.DataClassificationPublic"
)
INTERNAL INTERNAL
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.DataClassificationInternal")
value: "go.probo.inc/probo/pkg/coredata.DataClassificationInternal"
)
CONFIDENTIAL CONFIDENTIAL
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.DataClassificationConfidential" value: "go.probo.inc/probo/pkg/coredata.DataClassificationConfidential"
) )
SECRET SECRET
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.DataClassificationSecret")
value: "go.probo.inc/probo/pkg/coredata.DataClassificationSecret"
)
} }
enum ControlStatus enum ControlStatus
@goModel(model: "go.probo.inc/probo/pkg/coredata.ControlStatus") { @goModel(model: "go.probo.inc/probo/pkg/coredata.ControlStatus") {
INCLUDED INCLUDED
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.ControlStatusIncluded")
value: "go.probo.inc/probo/pkg/coredata.ControlStatusIncluded"
)
EXCLUDED EXCLUDED
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.ControlStatusExcluded")
value: "go.probo.inc/probo/pkg/coredata.ControlStatusExcluded"
)
} }
enum AuditOrderField enum AuditOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.AuditOrderField") { @goModel(model: "go.probo.inc/probo/pkg/coredata.AuditOrderField") {
CREATED_AT CREATED_AT
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditOrderFieldCreatedAt")
value: "go.probo.inc/probo/pkg/coredata.AuditOrderFieldCreatedAt"
)
VALID_FROM VALID_FROM
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditOrderFieldValidFrom")
value: "go.probo.inc/probo/pkg/coredata.AuditOrderFieldValidFrom"
)
VALID_UNTIL VALID_UNTIL
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditOrderFieldValidUntil")
value: "go.probo.inc/probo/pkg/coredata.AuditOrderFieldValidUntil" STATE @goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditOrderFieldState")
)
STATE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AuditOrderFieldState"
)
} }
enum NonconformityOrderField enum NonconformityOrderField
@goModel( @goModel(model: "go.probo.inc/probo/pkg/coredata.NonconformityOrderField") {
model: "go.probo.inc/probo/pkg/coredata.NonconformityOrderField"
) {
CREATED_AT CREATED_AT
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.NonconformityOrderFieldCreatedAt" value: "go.probo.inc/probo/pkg/coredata.NonconformityOrderFieldCreatedAt"
@@ -1123,9 +946,7 @@ enum NonconformityOrderField
} }
enum ObligationOrderField enum ObligationOrderField
@goModel( @goModel(model: "go.probo.inc/probo/pkg/coredata.ObligationOrderField") {
model: "go.probo.inc/probo/pkg/coredata.ObligationOrderField"
) {
CREATED_AT CREATED_AT
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.ObligationOrderFieldCreatedAt" value: "go.probo.inc/probo/pkg/coredata.ObligationOrderFieldCreatedAt"
@@ -1139,9 +960,7 @@ enum ObligationOrderField
value: "go.probo.inc/probo/pkg/coredata.ObligationOrderFieldDueDate" value: "go.probo.inc/probo/pkg/coredata.ObligationOrderFieldDueDate"
) )
STATUS STATUS
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.ObligationOrderFieldStatus")
value: "go.probo.inc/probo/pkg/coredata.ObligationOrderFieldStatus"
)
} }
enum ContinualImprovementOrderField enum ContinualImprovementOrderField
@@ -1227,9 +1046,7 @@ enum TrustCenterReferenceOrderField
} }
enum TrustCenterFileOrderField enum TrustCenterFileOrderField
@goModel( @goModel(model: "go.probo.inc/probo/pkg/coredata.TrustCenterFileOrderField") {
model: "go.probo.inc/probo/pkg/coredata.TrustCenterFileOrderField"
) {
NAME NAME
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrustCenterFileOrderFieldName" value: "go.probo.inc/probo/pkg/coredata.TrustCenterFileOrderFieldName"
@@ -1246,24 +1063,16 @@ enum TrustCenterFileOrderField
enum SnapshotsType enum SnapshotsType
@goModel(model: "go.probo.inc/probo/pkg/coredata.SnapshotsType") { @goModel(model: "go.probo.inc/probo/pkg/coredata.SnapshotsType") {
RISKS RISKS @goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeRisks")
@goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeRisks") VENDORS @goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeVendors")
VENDORS ASSETS @goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeAssets")
@goEnum( DATA @goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeData")
value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeVendors"
)
ASSETS
@goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeAssets")
DATA
@goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeData")
NONCONFORMITIES NONCONFORMITIES
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeNonconformities" value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeNonconformities"
) )
OBLIGATIONS OBLIGATIONS
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeObligations")
value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeObligations"
)
CONTINUAL_IMPROVEMENTS CONTINUAL_IMPROVEMENTS
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeContinualImprovements" value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeContinualImprovements"
@@ -1280,20 +1089,12 @@ enum SnapshotOrderField
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.SnapshotOrderFieldCreatedAt" value: "go.probo.inc/probo/pkg/coredata.SnapshotOrderFieldCreatedAt"
) )
NAME NAME @goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotOrderFieldName")
@goEnum( TYPE @goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotOrderFieldType")
value: "go.probo.inc/probo/pkg/coredata.SnapshotOrderFieldName"
)
TYPE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.SnapshotOrderFieldType"
)
} }
enum MembershipOrderField enum MembershipOrderField
@goModel( @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipOrderField") {
model: "go.probo.inc/probo/pkg/coredata.MembershipOrderField"
) {
FULL_NAME FULL_NAME
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldFullName" value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldFullName"
@@ -1303,9 +1104,7 @@ enum MembershipOrderField
value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldEmailAddress" value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldEmailAddress"
) )
ROLE ROLE
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldRole")
value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldRole"
)
CREATED_AT CREATED_AT
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldCreatedAt" value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldCreatedAt"
@@ -1313,21 +1112,15 @@ enum MembershipOrderField
} }
enum InvitationOrderField enum InvitationOrderField
@goModel( @goModel(model: "go.probo.inc/probo/pkg/coredata.InvitationOrderField") {
model: "go.probo.inc/probo/pkg/coredata.InvitationOrderField"
) {
FULL_NAME FULL_NAME
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldFullName" value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldFullName"
) )
EMAIL EMAIL
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldEmail")
value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldEmail"
)
ROLE ROLE
@goEnum( @goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldRole")
value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldRole"
)
CREATED_AT CREATED_AT
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldCreatedAt" value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldCreatedAt"
@@ -2347,13 +2140,13 @@ type ProcessingActivity implements Node {
purpose: String purpose: String
dataSubjectCategory: String dataSubjectCategory: String
personalDataCategory: String personalDataCategory: String
specialOrCriminalData: ProcessingActivitySpecialOrCriminalData! specialOrCriminalData: ProcessingActivitySpecialOrCriminalDatum!
consentEvidenceLink: String consentEvidenceLink: String
lawfulBasis: ProcessingActivityLawfulBasis! lawfulBasis: ProcessingActivityLawfulBasis!
recipients: String recipients: String
location: String location: String
internationalTransfers: Boolean! internationalTransfers: Boolean!
transferSafeguards: ProcessingActivityTransferSafeguards transferSafeguards: ProcessingActivityTransferSafeguard
retentionPeriod: String retentionPeriod: String
securityMeasures: String securityMeasures: String
dataProtectionImpactAssessment: ProcessingActivityDataProtectionImpactAssessment! dataProtectionImpactAssessment: ProcessingActivityDataProtectionImpactAssessment!
@@ -3920,13 +3713,13 @@ input CreateProcessingActivityInput {
purpose: String purpose: String
dataSubjectCategory: String dataSubjectCategory: String
personalDataCategory: String personalDataCategory: String
specialOrCriminalData: ProcessingActivitySpecialOrCriminalData! specialOrCriminalData: ProcessingActivitySpecialOrCriminalDatum!
consentEvidenceLink: String consentEvidenceLink: String
lawfulBasis: ProcessingActivityLawfulBasis! lawfulBasis: ProcessingActivityLawfulBasis!
recipients: String recipients: String
location: String location: String
internationalTransfers: Boolean! internationalTransfers: Boolean!
transferSafeguards: ProcessingActivityTransferSafeguards transferSafeguards: ProcessingActivityTransferSafeguard
retentionPeriod: String retentionPeriod: String
securityMeasures: String securityMeasures: String
dataProtectionImpactAssessment: ProcessingActivityDataProtectionImpactAssessment! dataProtectionImpactAssessment: ProcessingActivityDataProtectionImpactAssessment!
@@ -3940,13 +3733,13 @@ input UpdateProcessingActivityInput {
purpose: String @goField(omittable: true) purpose: String @goField(omittable: true)
dataSubjectCategory: String @goField(omittable: true) dataSubjectCategory: String @goField(omittable: true)
personalDataCategory: String @goField(omittable: true) personalDataCategory: String @goField(omittable: true)
specialOrCriminalData: ProcessingActivitySpecialOrCriminalData specialOrCriminalData: ProcessingActivitySpecialOrCriminalDatum
consentEvidenceLink: String consentEvidenceLink: String
lawfulBasis: ProcessingActivityLawfulBasis lawfulBasis: ProcessingActivityLawfulBasis
recipients: String @goField(omittable: true) recipients: String @goField(omittable: true)
location: String @goField(omittable: true) location: String @goField(omittable: true)
internationalTransfers: Boolean internationalTransfers: Boolean
transferSafeguards: ProcessingActivityTransferSafeguards transferSafeguards: ProcessingActivityTransferSafeguard
@goField(omittable: true) @goField(omittable: true)
retentionPeriod: String @goField(omittable: true) retentionPeriod: String @goField(omittable: true)
securityMeasures: String @goField(omittable: true) securityMeasures: String @goField(omittable: true)
@@ -4789,9 +4582,7 @@ type DeleteSnapshotPayload {
} }
enum SSLStatus enum SSLStatus
@goModel( @goModel(model: "go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatus") {
model: "go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatus"
) {
PENDING PENDING
@goEnum( @goEnum(
value: "go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatusPending" value: "go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatusPending"

File diff suppressed because it is too large Load Diff

View File

@@ -69,7 +69,7 @@ func NewProcessingActivity(par *coredata.ProcessingActivity) *ProcessingActivity
Recipients: par.Recipients, Recipients: par.Recipients,
Location: par.Location, Location: par.Location,
InternationalTransfers: par.InternationalTransfers, InternationalTransfers: par.InternationalTransfers,
TransferSafeguards: par.TransferSafeguards, TransferSafeguards: par.TransferSafeguard,
RetentionPeriod: par.RetentionPeriod, RetentionPeriod: par.RetentionPeriod,
SecurityMeasures: par.SecurityMeasures, SecurityMeasures: par.SecurityMeasures,
DataProtectionImpactAssessment: par.DataProtectionImpactAssessment, DataProtectionImpactAssessment: par.DataProtectionImpactAssessment,

View File

@@ -440,13 +440,13 @@ type CreateProcessingActivityInput struct {
Purpose *string `json:"purpose,omitempty"` Purpose *string `json:"purpose,omitempty"`
DataSubjectCategory *string `json:"dataSubjectCategory,omitempty"` DataSubjectCategory *string `json:"dataSubjectCategory,omitempty"`
PersonalDataCategory *string `json:"personalDataCategory,omitempty"` PersonalDataCategory *string `json:"personalDataCategory,omitempty"`
SpecialOrCriminalData coredata.ProcessingActivitySpecialOrCriminalData `json:"specialOrCriminalData"` SpecialOrCriminalData coredata.ProcessingActivitySpecialOrCriminalDatum `json:"specialOrCriminalData"`
ConsentEvidenceLink *string `json:"consentEvidenceLink,omitempty"` ConsentEvidenceLink *string `json:"consentEvidenceLink,omitempty"`
LawfulBasis coredata.ProcessingActivityLawfulBasis `json:"lawfulBasis"` LawfulBasis coredata.ProcessingActivityLawfulBasis `json:"lawfulBasis"`
Recipients *string `json:"recipients,omitempty"` Recipients *string `json:"recipients,omitempty"`
Location *string `json:"location,omitempty"` Location *string `json:"location,omitempty"`
InternationalTransfers bool `json:"internationalTransfers"` InternationalTransfers bool `json:"internationalTransfers"`
TransferSafeguards *coredata.ProcessingActivityTransferSafeguards `json:"transferSafeguards,omitempty"` TransferSafeguard *coredata.ProcessingActivityTransferSafeguard `json:"transferSafeguards,omitempty"`
RetentionPeriod *string `json:"retentionPeriod,omitempty"` RetentionPeriod *string `json:"retentionPeriod,omitempty"`
SecurityMeasures *string `json:"securityMeasures,omitempty"` SecurityMeasures *string `json:"securityMeasures,omitempty"`
DataProtectionImpactAssessment coredata.ProcessingActivityDataProtectionImpactAssessment `json:"dataProtectionImpactAssessment"` DataProtectionImpactAssessment coredata.ProcessingActivityDataProtectionImpactAssessment `json:"dataProtectionImpactAssessment"`
@@ -1518,13 +1518,13 @@ type ProcessingActivity struct {
Purpose *string `json:"purpose,omitempty"` Purpose *string `json:"purpose,omitempty"`
DataSubjectCategory *string `json:"dataSubjectCategory,omitempty"` DataSubjectCategory *string `json:"dataSubjectCategory,omitempty"`
PersonalDataCategory *string `json:"personalDataCategory,omitempty"` PersonalDataCategory *string `json:"personalDataCategory,omitempty"`
SpecialOrCriminalData coredata.ProcessingActivitySpecialOrCriminalData `json:"specialOrCriminalData"` SpecialOrCriminalData coredata.ProcessingActivitySpecialOrCriminalDatum `json:"specialOrCriminalData"`
ConsentEvidenceLink *string `json:"consentEvidenceLink,omitempty"` ConsentEvidenceLink *string `json:"consentEvidenceLink,omitempty"`
LawfulBasis coredata.ProcessingActivityLawfulBasis `json:"lawfulBasis"` LawfulBasis coredata.ProcessingActivityLawfulBasis `json:"lawfulBasis"`
Recipients *string `json:"recipients,omitempty"` Recipients *string `json:"recipients,omitempty"`
Location *string `json:"location,omitempty"` Location *string `json:"location,omitempty"`
InternationalTransfers bool `json:"internationalTransfers"` InternationalTransfers bool `json:"internationalTransfers"`
TransferSafeguards *coredata.ProcessingActivityTransferSafeguards `json:"transferSafeguards,omitempty"` TransferSafeguards *coredata.ProcessingActivityTransferSafeguard `json:"transferSafeguards,omitempty"`
RetentionPeriod *string `json:"retentionPeriod,omitempty"` RetentionPeriod *string `json:"retentionPeriod,omitempty"`
SecurityMeasures *string `json:"securityMeasures,omitempty"` SecurityMeasures *string `json:"securityMeasures,omitempty"`
DataProtectionImpactAssessment coredata.ProcessingActivityDataProtectionImpactAssessment `json:"dataProtectionImpactAssessment"` DataProtectionImpactAssessment coredata.ProcessingActivityDataProtectionImpactAssessment `json:"dataProtectionImpactAssessment"`
@@ -2022,23 +2022,23 @@ type UpdatePeoplePayload struct {
} }
type UpdateProcessingActivityInput struct { type UpdateProcessingActivityInput struct {
ID gid.GID `json:"id"` ID gid.GID `json:"id"`
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Purpose graphql.Omittable[*string] `json:"purpose,omitempty"` Purpose graphql.Omittable[*string] `json:"purpose,omitempty"`
DataSubjectCategory graphql.Omittable[*string] `json:"dataSubjectCategory,omitempty"` DataSubjectCategory graphql.Omittable[*string] `json:"dataSubjectCategory,omitempty"`
PersonalDataCategory graphql.Omittable[*string] `json:"personalDataCategory,omitempty"` PersonalDataCategory graphql.Omittable[*string] `json:"personalDataCategory,omitempty"`
SpecialOrCriminalData *coredata.ProcessingActivitySpecialOrCriminalData `json:"specialOrCriminalData,omitempty"` SpecialOrCriminalData *coredata.ProcessingActivitySpecialOrCriminalDatum `json:"specialOrCriminalData,omitempty"`
ConsentEvidenceLink *string `json:"consentEvidenceLink,omitempty"` ConsentEvidenceLink *string `json:"consentEvidenceLink,omitempty"`
LawfulBasis *coredata.ProcessingActivityLawfulBasis `json:"lawfulBasis,omitempty"` LawfulBasis *coredata.ProcessingActivityLawfulBasis `json:"lawfulBasis,omitempty"`
Recipients graphql.Omittable[*string] `json:"recipients,omitempty"` Recipients graphql.Omittable[*string] `json:"recipients,omitempty"`
Location graphql.Omittable[*string] `json:"location,omitempty"` Location graphql.Omittable[*string] `json:"location,omitempty"`
InternationalTransfers *bool `json:"internationalTransfers,omitempty"` InternationalTransfers *bool `json:"internationalTransfers,omitempty"`
TransferSafeguards graphql.Omittable[*coredata.ProcessingActivityTransferSafeguards] `json:"transferSafeguards,omitempty"` TransferSafeguards graphql.Omittable[*coredata.ProcessingActivityTransferSafeguard] `json:"transferSafeguards,omitempty"`
RetentionPeriod graphql.Omittable[*string] `json:"retentionPeriod,omitempty"` RetentionPeriod graphql.Omittable[*string] `json:"retentionPeriod,omitempty"`
SecurityMeasures graphql.Omittable[*string] `json:"securityMeasures,omitempty"` SecurityMeasures graphql.Omittable[*string] `json:"securityMeasures,omitempty"`
DataProtectionImpactAssessment *coredata.ProcessingActivityDataProtectionImpactAssessment `json:"dataProtectionImpactAssessment,omitempty"` DataProtectionImpactAssessment *coredata.ProcessingActivityDataProtectionImpactAssessment `json:"dataProtectionImpactAssessment,omitempty"`
TransferImpactAssessment *coredata.ProcessingActivityTransferImpactAssessment `json:"transferImpactAssessment,omitempty"` TransferImpactAssessment *coredata.ProcessingActivityTransferImpactAssessment `json:"transferImpactAssessment,omitempty"`
VendorIds []gid.GID `json:"vendorIds,omitempty"` VendorIds []gid.GID `json:"vendorIds,omitempty"`
} }
type UpdateProcessingActivityPayload struct { type UpdateProcessingActivityPayload struct {

View File

@@ -1384,9 +1384,7 @@ func (r *mutationResolver) UploadTrustCenterNda(ctx context.Context, input types
func (r *mutationResolver) DeleteTrustCenterNda(ctx context.Context, input types.DeleteTrustCenterNDAInput) (*types.DeleteTrustCenterNDAPayload, error) { func (r *mutationResolver) DeleteTrustCenterNda(ctx context.Context, input types.DeleteTrustCenterNDAInput) (*types.DeleteTrustCenterNDAPayload, error) {
prb := r.ProboService(ctx, input.TrustCenterID.TenantID()) prb := r.ProboService(ctx, input.TrustCenterID.TenantID())
trustCenter, file, err := prb.TrustCenters.DeleteNDA(ctx, &probo.DeleteTrustCenterNDARequest{ trustCenter, file, err := prb.TrustCenters.DeleteNDA(ctx, input.TrustCenterID)
TrustCenterID: input.TrustCenterID,
})
if err != nil { if err != nil {
panic(fmt.Errorf("cannot delete trust center NDA: %w", err)) panic(fmt.Errorf("cannot delete trust center NDA: %w", err))
} }
@@ -1443,9 +1441,7 @@ func (r *mutationResolver) UpdateTrustCenterAccess(ctx context.Context, input ty
func (r *mutationResolver) DeleteTrustCenterAccess(ctx context.Context, input types.DeleteTrustCenterAccessInput) (*types.DeleteTrustCenterAccessPayload, error) { func (r *mutationResolver) DeleteTrustCenterAccess(ctx context.Context, input types.DeleteTrustCenterAccessInput) (*types.DeleteTrustCenterAccessPayload, error) {
prb := r.ProboService(ctx, input.ID.TenantID()) prb := r.ProboService(ctx, input.ID.TenantID())
err := prb.TrustCenterAccesses.Delete(ctx, &probo.DeleteTrustCenterAccessRequest{ err := prb.TrustCenterAccesses.Delete(ctx, input.ID)
ID: input.ID,
})
if err != nil { if err != nil {
panic(fmt.Errorf("cannot delete trust center access: %w", err)) panic(fmt.Errorf("cannot delete trust center access: %w", err))
} }
@@ -1515,9 +1511,7 @@ func (r *mutationResolver) UpdateTrustCenterReference(ctx context.Context, input
func (r *mutationResolver) DeleteTrustCenterReference(ctx context.Context, input types.DeleteTrustCenterReferenceInput) (*types.DeleteTrustCenterReferencePayload, error) { func (r *mutationResolver) DeleteTrustCenterReference(ctx context.Context, input types.DeleteTrustCenterReferenceInput) (*types.DeleteTrustCenterReferencePayload, error) {
prb := r.ProboService(ctx, input.ID.TenantID()) prb := r.ProboService(ctx, input.ID.TenantID())
err := prb.TrustCenterReferences.Delete(ctx, &probo.DeleteTrustCenterReferenceRequest{ err := prb.TrustCenterReferences.Delete(ctx, input.ID)
ID: input.ID,
})
if err != nil { if err != nil {
panic(fmt.Errorf("cannot delete trust center reference: %w", err)) panic(fmt.Errorf("cannot delete trust center reference: %w", err))
} }
@@ -1589,9 +1583,7 @@ func (r *mutationResolver) GetTrustCenterFile(ctx context.Context, input types.G
func (r *mutationResolver) DeleteTrustCenterFile(ctx context.Context, input types.DeleteTrustCenterFileInput) (*types.DeleteTrustCenterFilePayload, error) { func (r *mutationResolver) DeleteTrustCenterFile(ctx context.Context, input types.DeleteTrustCenterFileInput) (*types.DeleteTrustCenterFilePayload, error) {
prb := r.ProboService(ctx, input.ID.TenantID()) prb := r.ProboService(ctx, input.ID.TenantID())
err := prb.TrustCenterFiles.Delete(ctx, &probo.DeleteTrustCenterFileRequest{ err := prb.TrustCenterFiles.Delete(ctx, input.ID)
ID: input.ID,
})
if err != nil { if err != nil {
panic(fmt.Errorf("cannot delete trust center file: %w", err)) panic(fmt.Errorf("cannot delete trust center file: %w", err))
} }
@@ -2858,7 +2850,7 @@ func (r *mutationResolver) PublishDocumentVersion(ctx context.Context, input typ
if err != nil { if err != nil {
var errNoChanges *coredata.ErrDocumentVersionNoChanges var errNoChanges *coredata.ErrDocumentVersionNoChanges
if errors.As(err, &errNoChanges) { if errors.As(err, &errNoChanges) {
return nil, gqlutils.Invalid(errNoChanges) return nil, gqlutils.Invalid(errNoChanges, nil)
} }
panic(fmt.Errorf("cannot publish document version: %w", err)) panic(fmt.Errorf("cannot publish document version: %w", err))
} }
@@ -3568,7 +3560,7 @@ func (r *mutationResolver) CreateProcessingActivity(ctx context.Context, input t
Recipients: input.Recipients, Recipients: input.Recipients,
Location: input.Location, Location: input.Location,
InternationalTransfers: input.InternationalTransfers, InternationalTransfers: input.InternationalTransfers,
TransferSafeguards: input.TransferSafeguards, TransferSafeguard: input.TransferSafeguard,
RetentionPeriod: input.RetentionPeriod, RetentionPeriod: input.RetentionPeriod,
SecurityMeasures: input.SecurityMeasures, SecurityMeasures: input.SecurityMeasures,
DataProtectionImpactAssessment: input.DataProtectionImpactAssessment, DataProtectionImpactAssessment: input.DataProtectionImpactAssessment,
@@ -3601,7 +3593,7 @@ func (r *mutationResolver) UpdateProcessingActivity(ctx context.Context, input t
Recipients: UnwrapOmittable(input.Recipients), Recipients: UnwrapOmittable(input.Recipients),
Location: UnwrapOmittable(input.Location), Location: UnwrapOmittable(input.Location),
InternationalTransfers: input.InternationalTransfers, InternationalTransfers: input.InternationalTransfers,
TransferSafeguards: UnwrapOmittable(input.TransferSafeguards), TransferSafeguard: UnwrapOmittable(input.TransferSafeguards),
RetentionPeriod: UnwrapOmittable(input.RetentionPeriod), RetentionPeriod: UnwrapOmittable(input.RetentionPeriod),
SecurityMeasures: UnwrapOmittable(input.SecurityMeasures), SecurityMeasures: UnwrapOmittable(input.SecurityMeasures),
DataProtectionImpactAssessment: input.DataProtectionImpactAssessment, DataProtectionImpactAssessment: input.DataProtectionImpactAssessment,

View File

@@ -30,9 +30,7 @@ func Unauthorized() *gqlerror.Error {
} }
func AuthenticationRequired(details map[string]any) *gqlerror.Error { func AuthenticationRequired(details map[string]any) *gqlerror.Error {
extensions := map[string]any{ extensions := map[string]any{"code": "AUTHENTICATION_REQUIRED"}
"code": "AUTHENTICATION_REQUIRED",
}
maps.Copy(extensions, details) maps.Copy(extensions, details)
return &gqlerror.Error{ return &gqlerror.Error{
@@ -59,11 +57,14 @@ func Conflict(err error) *gqlerror.Error {
} }
} }
func Invalid(err error) *gqlerror.Error { func Invalid(err error, details map[string]any) *gqlerror.Error {
extensions := map[string]any{"code": "INVALID_REQUEST"}
if details != nil {
maps.Copy(extensions, details)
}
return &gqlerror.Error{ return &gqlerror.Error{
Message: err.Error(), Message: err.Error(),
Extensions: map[string]any{ Extensions: extensions,
"code": "INVALID",
},
} }
} }

View File

@@ -24,6 +24,7 @@ import (
"go.gearno.de/kit/log" "go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/auth" "go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/authz" "go.probo.inc/probo/pkg/authz"
"go.probo.inc/probo/pkg/validator"
) )
func RecoverFunc(ctx context.Context, err any) error { func RecoverFunc(ctx context.Context, err any) error {
@@ -50,6 +51,27 @@ func RecoverFunc(ctx context.Context, err any) error {
}) })
} }
var errValidations validator.ValidationErrors
if errors.As(asError(err), &errValidations) {
gqlErrors := gqlerror.List{}
for _, err := range errValidations {
gqlErrors = append(
gqlErrors,
Invalid(
err,
map[string]any{
"cause": err.Code,
"field": err.Field,
"value": err.Value,
},
),
)
}
return gqlErrors
}
var tenantAccessErr *authz.TenantAccessError var tenantAccessErr *authz.TenantAccessError
if errTyped, ok := err.(error); ok && errors.As(errTyped, &tenantAccessErr) { if errTyped, ok := err.(error); ok && errors.As(errTyped, &tenantAccessErr) {
return Unauthorized() return Unauthorized()
@@ -65,5 +87,6 @@ func asError(err any) error {
if e, ok := err.(error); ok { if e, ok := err.(error); ok {
return e return e
} }
return errors.New("unknown panic") return errors.New("unknown panic")
} }

View File

@@ -0,0 +1,201 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"testing"
)
// CustomType simulates types like gid.GID
type CustomType string
func TestCheckEach_EmptyTypedSlice(t *testing.T) {
v := New()
// Simulate what happens with []gid.GID{} (empty slice of custom type)
emptySlice := []CustomType{}
v.CheckEach(emptySlice, "items", func(index int, item any) {
// This callback should never be called for an empty slice
t.Error("callback should not be called for empty slice")
})
if v.HasErrors() {
t.Errorf("unexpected error for empty slice: %v", v.Error())
}
}
func TestCheckEach_NonEmptyTypedSlice(t *testing.T) {
v := New()
// Simulate what happens with []gid.GID{"abc", "def"}
slice := []CustomType{"abc", "def"}
callCount := 0
v.CheckEach(slice, "items", func(index int, item any) {
callCount++
// Verify the item is the correct type
str, ok := item.(CustomType)
if !ok {
t.Errorf("expected CustomType, got %T", item)
}
if index == 0 && str != "abc" {
t.Errorf("expected 'abc', got %s", str)
}
if index == 1 && str != "def" {
t.Errorf("expected 'def', got %s", str)
}
})
if callCount != 2 {
t.Errorf("expected callback to be called 2 times, got %d", callCount)
}
if v.HasErrors() {
t.Errorf("unexpected error: %v", v.Error())
}
}
func TestCheckEach_NilTypedSlice(t *testing.T) {
v := New()
// Simulate what happens with var x []gid.GID (nil slice)
var nilSlice []CustomType
v.CheckEach(nilSlice, "items", func(index int, item any) {
// This callback should never be called for a nil slice
t.Error("callback should not be called for nil slice")
})
if v.HasErrors() {
t.Errorf("unexpected error for nil slice: %v", v.Error())
}
}
func TestCheckEach_PointerToNonEmptySlice(t *testing.T) {
v := New()
// Simulate what happens with *[]gid.GID (pointer to slice)
slice := []CustomType{"abc", "def", "ghi"}
ptrToSlice := &slice
callCount := 0
v.CheckEach(ptrToSlice, "items", func(index int, item any) {
callCount++
str, ok := item.(CustomType)
if !ok {
t.Errorf("expected CustomType, got %T", item)
}
expectedValues := []CustomType{"abc", "def", "ghi"}
if str != expectedValues[index] {
t.Errorf("at index %d: expected %s, got %s", index, expectedValues[index], str)
}
})
if callCount != 3 {
t.Errorf("expected callback to be called 3 times, got %d", callCount)
}
if v.HasErrors() {
t.Errorf("unexpected error for pointer to slice: %v", v.Error())
}
}
func TestCheckEach_PointerToEmptySlice(t *testing.T) {
v := New()
// Simulate what happens with *[]gid.GID{} (pointer to empty slice)
slice := []CustomType{}
ptrToSlice := &slice
v.CheckEach(ptrToSlice, "items", func(index int, item any) {
t.Error("callback should not be called for empty slice")
})
if v.HasErrors() {
t.Errorf("unexpected error for pointer to empty slice: %v", v.Error())
}
}
func TestCheckEach_NilPointerToSlice(t *testing.T) {
v := New()
// Simulate what happens with var x *[]gid.GID (nil pointer to slice)
var nilPtrToSlice *[]CustomType
v.CheckEach(nilPtrToSlice, "items", func(index int, item any) {
t.Error("callback should not be called for nil pointer to slice")
})
if v.HasErrors() {
t.Errorf("unexpected error for nil pointer to slice: %v", v.Error())
}
}
func TestCheckEach_DoublePointerToSlice(t *testing.T) {
v := New()
// Simulate what happens with **[]gid.GID (double pointer to slice)
slice := []CustomType{"x", "y"}
ptrToSlice := &slice
doublePtrToSlice := &ptrToSlice
callCount := 0
v.CheckEach(doublePtrToSlice, "items", func(index int, item any) {
callCount++
str, ok := item.(CustomType)
if !ok {
t.Errorf("expected CustomType, got %T", item)
}
expectedValues := []CustomType{"x", "y"}
if str != expectedValues[index] {
t.Errorf("at index %d: expected %s, got %s", index, expectedValues[index], str)
}
})
if callCount != 2 {
t.Errorf("expected callback to be called 2 times, got %d", callCount)
}
if v.HasErrors() {
t.Errorf("unexpected error for double pointer to slice: %v", v.Error())
}
}
func TestCheckEach_NonSliceValue(t *testing.T) {
v := New()
// Pass a non-slice value
notASlice := "this is a string"
v.CheckEach(notASlice, "items", func(index int, item any) {
t.Error("callback should not be called for non-slice value")
})
if !v.HasErrors() {
t.Error("expected error for non-slice value")
}
errors := v.Errors()
if len(errors) != 1 {
t.Errorf("expected 1 error, got %d", len(errors))
}
if errors[0].Code != ErrorCodeInvalidFormat {
t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, errors[0].Code)
}
if errors[0].Message != "expected a slice" {
t.Errorf("expected message 'expected a slice', got '%s'", errors[0].Message)
}
}

View File

@@ -0,0 +1,98 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator_test
import (
"testing"
"go.probo.inc/probo/pkg/validator"
)
func TestDoublePointerValidation(t *testing.T) {
t.Run("valid double pointer string", func(t *testing.T) {
v := validator.New()
str := "hello"
ptr := &str
doublePtr := &ptr
v.Check(doublePtr, "name", validator.Required(), validator.NotEmpty(), validator.MaxLen(1000))
if v.HasErrors() {
t.Errorf("expected no errors, got: %v", v.Error())
}
})
t.Run("invalid double pointer string - empty", func(t *testing.T) {
v := validator.New()
str := ""
ptr := &str
doublePtr := &ptr
v.Check(doublePtr, "name", validator.Required(), validator.NotEmpty())
if !v.HasErrors() {
t.Error("expected errors for empty string")
}
})
t.Run("invalid double pointer string - too long", func(t *testing.T) {
v := validator.New()
str := "this is a very long string that exceeds the maximum length"
ptr := &str
doublePtr := &ptr
v.Check(doublePtr, "name", validator.Required(), validator.MaxLen(10))
if !v.HasErrors() {
t.Error("expected errors for string exceeding max length")
}
})
t.Run("optional double pointer - nil outer pointer", func(t *testing.T) {
v := validator.New()
var doublePtr **string = nil
v.Check(doublePtr, "name", validator.NotEmpty(), validator.MaxLen(1000))
if v.HasErrors() {
t.Errorf("expected no errors for nil optional field, got: %v", v.Error())
}
})
t.Run("optional double pointer - nil inner pointer", func(t *testing.T) {
v := validator.New()
var ptr *string = nil
doublePtr := &ptr
v.Check(doublePtr, "name", validator.NotEmpty(), validator.MaxLen(1000))
if v.HasErrors() {
t.Errorf("expected no errors for nil optional field, got: %v", v.Error())
}
})
t.Run("optional double pointer - valid value", func(t *testing.T) {
v := validator.New()
str := "hello"
ptr := &str
doublePtr := &ptr
v.Check(doublePtr, "name", validator.NotEmpty(), validator.MaxLen(1000))
if v.HasErrors() {
t.Errorf("expected no errors, got: %v", v.Error())
}
})
}

107
pkg/validator/errors.go Normal file
View File

@@ -0,0 +1,107 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"fmt"
"strings"
)
type ErrorCode string
const (
ErrorCodeRequired ErrorCode = "REQUIRED"
ErrorCodeInvalidFormat ErrorCode = "INVALID_FORMAT"
ErrorCodeOutOfRange ErrorCode = "OUT_OF_RANGE"
ErrorCodeTooShort ErrorCode = "TOO_SHORT"
ErrorCodeTooLong ErrorCode = "TOO_LONG"
ErrorCodeInvalidEmail ErrorCode = "INVALID_EMAIL"
ErrorCodeInvalidURL ErrorCode = "INVALID_URL"
ErrorCodeInvalidEnum ErrorCode = "INVALID_ENUM"
ErrorCodeInvalidGID ErrorCode = "INVALID_GID"
ErrorCodeUnsafeContent ErrorCode = "UNSAFE_CONTENT"
ErrorCodeCustom ErrorCode = "CUSTOM"
)
type ValidationError struct {
Field string
Code ErrorCode
Message string
Value any
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s %s", e.Field, e.Message)
}
type ValidationErrors []*ValidationError
func (ve ValidationErrors) Error() string {
if len(ve) == 0 {
return ""
}
var messages []string
for _, err := range ve {
messages = append(messages, err.Error())
}
return strings.Join(messages, "; ")
}
func (ve ValidationErrors) HasErrors() bool {
return len(ve) > 0
}
func (ve ValidationErrors) Fields() []string {
fields := make([]string, 0, len(ve))
for _, err := range ve {
fields = append(fields, err.Field)
}
return fields
}
func (ve ValidationErrors) ByField(field string) ValidationErrors {
var errors ValidationErrors
for _, err := range ve {
if err.Field == field {
errors = append(errors, err)
}
}
return errors
}
func (ve ValidationErrors) ByCode(code ErrorCode) ValidationErrors {
var errors ValidationErrors
for _, err := range ve {
if err.Code == code {
errors = append(errors, err)
}
}
return errors
}
func (ve ValidationErrors) First() *ValidationError {
if len(ve) == 0 {
return nil
}
return ve[0]
}
func newValidationError(code ErrorCode, message string) *ValidationError {
return &ValidationError{
Code: code,
Message: message,
}
}

View File

@@ -0,0 +1,82 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"testing"
)
// AssetType simulates coredata.AssetType
type AssetType string
const (
AssetTypePhysical AssetType = "PHYSICAL"
AssetTypeVirtual AssetType = "VIRTUAL"
)
func (at AssetType) String() string {
return string(at)
}
func TestOneOf_CustomStringType(t *testing.T) {
tests := []struct {
name string
value any
allowed []string
expectError bool
}{
{
name: "valid custom type - physical",
value: AssetTypePhysical,
allowed: []string{"PHYSICAL", "VIRTUAL"},
expectError: false,
},
{
name: "valid custom type - virtual",
value: AssetTypeVirtual,
allowed: []string{"PHYSICAL", "VIRTUAL"},
expectError: false,
},
{
name: "invalid custom type",
value: AssetType("INVALID"),
allowed: []string{"PHYSICAL", "VIRTUAL"},
expectError: true,
},
{
name: "custom type not in allowed list",
value: AssetTypePhysical,
allowed: []string{"VIRTUAL"},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := New()
v.Check(tt.value, "asset_type", OneOfSlice(tt.allowed))
if tt.expectError {
if !v.HasErrors() {
t.Error("expected error but got none")
}
} else {
if v.HasErrors() {
t.Errorf("unexpected error: %v", v.Error())
}
}
})
}
}

View File

@@ -0,0 +1,116 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"testing"
"go.probo.inc/probo/pkg/gid"
)
// CustomStringType simulates coredata.AssetType
type CustomStringType string
func (c CustomStringType) String() string {
return string(c)
}
func TestOptional_WithGIDPointer(t *testing.T) {
tenantID := gid.NewTenantID()
tests := []struct {
name string
value *gid.GID
expectError bool
}{
{
name: "nil pointer - should skip validation",
value: nil,
expectError: false,
},
{
name: "valid GID pointer",
value: func() *gid.GID {
g := gid.New(tenantID, 100)
return &g
}(),
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := New()
v.Check(tt.value, "owner_id", GID(100))
if tt.expectError {
if !v.HasErrors() {
t.Error("expected error but got none")
}
} else {
if v.HasErrors() {
t.Errorf("unexpected error: %v", v.Error())
}
}
})
}
}
func TestOptional_WithCustomTypePointer(t *testing.T) {
tests := []struct {
name string
value *CustomStringType
expectError bool
}{
{
name: "nil pointer - should skip validation",
value: nil,
expectError: false,
},
{
name: "valid custom type pointer",
value: func() *CustomStringType {
v := CustomStringType("VALID")
return &v
}(),
expectError: false,
},
{
name: "invalid custom type pointer",
value: func() *CustomStringType {
v := CustomStringType("INVALID")
return &v
}(),
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := New()
v.Check(tt.value, "asset_type", OneOf("VALID", "ANOTHER"))
if tt.expectError {
if !v.HasErrors() {
t.Error("expected error but got none")
}
} else {
if v.HasErrors() {
t.Errorf("unexpected error: %v", v.Error())
}
}
})
}
}

149
pkg/validator/validation.go Normal file
View File

@@ -0,0 +1,149 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"fmt"
"reflect"
)
type Validator struct {
errors ValidationErrors
}
func New() *Validator {
return &Validator{
errors: ValidationErrors{},
}
}
func (v *Validator) Check(value any, field string, validators ...ValidatorFunc) {
if len(validators) == 0 {
return
}
// Dereference pointer values to get the actual value for validation
actualValue := value
if value != nil {
val := reflect.ValueOf(value)
// Dereference all pointer levels
for val.Kind() == reflect.Ptr && !val.IsNil() {
val = val.Elem()
actualValue = val.Interface()
}
// If we ended up with a nil pointer at any level, set actualValue to nil
if val.Kind() == reflect.Ptr && val.IsNil() {
actualValue = nil
}
}
for _, validator := range validators {
if err := validator(actualValue); err != nil {
v.errors = append(v.errors, &ValidationError{
Field: field,
Code: err.Code,
Message: err.Message,
Value: value,
})
}
}
}
func (v *Validator) CheckEach(items any, field string, fn func(index int, item any)) {
if items == nil {
return
}
if slice, ok := items.([]any); ok {
for i, item := range slice {
fn(i, item)
}
return
}
val := reflect.ValueOf(items)
// Dereference pointer levels to get to the actual slice
for val.Kind() == reflect.Ptr {
if val.IsNil() {
return
}
val = val.Elem()
}
if val.Kind() != reflect.Slice {
v.errors = append(v.errors, &ValidationError{
Field: field,
Code: ErrorCodeInvalidFormat,
Message: "expected a slice",
Value: items,
})
return
}
for i := 0; i < val.Len(); i++ {
fn(i, val.Index(i).Interface())
}
}
func (v *Validator) CheckNested(field string, fn func(v *Validator)) {
nestedValidator := New()
fn(nestedValidator)
for _, err := range nestedValidator.errors {
prefixedErr := &ValidationError{
Field: fmt.Sprintf("%s.%s", field, err.Field),
Code: err.Code,
Message: err.Message,
Value: err.Value,
}
v.errors = append(v.errors, prefixedErr)
}
}
func (v *Validator) HasErrors() bool {
return len(v.errors) > 0
}
func (v *Validator) Errors() ValidationErrors {
return v.errors
}
func (v *Validator) Error() error {
if len(v.errors) == 0 {
return nil
}
return v.errors
}
type ValidatorFunc func(value any) *ValidationError
// dereferenceValue recursively dereferences all pointer levels.
// Returns the final dereferenced value and a boolean indicating if any pointer in the chain was nil.
func dereferenceValue(value any) (any, bool) {
if value == nil {
return nil, true
}
val := reflect.ValueOf(value)
// Dereference all pointer levels
for val.Kind() == reflect.Ptr {
if val.IsNil() {
return nil, true
}
val = val.Elem()
}
return val.Interface(), false
}

View File

@@ -0,0 +1,425 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"testing"
"time"
)
func BenchmarkValidate_SingleField(b *testing.B) {
email := "test@example.com"
b.ResetTimer()
for i := 0; i < b.N; i++ {
v := New()
v.Check(&email, "email", Required(), Email())
}
}
func BenchmarkValidate_MultipleFields(b *testing.B) {
email := "test@example.com"
password := "password123"
age := 25
b.ResetTimer()
for i := 0; i < b.N; i++ {
v := New()
v.Check(&email, "email", Required(), Email())
v.Check(&password, "password", Required(), MinLen(8))
v.Check(&age, "age", Min(18), Max(120))
}
}
func BenchmarkValidate_OptionalField(b *testing.B) {
var website *string
b.ResetTimer()
for i := 0; i < b.N; i++ {
v := New()
v.Check(website, "website", URL())
}
}
func BenchmarkValidate_NestedStruct(b *testing.B) {
type Address struct {
City string
ZipCode string
}
type User struct {
Name string
Address Address
}
user := User{
Name: "John Doe",
Address: Address{
City: "New York",
ZipCode: "10001",
},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
v := New()
v.Check(&user.Name, "name", Required())
v.CheckNested("address", func(av *Validator) {
av.Check(&user.Address.City, "city", Required())
av.Check(&user.Address.ZipCode, "zipCode", Pattern(`^\d{5}$`, ""))
})
}
}
func BenchmarkValidate_ArrayValidation(b *testing.B) {
type Item struct {
Name string
Price int
}
items := []Item{
{Name: "Item 1", Price: 100},
{Name: "Item 2", Price: 200},
{Name: "Item 3", Price: 300},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
v := New()
for j, item := range items {
v.CheckNested("items[0]", func(iv *Validator) {
iv.Check(&item.Name, "name", Required())
iv.Check(&item.Price, "price", Min(0))
_ = j
})
}
}
}
func BenchmarkEmail(b *testing.B) {
email := "test@example.com"
validator := Email()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&email)
}
}
func BenchmarkURL(b *testing.B) {
urlStr := "https://example.com"
validator := URL()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&urlStr)
}
}
func BenchmarkUUID(b *testing.B) {
uuid := "550e8400-e29b-41d4-a716-446655440000"
validator := UUID()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&uuid)
}
}
func BenchmarkMinLen(b *testing.B) {
str := "hello world"
validator := MinLen(5)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&str)
}
}
func BenchmarkMin(b *testing.B) {
num := 42
validator := Min(18)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&num)
}
}
func BenchmarkMinFloat(b *testing.B) {
num := 99.99
validator := MinFloat(0.01)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&num)
}
}
func BenchmarkMaxFloat(b *testing.B) {
num := 50.50
validator := MaxFloat(99.99)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&num)
}
}
func BenchmarkRangeFloat(b *testing.B) {
num := 50.50
validator := RangeFloat(0.01, 99.99)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&num)
}
}
func BenchmarkNotEmpty(b *testing.B) {
str := "hello world"
validator := NotEmpty()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&str)
}
}
func BenchmarkPattern(b *testing.B) {
zipCode := "12345"
validator := Pattern(`^\d{5}$`, "")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&zipCode)
}
}
func BenchmarkValidate_WithErrors(b *testing.B) {
email := "invalid-email"
b.ResetTimer()
for i := 0; i < b.N; i++ {
v := New()
v.Check(&email, "email", Required(), Email())
if !v.HasErrors() {
b.Fatal("expected validation error")
}
}
}
func BenchmarkValidate_ComplexForm(b *testing.B) {
type Address struct {
Street string
City string
ZipCode string
}
type User struct {
Email string
Name string
Age int
Website *string
PhoneNumber *string
Price float64
Address Address
}
website := "https://example.com"
user := User{
Email: "user@example.com",
Name: "John Doe",
Age: 30,
Website: &website,
PhoneNumber: nil,
Price: 99.99,
Address: Address{
Street: "123 Main St",
City: "New York",
ZipCode: "10001",
},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
v := New()
v.Check(&user.Email, "email", Required(), Email())
v.Check(&user.Name, "name", Required(), MinLen(2))
v.Check(&user.Age, "age", Min(18), Max(120))
v.Check(user.Website, "website", URL())
v.Check(user.PhoneNumber, "phoneNumber", MinLen(10))
v.Check(&user.Price, "price", MinFloat(0.01))
v.CheckNested("address", func(av *Validator) {
av.Check(&user.Address.Street, "street", Required())
av.Check(&user.Address.City, "city", Required())
av.Check(&user.Address.ZipCode, "zipCode", Pattern(`^\d{5}$`, ""))
})
}
}
func BenchmarkMinItems(b *testing.B) {
items := []string{"a", "b", "c"}
validator := MinItems(2)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&items)
}
}
func BenchmarkMaxItems(b *testing.B) {
items := []string{"a", "b", "c"}
validator := MaxItems(5)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&items)
}
}
func BenchmarkUniqueItems(b *testing.B) {
items := []string{"a", "b", "c"}
validator := UniqueItems()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&items)
}
}
func BenchmarkAlphaNumeric(b *testing.B) {
str := "abc123DEF456"
validator := AlphaNumeric()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&str)
}
}
func BenchmarkNoSpaces(b *testing.B) {
str := "hello-world-test"
validator := NoSpaces()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&str)
}
}
func BenchmarkSlug(b *testing.B) {
str := "hello-world-123"
validator := Slug()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&str)
}
}
func BenchmarkAfter(b *testing.B) {
now := time.Now()
future := now.Add(24 * time.Hour)
validator := After(now)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&future)
}
}
func BenchmarkBefore(b *testing.B) {
now := time.Now()
past := now.Add(-24 * time.Hour)
validator := Before(now)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&past)
}
}
func BenchmarkFutureDate(b *testing.B) {
future := time.Now().Add(24 * time.Hour)
validator := FutureDate()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&future)
}
}
func BenchmarkPastDate(b *testing.B) {
past := time.Now().Add(-24 * time.Hour)
validator := PastDate()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&past)
}
}
func BenchmarkEqualTo(b *testing.B) {
str1 := "password"
str2 := "password"
validator := EqualTo(&str2)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&str1)
}
}
func BenchmarkNotEqualTo(b *testing.B) {
str1 := "password"
str2 := "different"
validator := NotEqualTo(&str2)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&str1)
}
}
func BenchmarkDomain(b *testing.B) {
str := "api.example.com"
validator := Domain()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&str)
}
}
func BenchmarkHTTPUrl(b *testing.B) {
str := "http://api.example.com/v1/users"
validator := HTTPUrl()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&str)
}
}
func BenchmarkHTTPSUrl(b *testing.B) {
str := "https://api.example.com/v1/users"
validator := HTTPSUrl()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&str)
}
}

View File

@@ -0,0 +1,451 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"fmt"
"testing"
"go.gearno.de/x/ref"
)
func TestValidator_Validate(t *testing.T) {
t.Run("single field validation", func(t *testing.T) {
v := New()
email := "test@example.com"
v.Check(&email, "email", Required(), Email())
if v.HasErrors() {
t.Errorf("expected no errors, got: %v", v.Errors())
}
})
t.Run("multiple field validations", func(t *testing.T) {
v := New()
email := ""
password := "123"
v.Check(&email, "email", Required(), Email())
v.Check(&password, "password", Required(), MinLen(8))
if !v.HasErrors() {
t.Error("expected validation errors")
}
errors := v.Errors()
// email: 1 error (Required), password: 1 error (MinLen - Required passes because it's not empty)
if len(errors) != 2 {
t.Errorf("expected 2 errors, got %d: %v", len(errors), errors)
}
})
t.Run("collect multiple errors for same field", func(t *testing.T) {
v := New()
value := "abc"
v.Check(&value, "password", MinLen(8), MaxLen(5))
errors := v.Errors()
// Both MinLen and MaxLen will fail (too short and somehow conflicts, but logically MinLen will fail)
if len(errors) < 1 {
t.Errorf("expected at least 1 error, got %d", len(errors))
}
})
}
func TestValidator_CheckNested(t *testing.T) {
v := New()
v.CheckNested("user", func(nv *Validator) {
email := "invalid"
nv.Check(&email, "email", Email())
nv.CheckNested("address", func(av *Validator) {
city := ""
av.Check(&city, "city", Required())
})
})
if !v.HasErrors() {
t.Error("expected validation errors")
}
errors := v.Errors()
if len(errors) != 2 {
t.Errorf("expected 2 errors, got %d", len(errors))
}
// Check field paths
expectedFields := map[string]bool{
"user.email": true,
"user.address.city": true,
}
for _, err := range errors {
if !expectedFields[err.Field] {
t.Errorf("unexpected field path: %s", err.Field)
}
}
}
func TestValidator_Error(t *testing.T) {
t.Run("no errors", func(t *testing.T) {
v := New()
if v.Error() != nil {
t.Errorf("expected nil error, got: %v", v.Error())
}
})
t.Run("with errors", func(t *testing.T) {
v := New()
email := ""
v.Check(&email, "email", Required())
err := v.Error()
if err == nil {
t.Error("expected error, got nil")
}
})
}
func TestValidationErrors_Methods(t *testing.T) {
errors := ValidationErrors{
{Field: "email", Code: ErrorCodeInvalidEmail, Message: "invalid email"},
{Field: "password", Code: ErrorCodeTooShort, Message: "too short"},
{Field: "email", Code: ErrorCodeRequired, Message: "required"},
}
t.Run("Fields", func(t *testing.T) {
fields := errors.Fields()
if len(fields) != 3 {
t.Errorf("expected 3 fields, got %d", len(fields))
}
})
t.Run("ByField", func(t *testing.T) {
emailErrors := errors.ByField("email")
if len(emailErrors) != 2 {
t.Errorf("expected 2 email errors, got %d", len(emailErrors))
}
})
t.Run("ByCode", func(t *testing.T) {
requiredErrors := errors.ByCode(ErrorCodeRequired)
if len(requiredErrors) != 1 {
t.Errorf("expected 1 required error, got %d", len(requiredErrors))
}
})
t.Run("First", func(t *testing.T) {
first := errors.First()
if first == nil {
t.Error("expected first error")
}
if first.Field != "email" {
t.Errorf("expected first field to be 'email', got '%s'", first.Field)
}
})
t.Run("Error", func(t *testing.T) {
errorStr := errors.Error()
if errorStr == "" {
t.Error("expected non-empty error string")
}
})
}
func TestOptionalFieldExample(t *testing.T) {
type CreateUserRequest struct {
Email string
Name string
Website *string
PhoneNumber *string
Age *int
}
website := "not-a-url"
req := CreateUserRequest{
Email: "user@example.com",
Name: "John Doe",
Website: &website,
PhoneNumber: nil,
Age: nil,
}
v := New()
v.Check(&req.Email, "email", Required(), Email())
v.Check(&req.Name, "name", Required(), MinLen(2))
v.Check(req.Website, "website", URL())
v.Check(req.PhoneNumber, "phoneNumber", MinLen(10))
v.Check(req.Age, "age", Min(18), Max(120))
if !v.HasErrors() {
t.Fatal("expected validation errors")
}
errors := v.Errors()
websiteErr := errors.ByField("website")
if len(websiteErr) != 1 {
t.Errorf("expected 1 website error, got %d", len(websiteErr))
}
phoneErr := errors.ByField("phoneNumber")
if len(phoneErr) != 0 {
t.Errorf("expected 0 phoneNumber errors (nil should be skipped), got %d", len(phoneErr))
}
ageErr := errors.ByField("age")
if len(ageErr) != 0 {
t.Errorf("expected 0 age errors (nil should be skipped), got %d", len(ageErr))
}
t.Logf("Optional field validation errors: %s", errors.Error())
}
func TestRealWorldExample(t *testing.T) {
// Simulate a user registration form
type Address struct {
City string
ZipCode string
}
type User struct {
Email string
Password string
Age int
Website *string
Address Address
}
user := User{
Email: "invalid-email",
Password: "123",
Age: 15,
Website: ref.Ref("not-a-url"),
Address: Address{
City: "",
ZipCode: "12345",
},
}
v := New()
// Validate user fields
v.Check(&user.Email, "email", Required(), Email())
v.Check(&user.Password, "password", Required(), MinLen(8))
v.Check(&user.Age, "age", Min(18), Max(120))
v.Check(user.Website, "website", URL())
// Validate nested address
v.CheckNested("address", func(av *Validator) {
av.Check(&user.Address.City, "city", Required())
av.Check(&user.Address.ZipCode, "zipCode", Pattern(`^\d{5}$`, "must be 5 digits"))
})
if !v.HasErrors() {
t.Fatal("expected validation errors")
}
errors := v.Errors()
expectedErrors := map[string]ErrorCode{
"email": ErrorCodeInvalidEmail,
"password": ErrorCodeTooShort,
"age": ErrorCodeOutOfRange,
"website": ErrorCodeInvalidURL,
"address.city": ErrorCodeRequired,
}
// Check that we have the expected errors
for field, expectedCode := range expectedErrors {
found := false
for _, err := range errors {
if err.Field == field && err.Code == expectedCode {
found = true
break
}
}
if !found {
t.Errorf("expected error for field '%s' with code '%s'", field, expectedCode)
}
}
// Print errors for debugging
t.Logf("Validation errors: %s", errors.Error())
}
func TestArrayValidation(t *testing.T) {
type Item struct {
Name string
Price int
}
items := []Item{
{Name: "", Price: -10},
{Name: "Valid", Price: 100},
{Name: "X", Price: 10},
}
v := New()
// Validate each item
for i, item := range items {
field := fmt.Sprintf("items[%d]", i)
v.CheckNested(field, func(iv *Validator) {
iv.Check(&item.Name, "name", Required(), MinLen(2))
iv.Check(&item.Price, "price", Min(0))
})
}
if !v.HasErrors() {
t.Fatal("expected validation errors")
}
errors := v.Errors()
// Check for specific field paths
expectedFields := []string{
"items[0].name",
"items[0].price",
"items[2].name",
}
for _, expectedField := range expectedFields {
found := false
for _, err := range errors {
if err.Field == expectedField {
found = true
break
}
}
if !found {
t.Errorf("expected error for field '%s'", expectedField)
}
}
t.Logf("Array validation errors: %s", errors.Error())
}
func TestDuplicateValidators(t *testing.T) {
t.Run("duplicate MinLen creates two errors", func(t *testing.T) {
v := New()
name := "abc"
v.Check(&name, "name", MinLen(5), MinLen(5))
if !v.HasErrors() {
t.Error("expected validation errors")
}
errors := v.Errors()
if len(errors) != 2 {
t.Errorf("expected 2 errors (one per MinLen), got %d", len(errors))
}
if errors[0].Message != "must be at least 5 characters" {
t.Errorf("unexpected first error: %s", errors[0].Message)
}
if errors[1].Message != "must be at least 5 characters" {
t.Errorf("unexpected second error: %s", errors[1].Message)
}
})
t.Run("duplicate Required creates two errors", func(t *testing.T) {
v := New()
name := ""
v.Check(&name, "name", Required(), Required())
errors := v.Errors()
if len(errors) != 2 {
t.Errorf("expected 2 errors, got %d", len(errors))
}
})
t.Run("duplicate Email creates two errors", func(t *testing.T) {
v := New()
email := "invalid"
v.Check(&email, "email", Email(), Email())
errors := v.Errors()
if len(errors) != 2 {
t.Errorf("expected 2 errors, got %d", len(errors))
}
})
t.Run("same validator with different parameters", func(t *testing.T) {
v := New()
name := "test"
v.Check(&name, "name", MinLen(5), MinLen(10))
errors := v.Errors()
if len(errors) != 2 {
t.Errorf("expected 2 errors, got %d", len(errors))
}
if errors[0].Message != "must be at least 5 characters" {
t.Errorf("unexpected first error: %s", errors[0].Message)
}
if errors[1].Message != "must be at least 10 characters" {
t.Errorf("unexpected second error: %s", errors[1].Message)
}
})
}
func TestStandardErrorPattern(t *testing.T) {
// Simulates a typical validation function
validateUser := func(email, password string) error {
v := New()
v.Check(&email, "email", Required(), Email())
v.Check(&password, "password", Required(), MinLen(8))
return v.Error()
}
t.Run("valid data returns nil", func(t *testing.T) {
err := validateUser("user@example.com", "password123")
if err != nil {
t.Errorf("expected nil, got: %v", err)
}
})
t.Run("invalid data returns ValidationErrors as error", func(t *testing.T) {
err := validateUser("", "123")
if err == nil {
t.Fatal("expected validation errors")
}
// Standard error handling
t.Logf("validation failed: %v", err)
// Can get detailed errors if needed
if validationErrs, ok := err.(ValidationErrors); ok {
for _, e := range validationErrs {
t.Logf(" - %s: %s (code: %s)", e.Field, e.Message, e.Code)
}
// Can use helper methods
emailErrs := validationErrs.ByField("email")
if len(emailErrs) != 1 {
t.Errorf("expected 1 email error, got %d", len(emailErrs))
}
passwordErrs := validationErrs.ByField("password")
if len(passwordErrs) != 1 {
t.Errorf("expected 1 password error, got %d", len(passwordErrs))
}
} else {
t.Error("expected ValidationErrors type")
}
})
}

View File

@@ -0,0 +1,109 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"fmt"
"reflect"
)
// MinItems validates that a slice or array has at least the specified minimum number of items.
func MinItems(min int) ValidatorFunc {
return func(value any) *ValidationError {
v := reflect.ValueOf(value)
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil
}
v = v.Elem()
}
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return newValidationError(ErrorCodeInvalidFormat, "value must be a slice or array")
}
if v.Len() < min {
return newValidationError(
ErrorCodeOutOfRange,
fmt.Sprintf("must contain at least %d items", min),
)
}
return nil
}
}
// MaxItems validates that a slice or array does not exceed the specified maximum number of items.
func MaxItems(max int) ValidatorFunc {
return func(value any) *ValidationError {
v := reflect.ValueOf(value)
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil
}
v = v.Elem()
}
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return newValidationError(ErrorCodeInvalidFormat, "value must be a slice or array")
}
if v.Len() > max {
return newValidationError(
ErrorCodeOutOfRange,
fmt.Sprintf("must contain at most %d items", max),
)
}
return nil
}
}
// UniqueItems validates that all items in a slice or array are unique.
func UniqueItems() ValidatorFunc {
return func(value any) *ValidationError {
v := reflect.ValueOf(value)
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil
}
v = v.Elem()
}
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return newValidationError(ErrorCodeInvalidFormat, "value must be a slice or array")
}
if v.Len() == 0 {
return nil
}
elemType := v.Type().Elem()
if !elemType.Comparable() {
return newValidationError(ErrorCodeInvalidFormat, "cannot validate uniqueness for non-comparable types")
}
seen := make(map[any]bool)
for i := 0; i < v.Len(); i++ {
item := v.Index(i).Interface()
if seen[item] {
return newValidationError(ErrorCodeInvalidFormat, "items must be unique")
}
seen[item] = true
}
return nil
}
}

View File

@@ -0,0 +1,220 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"testing"
)
func TestMinItems(t *testing.T) {
t.Run("valid slice", func(t *testing.T) {
items := []string{"a", "b", "c"}
err := MinItems(2)(&items)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("exact minimum", func(t *testing.T) {
items := []int{1, 2}
err := MinItems(2)(&items)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("too few items", func(t *testing.T) {
items := []string{"a"}
err := MinItems(2)(&items)
if err == nil {
t.Error("expected validation error")
}
if err.Code != ErrorCodeOutOfRange {
t.Errorf("expected error code %s, got %s", ErrorCodeOutOfRange, err.Code)
}
})
t.Run("nil slice", func(t *testing.T) {
var items *[]string
err := MinItems(2)(items)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
t.Run("non-slice value", func(t *testing.T) {
value := "not a slice"
err := MinItems(2)(&value)
if err == nil || err.Code != ErrorCodeInvalidFormat {
t.Error("expected invalid format error")
}
})
}
func TestMaxItems(t *testing.T) {
t.Run("valid slice", func(t *testing.T) {
items := []string{"a", "b"}
err := MaxItems(5)(&items)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("exact maximum", func(t *testing.T) {
items := []int{1, 2, 3}
err := MaxItems(3)(&items)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("too many items", func(t *testing.T) {
items := []string{"a", "b", "c", "d"}
err := MaxItems(2)(&items)
if err == nil {
t.Error("expected validation error")
}
if err.Code != ErrorCodeOutOfRange {
t.Errorf("expected error code %s, got %s", ErrorCodeOutOfRange, err.Code)
}
})
t.Run("nil slice", func(t *testing.T) {
var items *[]string
err := MaxItems(2)(items)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
}
func TestUniqueItems(t *testing.T) {
t.Run("unique items", func(t *testing.T) {
items := []string{"a", "b", "c"}
err := UniqueItems()(&items)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("duplicate items", func(t *testing.T) {
items := []string{"a", "b", "a"}
err := UniqueItems()(&items)
if err == nil {
t.Error("expected validation error")
}
if err.Code != ErrorCodeInvalidFormat {
t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, err.Code)
}
})
t.Run("unique integers", func(t *testing.T) {
items := []int{1, 2, 3}
err := UniqueItems()(&items)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("duplicate integers", func(t *testing.T) {
items := []int{1, 2, 1}
err := UniqueItems()(&items)
if err == nil {
t.Error("expected validation error")
}
})
t.Run("nil slice", func(t *testing.T) {
var items *[]string
err := UniqueItems()(items)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
t.Run("empty slice", func(t *testing.T) {
items := []string{}
err := UniqueItems()(&items)
if err != nil {
t.Errorf("expected no error for empty slice, got: %v", err)
}
})
t.Run("non-comparable type - slice of slices", func(t *testing.T) {
items := [][]int{{1, 2}, {3, 4}}
err := UniqueItems()(&items)
if err == nil {
t.Error("expected validation error for non-comparable type")
}
if err.Code != ErrorCodeInvalidFormat {
t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, err.Code)
}
if err.Message != "cannot validate uniqueness for non-comparable types" {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("non-comparable type - slice of maps", func(t *testing.T) {
items := []map[string]int{{"a": 1}, {"b": 2}}
err := UniqueItems()(&items)
if err == nil {
t.Error("expected validation error for non-comparable type")
}
if err.Code != ErrorCodeInvalidFormat {
t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, err.Code)
}
})
t.Run("non-comparable type - struct with slice field", func(t *testing.T) {
type NonComparable struct {
Items []int
}
items := []NonComparable{{Items: []int{1, 2}}, {Items: []int{3, 4}}}
err := UniqueItems()(&items)
if err == nil {
t.Error("expected validation error for non-comparable type")
}
if err.Code != ErrorCodeInvalidFormat {
t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, err.Code)
}
})
t.Run("comparable struct with unique values", func(t *testing.T) {
type ComparableStruct struct {
ID int
Name string
}
items := []ComparableStruct{{ID: 1, Name: "a"}, {ID: 2, Name: "b"}}
err := UniqueItems()(&items)
if err != nil {
t.Errorf("expected no error for comparable structs, got: %v", err)
}
})
t.Run("comparable struct with duplicate values", func(t *testing.T) {
type ComparableStruct struct {
ID int
Name string
}
items := []ComparableStruct{{ID: 1, Name: "a"}, {ID: 2, Name: "b"}, {ID: 1, Name: "a"}}
err := UniqueItems()(&items)
if err == nil {
t.Error("expected validation error for duplicate comparable structs")
}
if err.Code != ErrorCodeInvalidFormat {
t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, err.Code)
}
})
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"reflect"
"strings"
)
// Required validates that a field has a value.
// For strings, it also checks that the value is not empty or just whitespace.
// For slices, it checks that the slice is not empty.
func Required() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return newValidationError(ErrorCodeRequired, "field is required")
}
switch v := actualValue.(type) {
case string:
if strings.TrimSpace(v) == "" {
return newValidationError(ErrorCodeRequired, "field is required")
}
default:
rv := reflect.ValueOf(actualValue)
if rv.Kind() == reflect.Slice && rv.Len() == 0 {
return newValidationError(ErrorCodeRequired, "field is required")
}
}
return nil
}
}
// NotEmpty validates that a field is not empty.
// Similar to Required, but can be used independently.
func NotEmpty() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
switch v := actualValue.(type) {
case string:
if strings.TrimSpace(v) == "" {
return newValidationError(ErrorCodeRequired, "field cannot be empty")
}
default:
rv := reflect.ValueOf(actualValue)
if rv.Kind() == reflect.Slice && rv.Len() == 0 {
return newValidationError(ErrorCodeRequired, "field cannot be empty")
}
}
return nil
}
}

View File

@@ -0,0 +1,270 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"testing"
)
func TestOptionalByDefault(t *testing.T) {
t.Run("nil value skips validation by default", func(t *testing.T) {
v := New()
v.Check(nil, "field", MinLen(5))
if v.HasErrors() {
t.Errorf("expected no errors for nil (optional by default), got: %v", v.Errors())
}
})
t.Run("nil pointer skips validation by default", func(t *testing.T) {
v := New()
var str *string
v.Check(str, "field", MinLen(5))
if v.HasErrors() {
t.Errorf("expected no errors for nil pointer (optional by default), got: %v", v.Errors())
}
})
t.Run("valid value passes validation", func(t *testing.T) {
v := New()
str := "hello world"
v.Check(&str, "field", MinLen(5))
if v.HasErrors() {
t.Errorf("expected no errors, got: %v", v.Errors())
}
})
t.Run("invalid value fails validation", func(t *testing.T) {
v := New()
str := "hi"
v.Check(&str, "field", MinLen(5))
if !v.HasErrors() {
t.Error("expected validation error")
}
})
t.Run("multiple validators", func(t *testing.T) {
v := New()
str := "hello"
v.Check(&str, "field", MinLen(3), MaxLen(10))
if v.HasErrors() {
t.Errorf("expected no errors, got: %v", v.Errors())
}
})
t.Run("empty string is not nil and gets validated", func(t *testing.T) {
v := New()
str := ""
v.Check(&str, "field", MinLen(5))
if !v.HasErrors() {
t.Error("expected validation error for empty string")
}
})
t.Run("Required() validates nil values", func(t *testing.T) {
v := New()
var str *string
v.Check(str, "field", Required())
if !v.HasErrors() {
t.Error("expected validation error for nil with Required()")
}
})
}
func TestRequired(t *testing.T) {
t.Run("valid string", func(t *testing.T) {
str := "hello"
err := Required()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("empty string", func(t *testing.T) {
str := ""
err := Required()(&str)
if err == nil {
t.Error("expected validation error")
}
if err.Code != ErrorCodeRequired {
t.Errorf("expected error code %s, got %s", ErrorCodeRequired, err.Code)
}
})
t.Run("whitespace string", func(t *testing.T) {
str := " "
err := Required()(&str)
if err == nil {
t.Error("expected validation error for whitespace")
}
})
t.Run("nil string pointer", func(t *testing.T) {
var str *string
err := Required()(str)
if err == nil {
t.Error("expected validation error for nil pointer")
}
})
t.Run("valid string pointer", func(t *testing.T) {
str := "hello"
err := Required()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("nil interface", func(t *testing.T) {
err := Required()(nil)
if err == nil {
t.Error("expected validation error for nil")
}
})
t.Run("zero int", func(t *testing.T) {
num := 0
err := Required()(&num)
if err != nil {
t.Errorf("expected no error for zero int, got: %v", err)
}
})
t.Run("positive int", func(t *testing.T) {
num := 42
err := Required()(&num)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("nil int pointer", func(t *testing.T) {
var num *int
err := Required()(num)
if err == nil {
t.Error("expected validation error for nil int pointer")
}
})
t.Run("valid int pointer", func(t *testing.T) {
num := 42
err := Required()(&num)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("empty slice", func(t *testing.T) {
slice := []any{}
err := Required()(slice)
if err == nil {
t.Error("expected validation error for empty slice")
}
})
t.Run("non-empty slice", func(t *testing.T) {
slice := []any{1, 2, 3}
err := Required()(slice)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("empty string slice", func(t *testing.T) {
slice := []string{}
err := Required()(slice)
if err == nil {
t.Error("expected validation error for empty []string slice")
}
if err.Code != ErrorCodeRequired {
t.Errorf("expected error code %s, got %s", ErrorCodeRequired, err.Code)
}
})
t.Run("non-empty string slice", func(t *testing.T) {
slice := []string{"a", "b", "c"}
err := Required()(slice)
if err != nil {
t.Errorf("expected no error for non-empty []string, got: %v", err)
}
})
t.Run("empty int slice", func(t *testing.T) {
slice := []int{}
err := Required()(slice)
if err == nil {
t.Error("expected validation error for empty []int slice")
}
if err.Code != ErrorCodeRequired {
t.Errorf("expected error code %s, got %s", ErrorCodeRequired, err.Code)
}
})
t.Run("non-empty int slice", func(t *testing.T) {
slice := []int{1, 2, 3}
err := Required()(slice)
if err != nil {
t.Errorf("expected no error for non-empty []int, got: %v", err)
}
})
t.Run("empty custom type slice", func(t *testing.T) {
type CustomType struct {
ID int
}
slice := []CustomType{}
err := Required()(slice)
if err == nil {
t.Error("expected validation error for empty custom type slice")
}
if err.Code != ErrorCodeRequired {
t.Errorf("expected error code %s, got %s", ErrorCodeRequired, err.Code)
}
})
t.Run("non-empty custom type slice", func(t *testing.T) {
type CustomType struct {
ID int
}
slice := []CustomType{{ID: 1}, {ID: 2}}
err := Required()(slice)
if err != nil {
t.Errorf("expected no error for non-empty custom type slice, got: %v", err)
}
})
t.Run("empty pointer slice", func(t *testing.T) {
slice := []*string{}
err := Required()(slice)
if err == nil {
t.Error("expected validation error for empty []*string slice")
}
})
t.Run("non-empty pointer slice", func(t *testing.T) {
str1, str2 := "a", "b"
slice := []*string{&str1, &str2}
err := Required()(slice)
if err != nil {
t.Errorf("expected no error for non-empty []*string, got: %v", err)
}
})
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"reflect"
"time"
)
// EqualTo validates that a value equals another value using deep equality.
// Special handling for time.Time to compare instants rather than internal structure.
func EqualTo(other any) ValidatorFunc {
return func(value any) *ValidationError {
if !areEqual(value, other) {
return newValidationError(ErrorCodeInvalidFormat, "values must match")
}
return nil
}
}
// NotEqualTo validates that a value does not equal another value using deep equality.
// Special handling for time.Time to compare instants rather than internal structure.
func NotEqualTo(other any) ValidatorFunc {
return func(value any) *ValidationError {
if areEqual(value, other) {
return newValidationError(ErrorCodeInvalidFormat, "values must not match")
}
return nil
}
}
// areEqual compares two values for equality with special handling for time.Time.
func areEqual(a, b any) bool {
// Dereference both values
aVal, aIsNil := dereferenceValue(a)
bVal, bIsNil := dereferenceValue(b)
// If both are nil, they're equal
if aIsNil && bIsNil {
return true
}
// If only one is nil, they're not equal
if aIsNil || bIsNil {
return false
}
// Special handling for time.Time
aTime, aIsTime := aVal.(time.Time)
bTime, bIsTime := bVal.(time.Time)
if aIsTime && bIsTime {
// Use time.Time.Equal() which compares the instant, ignoring location and monotonic clock
return aTime.Equal(bTime)
}
// Fall back to reflect.DeepEqual for all other types
return reflect.DeepEqual(aVal, bVal)
}

View File

@@ -0,0 +1,182 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"testing"
"time"
)
func TestEqualTo(t *testing.T) {
t.Run("equal strings", func(t *testing.T) {
str1 := "password"
str2 := "password"
err := EqualTo(&str2)(&str1)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("different strings", func(t *testing.T) {
str1 := "password"
str2 := "different"
err := EqualTo(&str2)(&str1)
if err == nil {
t.Error("expected validation error")
}
})
t.Run("equal integers", func(t *testing.T) {
num1 := 42
num2 := 42
err := EqualTo(&num2)(&num1)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("different integers", func(t *testing.T) {
num1 := 42
num2 := 43
err := EqualTo(&num2)(&num1)
if err == nil {
t.Error("expected validation error")
}
})
}
func TestNotEqualTo(t *testing.T) {
t.Run("different strings", func(t *testing.T) {
str1 := "password"
str2 := "different"
err := NotEqualTo(&str2)(&str1)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("equal strings", func(t *testing.T) {
str1 := "password"
str2 := "password"
err := NotEqualTo(&str2)(&str1)
if err == nil {
t.Error("expected validation error")
}
})
}
func TestEqualTo_TimeComparison(t *testing.T) {
t.Run("same instant same location", func(t *testing.T) {
time1 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
time2 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
err := EqualTo(time2)(time1)
if err != nil {
t.Errorf("expected no error for same instant, got: %v", err)
}
})
t.Run("same instant different location", func(t *testing.T) {
// Create the same instant in different time zones
utcTime := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
est, _ := time.LoadLocation("America/New_York")
estTime := time.Date(2025, 11, 5, 7, 0, 0, 0, est) // 7am EST = 12pm UTC
err := EqualTo(utcTime)(estTime)
if err != nil {
t.Errorf("expected no error for same instant in different locations, got: %v", err)
}
})
t.Run("different instants same location", func(t *testing.T) {
time1 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
time2 := time.Date(2025, 11, 5, 13, 0, 0, 0, time.UTC)
err := EqualTo(time2)(time1)
if err == nil {
t.Error("expected validation error for different instants")
}
})
t.Run("pointer to time same instant", func(t *testing.T) {
time1 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
time2 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
err := EqualTo(&time2)(&time1)
if err != nil {
t.Errorf("expected no error for pointer to same instant, got: %v", err)
}
})
t.Run("nil time pointers", func(t *testing.T) {
var time1 *time.Time
var time2 *time.Time
err := EqualTo(time2)(time1)
if err != nil {
t.Errorf("expected no error for nil time pointers, got: %v", err)
}
})
t.Run("one nil one non-nil time pointer", func(t *testing.T) {
var time1 *time.Time
time2 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
err := EqualTo(&time2)(time1)
if err == nil {
t.Error("expected validation error for nil vs non-nil time")
}
})
t.Run("same instant with monotonic clock difference", func(t *testing.T) {
// Simulate times with different monotonic clock data
baseTime := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
time1 := baseTime
time.Sleep(1 * time.Millisecond) // Advances monotonic clock
time2 := baseTime
// Even though monotonic clocks differ, the instants are the same
err := EqualTo(time2)(time1)
if err != nil {
t.Errorf("expected no error despite monotonic clock difference, got: %v", err)
}
})
}
func TestNotEqualTo_TimeComparison(t *testing.T) {
t.Run("different instants", func(t *testing.T) {
time1 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
time2 := time.Date(2025, 11, 5, 13, 0, 0, 0, time.UTC)
err := NotEqualTo(time2)(time1)
if err != nil {
t.Errorf("expected no error for different instants, got: %v", err)
}
})
t.Run("same instant same location", func(t *testing.T) {
time1 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
time2 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
err := NotEqualTo(time2)(time1)
if err == nil {
t.Error("expected validation error for same instant")
}
})
t.Run("same instant different location", func(t *testing.T) {
utcTime := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
est, _ := time.LoadLocation("America/New_York")
estTime := time.Date(2025, 11, 5, 7, 0, 0, 0, est)
err := NotEqualTo(utcTime)(estTime)
if err == nil {
t.Error("expected validation error for same instant in different locations")
}
})
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
// Custom creates a custom validator with a specified error code, message, and validation function.
// The validation function should return true if the value is valid, false otherwise.
func Custom(code ErrorCode, message string, fn func(value any) bool) ValidatorFunc {
return func(value any) *ValidationError {
if !fn(value) {
return newValidationError(code, message)
}
return nil
}
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"testing"
)
func TestCustom(t *testing.T) {
validator := Custom(ErrorCodeCustom, "value must be positive", func(value any) bool {
if num, ok := value.(int); ok {
return num > 0
}
return false
})
tests := []struct {
name string
value any
wantError bool
}{
{"valid positive", 5, false},
{"invalid zero", 0, true},
{"invalid negative", -5, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validator(tt.value)
if (err != nil) != tt.wantError {
t.Errorf("Custom() error = %v, wantError %v", err, tt.wantError)
}
})
}
}

View File

@@ -0,0 +1,255 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"net/url"
"regexp"
"go.probo.inc/probo/pkg/gid"
)
var (
emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
gidRegex = regexp.MustCompile(`^gid://[a-zA-Z0-9\-_]+/[a-zA-Z0-9\-_]+/[a-zA-Z0-9\-_]+$`)
uuidRegex = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
domainRegex = regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`)
)
// Email validates that a string is a valid email address.
func Email() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidEmail, "value must be a string")
}
if str == "" {
return nil
}
if !emailRegex.MatchString(str) {
return newValidationError(ErrorCodeInvalidEmail, "invalid email address")
}
return nil
}
}
// URL validates that a string is a valid URL with http or https scheme.
func URL() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidURL, "value must be a string")
}
if str == "" {
return nil
}
parsedURL, err := url.Parse(str)
if err != nil {
return newValidationError(ErrorCodeInvalidURL, "invalid URL format")
}
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return newValidationError(ErrorCodeInvalidURL, "URL must use http or https scheme")
}
if parsedURL.Host == "" {
return newValidationError(ErrorCodeInvalidURL, "URL must have a host")
}
return nil
}
}
// HTTPUrl validates that a string is a valid HTTP URL (not HTTPS).
func HTTPUrl() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidURL, "value must be a string")
}
if str == "" {
return nil
}
parsedURL, err := url.Parse(str)
if err != nil {
return newValidationError(ErrorCodeInvalidURL, "invalid URL format")
}
if parsedURL.Scheme != "http" {
return newValidationError(ErrorCodeInvalidURL, "URL must use http scheme")
}
if parsedURL.Host == "" {
return newValidationError(ErrorCodeInvalidURL, "URL must have a host")
}
return nil
}
}
// HTTPSUrl validates that a string is a valid HTTPS URL (not HTTP).
func HTTPSUrl() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidURL, "value must be a string")
}
if str == "" {
return nil
}
parsedURL, err := url.Parse(str)
if err != nil {
return newValidationError(ErrorCodeInvalidURL, "invalid URL format")
}
if parsedURL.Scheme != "https" {
return newValidationError(ErrorCodeInvalidURL, "URL must use https scheme")
}
if parsedURL.Host == "" {
return newValidationError(ErrorCodeInvalidURL, "URL must have a host")
}
return nil
}
}
// UUID validates that a string is a valid UUID.
func UUID() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
}
if str == "" {
return nil
}
if !uuidRegex.MatchString(str) {
return newValidationError(ErrorCodeInvalidFormat, "invalid UUID format")
}
return nil
}
}
// GID validates that a string is a valid GID using gid.ParseGID.
// Optionally validates the entity type if provided.
//
// Example usage:
// - GID() validates any GID format
// - GID(100) validates GID with entity type 100
// - GID(100, 200) validates GID with entity type 100 or 200
func GID(entityTypes ...uint16) ValidatorFunc {
return func(value any) *ValidationError {
if value == nil {
return nil
}
var gidValue gid.GID
switch v := value.(type) {
case gid.GID:
gidValue = v
case *gid.GID:
if v == nil {
return nil
}
gidValue = *v
default:
return newValidationError(ErrorCodeInvalidGID, "value must be a GID")
}
if len(entityTypes) > 0 {
parsedEntityType := gidValue.EntityType()
valid := false
for _, expected := range entityTypes {
if parsedEntityType == expected {
valid = true
break
}
}
if !valid {
return newValidationError(ErrorCodeInvalidGID, "GID has invalid entity type")
}
}
return nil
}
}
// Domain validates that a string is a valid domain name.
func Domain() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
}
if str == "" {
return nil
}
if len(str) > 253 {
return newValidationError(ErrorCodeInvalidFormat, "domain name too long (max 253 characters)")
}
if !domainRegex.MatchString(str) {
return newValidationError(ErrorCodeInvalidFormat, "invalid domain name format")
}
return nil
}
}

View File

@@ -0,0 +1,432 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"strings"
"testing"
"go.probo.inc/probo/pkg/gid"
)
func TestEmail(t *testing.T) {
tests := []struct {
name string
value any
wantError bool
}{
{"valid email", "test@example.com", false},
{"valid email with plus", "test+tag@example.com", false},
{"invalid email no @", "testexample.com", true},
{"invalid email no domain", "test@", true},
{"invalid email no TLD", "test@example", true},
{"empty string", "", false}, // Empty is allowed, use Required() to enforce
{"nil pointer", (*string)(nil), false}, // Skip validation
{"non-string", 123, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := Email()(tt.value)
if (err != nil) != tt.wantError {
t.Errorf("Email() error = %v, wantError %v", err, tt.wantError)
}
if err != nil && err.Code != ErrorCodeInvalidEmail {
t.Errorf("Expected error code %s, got %s", ErrorCodeInvalidEmail, err.Code)
}
})
}
}
func TestURL(t *testing.T) {
tests := []struct {
name string
value any
wantError bool
}{
{"valid http URL", "http://example.com", false},
{"valid https URL", "https://example.com", false},
{"valid URL with path", "https://example.com/path", false},
{"invalid scheme", "ftp://example.com", true},
{"no scheme", "example.com", true},
{"no host", "https://", true},
{"empty string", "", false}, // Empty is allowed
{"nil pointer", (*string)(nil), false},
{"non-string", 123, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := URL()(tt.value)
if (err != nil) != tt.wantError {
t.Errorf("URL() error = %v, wantError %v", err, tt.wantError)
}
if err != nil && err.Code != ErrorCodeInvalidURL {
t.Errorf("Expected error code %s, got %s", ErrorCodeInvalidURL, err.Code)
}
})
}
}
func TestHTTPUrl(t *testing.T) {
t.Run("valid http URL", func(t *testing.T) {
str := "http://example.com"
err := HTTPUrl()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid http URL with path", func(t *testing.T) {
str := "http://example.com/path/to/resource"
err := HTTPUrl()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid http URL with query", func(t *testing.T) {
str := "http://example.com?foo=bar"
err := HTTPUrl()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("invalid - https scheme", func(t *testing.T) {
str := "https://example.com"
err := HTTPUrl()(&str)
if err == nil {
t.Error("expected validation error for https")
}
if err.Message != "URL must use http scheme" {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - no scheme", func(t *testing.T) {
str := "example.com"
err := HTTPUrl()(&str)
if err == nil {
t.Error("expected validation error for missing scheme")
}
})
t.Run("invalid - no host", func(t *testing.T) {
str := "http://"
err := HTTPUrl()(&str)
if err == nil {
t.Error("expected validation error for missing host")
}
})
t.Run("empty string", func(t *testing.T) {
str := ""
err := HTTPUrl()(&str)
if err != nil {
t.Errorf("expected no error for empty string, got: %v", err)
}
})
t.Run("nil pointer", func(t *testing.T) {
var str *string
err := HTTPUrl()(str)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
}
func TestHTTPSUrl(t *testing.T) {
t.Run("valid https URL", func(t *testing.T) {
str := "https://example.com"
err := HTTPSUrl()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid https URL with path", func(t *testing.T) {
str := "https://example.com/path/to/resource"
err := HTTPSUrl()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid https URL with query", func(t *testing.T) {
str := "https://api.example.com/v1/users?page=1"
err := HTTPSUrl()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("invalid - http scheme", func(t *testing.T) {
str := "http://example.com"
err := HTTPSUrl()(&str)
if err == nil {
t.Error("expected validation error for http")
}
if err.Message != "URL must use https scheme" {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - ftp scheme", func(t *testing.T) {
str := "ftp://example.com"
err := HTTPSUrl()(&str)
if err == nil {
t.Error("expected validation error for ftp")
}
})
t.Run("invalid - no scheme", func(t *testing.T) {
str := "example.com"
err := HTTPSUrl()(&str)
if err == nil {
t.Error("expected validation error for missing scheme")
}
})
t.Run("invalid - no host", func(t *testing.T) {
str := "https://"
err := HTTPSUrl()(&str)
if err == nil {
t.Error("expected validation error for missing host")
}
})
t.Run("empty string", func(t *testing.T) {
str := ""
err := HTTPSUrl()(&str)
if err != nil {
t.Errorf("expected no error for empty string, got: %v", err)
}
})
t.Run("nil pointer", func(t *testing.T) {
var str *string
err := HTTPSUrl()(str)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
}
func TestDomain(t *testing.T) {
t.Run("valid domain", func(t *testing.T) {
str := "example.com"
err := Domain()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid subdomain", func(t *testing.T) {
str := "api.example.com"
err := Domain()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid nested subdomain", func(t *testing.T) {
str := "api.v1.example.com"
err := Domain()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid domain with hyphens", func(t *testing.T) {
str := "my-api.example-site.com"
err := Domain()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("single word domain", func(t *testing.T) {
str := "localhost"
err := Domain()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("invalid - starts with hyphen", func(t *testing.T) {
str := "-example.com"
err := Domain()(&str)
if err == nil {
t.Error("expected validation error for domain starting with hyphen")
}
})
t.Run("invalid - ends with hyphen", func(t *testing.T) {
str := "example-.com"
err := Domain()(&str)
if err == nil {
t.Error("expected validation error for domain ending with hyphen")
}
})
t.Run("invalid - contains underscore", func(t *testing.T) {
str := "example_site.com"
err := Domain()(&str)
if err == nil {
t.Error("expected validation error for underscore")
}
})
t.Run("invalid - contains spaces", func(t *testing.T) {
str := "example site.com"
err := Domain()(&str)
if err == nil {
t.Error("expected validation error for spaces")
}
})
t.Run("invalid - empty label", func(t *testing.T) {
str := "example..com"
err := Domain()(&str)
if err == nil {
t.Error("expected validation error for empty label")
}
})
t.Run("invalid - too long", func(t *testing.T) {
str := strings.Repeat("a", 254)
err := Domain()(&str)
if err == nil {
t.Error("expected validation error for domain too long")
}
if err.Message != "domain name too long (max 253 characters)" {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("empty string", func(t *testing.T) {
str := ""
err := Domain()(&str)
if err != nil {
t.Errorf("expected no error for empty string, got: %v", err)
}
})
t.Run("nil pointer", func(t *testing.T) {
var str *string
err := Domain()(str)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
}
func TestGID(t *testing.T) {
// Create a valid GID for testing
tenantID := gid.TenantID([8]byte{1, 2, 3, 4, 5, 6, 7, 8})
validGID := gid.New(tenantID, 100)
t.Run("valid GID type - no entity type validation", func(t *testing.T) {
err := GID()(validGID)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid GID type - with matching entity type", func(t *testing.T) {
err := GID(100)(validGID)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid GID type - with multiple entity types", func(t *testing.T) {
err := GID(100, 200, 300)(validGID)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("invalid - wrong entity type", func(t *testing.T) {
err := GID(200)(validGID)
if err == nil {
t.Error("expected validation error for wrong entity type")
}
if err.Code != ErrorCodeInvalidGID {
t.Errorf("expected error code %s, got %s", ErrorCodeInvalidGID, err.Code)
}
if err.Message != "GID has invalid entity type" {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - wrong entity type with multiple options", func(t *testing.T) {
err := GID(200, 300)(validGID)
if err == nil {
t.Error("expected validation error for wrong entity type")
}
})
t.Run("valid - entity type matches one of multiple options", func(t *testing.T) {
err := GID(99, 100, 101)(validGID)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("nil GID pointer", func(t *testing.T) {
var gidPtr *gid.GID
err := GID()(gidPtr)
if err != nil {
t.Errorf("expected no error for nil GID pointer, got: %v", err)
}
})
t.Run("valid GID pointer", func(t *testing.T) {
err := GID()(&validGID)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid GID pointer with entity type validation", func(t *testing.T) {
err := GID(100)(&validGID)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("non-GID type", func(t *testing.T) {
err := GID()(123)
if err == nil {
t.Error("expected validation error for non-GID type")
}
if err.Message != "value must be a GID" {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("string type not supported", func(t *testing.T) {
err := GID()("some-string")
if err == nil {
t.Error("expected validation error for string type")
}
if err.Message != "value must be a GID" {
t.Errorf("unexpected error message: %s", err.Message)
}
})
}

View File

@@ -0,0 +1,203 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import "fmt"
// Min validates that a number is at least the specified minimum value.
func Min(min int) ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
var num int
switch v := actualValue.(type) {
case int:
num = v
case int32:
num = int(v)
case int64:
num = int(v)
default:
return newValidationError(ErrorCodeInvalidFormat, "value must be a number")
}
if num < min {
return newValidationError(
ErrorCodeOutOfRange,
fmt.Sprintf("must be at least %d", min),
)
}
return nil
}
}
// Max validates that a number does not exceed the specified maximum value.
func Max(max int) ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
var num int
switch v := actualValue.(type) {
case int:
num = v
case int32:
num = int(v)
case int64:
num = int(v)
default:
return newValidationError(ErrorCodeInvalidFormat, "value must be a number")
}
if num > max {
return newValidationError(
ErrorCodeOutOfRange,
fmt.Sprintf("must be at most %d", max),
)
}
return nil
}
}
// Range validates that a number is within the specified range (inclusive).
func Range(min, max int) ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
var num int
switch v := actualValue.(type) {
case int:
num = v
case int32:
num = int(v)
case int64:
num = int(v)
default:
return newValidationError(ErrorCodeInvalidFormat, "value must be a number")
}
if num < min || num > max {
return newValidationError(
ErrorCodeOutOfRange,
fmt.Sprintf("must be between %d and %d", min, max),
)
}
return nil
}
}
// MinFloat validates that a floating-point number is at least the specified minimum value.
func MinFloat(min float64) ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
var num float64
switch v := actualValue.(type) {
case float32:
num = float64(v)
case float64:
num = v
case int:
num = float64(v)
default:
return newValidationError(ErrorCodeInvalidFormat, "value must be a number")
}
if num < min {
return newValidationError(
ErrorCodeOutOfRange,
fmt.Sprintf("must be at least %g", min),
)
}
return nil
}
}
// MaxFloat validates that a floating-point number does not exceed the specified maximum value.
func MaxFloat(max float64) ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
var num float64
switch v := actualValue.(type) {
case float32:
num = float64(v)
case float64:
num = v
case int:
num = float64(v)
default:
return newValidationError(ErrorCodeInvalidFormat, "value must be a number")
}
if num > max {
return newValidationError(
ErrorCodeOutOfRange,
fmt.Sprintf("must be at most %g", max),
)
}
return nil
}
}
// RangeFloat validates that a floating-point number is within the specified range (inclusive).
func RangeFloat(min, max float64) ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
var num float64
switch v := actualValue.(type) {
case float32:
num = float64(v)
case float64:
num = v
case int:
num = float64(v)
default:
return newValidationError(ErrorCodeInvalidFormat, "value must be a number")
}
if num < min || num > max {
return newValidationError(
ErrorCodeOutOfRange,
fmt.Sprintf("must be between %g and %g", min, max),
)
}
return nil
}
}

View File

@@ -0,0 +1,95 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"testing"
"go.gearno.de/x/ref"
)
func TestMin(t *testing.T) {
tests := []struct {
name string
value any
min int
wantError bool
}{
{"valid int", 10, 5, false},
{"exact min", 5, 5, false},
{"below min", 3, 5, true},
{"valid int pointer", ref.Ref(10), 5, false},
{"nil pointer", (*int)(nil), 5, false}, // Skip validation
{"non-numeric", "test", 5, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := Min(tt.min)(tt.value)
if (err != nil) != tt.wantError {
t.Errorf("Min() error = %v, wantError %v", err, tt.wantError)
}
})
}
}
func TestMax(t *testing.T) {
tests := []struct {
name string
value any
max int
wantError bool
}{
{"valid int", 5, 10, false},
{"exact max", 10, 10, false},
{"above max", 15, 10, true},
{"valid int pointer", ref.Ref(5), 10, false},
{"nil pointer", (*int)(nil), 10, false}, // Skip validation
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := Max(tt.max)(tt.value)
if (err != nil) != tt.wantError {
t.Errorf("Max() error = %v, wantError %v", err, tt.wantError)
}
})
}
}
func TestRange(t *testing.T) {
tests := []struct {
name string
value any
min int
max int
wantError bool
}{
{"in range", 5, 1, 10, false},
{"at min", 1, 1, 10, false},
{"at max", 10, 1, 10, false},
{"below range", 0, 1, 10, true},
{"above range", 11, 1, 10, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := Range(tt.min, tt.max)(tt.value)
if (err != nil) != tt.wantError {
t.Errorf("Range() error = %v, wantError %v", err, tt.wantError)
}
})
}
}

View File

@@ -0,0 +1,173 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"fmt"
"regexp"
"strings"
)
var (
htmlTagRegex = regexp.MustCompile(`<[^>]*>`)
)
// NoHTML validates that a string does not contain HTML tags or angle brackets.
// It rejects:
// - HTML tags (e.g., <script>, <b>, <div>, etc.)
// - Angle brackets (< and >) even when not part of complete tags
//
// This helps prevent XSS attacks and ensures user input doesn't contain HTML markup.
// Combine with PrintableText() for comprehensive text field validation.
func NoHTML() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
}
if str == "" {
return nil
}
// Check for HTML tags first (more specific error message)
if htmlTagRegex.MatchString(str) {
return newValidationError(ErrorCodeInvalidFormat, "must not contain HTML tags")
}
// Check for angle brackets (even without complete tags)
if strings.ContainsAny(str, "<>") {
return newValidationError(ErrorCodeInvalidFormat, "must not contain angle brackets")
}
return nil
}
}
// PrintableText validates that a string contains only printable UTF-8 characters.
// It rejects:
// - Control characters (including null bytes, tabs, line breaks except space)
// - Unicode direction override characters (RLO, LRO, PDF, etc.)
// - Zero-width characters (ZWSP, ZWNJ, ZWJ, etc.)
// - Other invisible or formatting characters
// - Private use area characters
// - Replacement characters
//
// This validator does NOT check for HTML tags - use NoHTML() for that.
// This is ideal for validating titles, full names, display names, and similar text fields
// where only printable characters should be allowed.
func PrintableText() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
}
if str == "" {
return nil
}
// Check each rune for invisible or problematic characters
for i, r := range str {
// Allow normal space
if r == ' ' {
continue
}
// Reject control characters (0x00-0x1F and 0x7F-0x9F)
if r < 0x20 || (r >= 0x7F && r < 0xA0) {
return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains invalid control character at position %d", i))
}
// Reject Unicode direction override and formatting characters
// U+200E LEFT-TO-RIGHT MARK (LRM)
// U+200F RIGHT-TO-LEFT MARK (RLM)
// U+202A LEFT-TO-RIGHT EMBEDDING (LRE)
// U+202B RIGHT-TO-LEFT EMBEDDING (RLE)
// U+202C POP DIRECTIONAL FORMATTING (PDF)
// U+202D LEFT-TO-RIGHT OVERRIDE (LRO)
// U+202E RIGHT-TO-LEFT OVERRIDE (RLO)
// U+2066 LEFT-TO-RIGHT ISOLATE (LRI)
// U+2067 RIGHT-TO-LEFT ISOLATE (RLI)
// U+2068 FIRST STRONG ISOLATE (FSI)
// U+2069 POP DIRECTIONAL ISOLATE (PDI)
if r >= 0x200E && r <= 0x200F || r >= 0x202A && r <= 0x202E || r >= 0x2066 && r <= 0x2069 {
return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains bidirectional override character at position %d", i))
}
// Reject zero-width characters
// U+200B ZERO WIDTH SPACE (ZWSP)
// U+200C ZERO WIDTH NON-JOINER (ZWNJ)
// U+200D ZERO WIDTH JOINER (ZWJ)
// U+FEFF ZERO WIDTH NO-BREAK SPACE (BOM)
if r == 0x200B || r == 0x200C || r == 0x200D || r == 0xFEFF {
return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains zero-width character at position %d", i))
}
// Reject other format characters (Cf category)
// U+00AD SOFT HYPHEN
// U+2060 WORD JOINER
// U+180E MONGOLIAN VOWEL SEPARATOR (deprecated but still problematic)
if r == 0x00AD || r == 0x2060 || r == 0x180E {
return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains invisible formatting character at position %d", i))
}
// Reject private use area characters (often used for exploits)
// U+E000-U+F8FF Private Use Area
// U+F0000-U+FFFFD Supplementary Private Use Area-A
// U+100000-U+10FFFD Supplementary Private Use Area-B
if (r >= 0xE000 && r <= 0xF8FF) || (r >= 0xF0000 && r <= 0xFFFFD) || (r >= 0x100000 && r <= 0x10FFFD) {
return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains private use character at position %d", i))
}
// Reject replacement character (often indicates encoding issues)
if r == 0xFFFD {
return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains replacement character at position %d", i))
}
}
return nil
}
}
// SafeText validates that a string is non-empty, bounded, and contains only safe content.
// It combines NotEmpty, MaxLen, NoHTML, and PrintableText validators.
func SafeText(maxLen int) ValidatorFunc {
validators := []ValidatorFunc{
NotEmpty(),
MaxLen(maxLen),
NoHTML(),
PrintableText(),
}
return func(value any) *ValidationError {
for _, validator := range validators {
if err := validator(value); err != nil {
return err
}
}
return nil
}
}

View File

@@ -0,0 +1,755 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"strings"
"testing"
)
func TestNoHTML(t *testing.T) {
t.Run("valid text without HTML", func(t *testing.T) {
str := "This is a normal text"
err := NoHTML()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid text with special characters", func(t *testing.T) {
str := "Price: $10.99 - 20% off!"
err := NoHTML()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid UTF-8 text", func(t *testing.T) {
str := "José García 张伟"
err := NoHTML()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid text with emojis", func(t *testing.T) {
str := "Hello World 🌍"
err := NoHTML()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("invalid - script tag XSS", func(t *testing.T) {
str := "<script>alert('xss')</script>"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for script tag")
}
if !strings.Contains(err.Message, "HTML tags") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - simple bold tag", func(t *testing.T) {
str := "Hello <b>World</b>"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for bold tag")
}
if !strings.Contains(err.Message, "HTML tags") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - div tag", func(t *testing.T) {
str := "<div>Content</div>"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for div tag")
}
})
t.Run("invalid - self-closing tag", func(t *testing.T) {
str := "Line break<br/>here"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for self-closing tag")
}
})
t.Run("invalid - img tag", func(t *testing.T) {
str := `<img src="x" onerror="alert(1)">`
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for img tag")
}
})
t.Run("invalid - anchor tag", func(t *testing.T) {
str := `<a href="javascript:alert(1)">Click</a>`
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for anchor tag")
}
})
t.Run("invalid - less than symbol", func(t *testing.T) {
str := "5 < 10"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for angle bracket")
}
if !strings.Contains(err.Message, "angle brackets") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - greater than symbol", func(t *testing.T) {
str := "10 > 5"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for angle bracket")
}
})
t.Run("invalid - both angle brackets", func(t *testing.T) {
str := "5 < x > 10"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for angle brackets")
}
})
t.Run("invalid - malformed tag", func(t *testing.T) {
str := "text <incomplete"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for incomplete tag")
}
})
t.Run("invalid - encoded attempt", func(t *testing.T) {
str := "<ScRiPt>alert(1)</ScRiPt>"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for mixed case script tag")
}
})
t.Run("empty string", func(t *testing.T) {
str := ""
err := NoHTML()(&str)
if err != nil {
t.Errorf("expected no error for empty string, got: %v", err)
}
})
t.Run("nil pointer", func(t *testing.T) {
var str *string
err := NoHTML()(str)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
t.Run("not a string", func(t *testing.T) {
num := 123
err := NoHTML()(&num)
if err == nil {
t.Error("expected validation error for non-string")
}
if !strings.Contains(err.Message, "must be a string") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("combined with other validators", func(t *testing.T) {
v := New()
title := "Product Title 2024"
v.Check(&title, "title", Required(), NoHTML(), MinLen(3), MaxLen(100))
if v.HasErrors() {
t.Errorf("expected no errors, got: %v", v.Errors())
}
})
t.Run("combined with PrintableText", func(t *testing.T) {
v := New()
title := "José García-O'Brien"
v.Check(&title, "title", Required(), NoHTML(), PrintableText(), MinLen(3), MaxLen(100))
if v.HasErrors() {
t.Errorf("expected no errors, got: %v", v.Errors())
}
})
t.Run("combined validators catch XSS", func(t *testing.T) {
v := New()
malicious := "<script>alert('xss')</script>"
v.Check(&malicious, "content", Required(), NoHTML(), PrintableText())
if !v.HasErrors() {
t.Error("expected validation errors")
}
// Should have error from NoHTML
errors := v.Errors()
found := false
for _, err := range errors {
if strings.Contains(err.Message, "HTML tags") || strings.Contains(err.Message, "angle brackets") {
found = true
break
}
}
if !found {
t.Error("expected error about HTML tags or angle brackets")
}
})
t.Run("combined validators catch invisible chars and HTML", func(t *testing.T) {
v := New()
malicious := "<b>test\x00text</b>"
v.Check(&malicious, "content", NoHTML(), PrintableText())
if !v.HasErrors() {
t.Error("expected validation errors")
}
// Should have at least one error (NoHTML will catch it first)
if len(v.Errors()) < 1 {
t.Error("expected at least one validation error")
}
})
}
func TestPrintableText(t *testing.T) {
t.Run("valid UTF-8 text with accents", func(t *testing.T) {
str := "José García"
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error for valid UTF-8 name, got: %v", err)
}
})
t.Run("valid text with emojis", func(t *testing.T) {
str := "Hello World 🌍"
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error for emojis, got: %v", err)
}
})
t.Run("valid Chinese characters", func(t *testing.T) {
str := "张伟"
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error for Chinese characters, got: %v", err)
}
})
t.Run("valid Arabic text", func(t *testing.T) {
str := "محمد"
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error for Arabic text, got: %v", err)
}
})
t.Run("valid Cyrillic text", func(t *testing.T) {
str := "Александр"
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error for Cyrillic text, got: %v", err)
}
})
t.Run("valid text with apostrophe and hyphen", func(t *testing.T) {
str := "O'Brien-Smith"
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error for apostrophe and hyphen, got: %v", err)
}
})
t.Run("valid text with numbers", func(t *testing.T) {
str := "Product 2024"
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error for text with numbers, got: %v", err)
}
})
t.Run("valid text with punctuation", func(t *testing.T) {
str := "Hello, World! How are you?"
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error for punctuation, got: %v", err)
}
})
t.Run("valid text with angle brackets", func(t *testing.T) {
str := "5 < 10 > 3"
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error for angle brackets (HTML checking is separate), got: %v", err)
}
})
t.Run("invalid - RLO character", func(t *testing.T) {
str := "test\u202Eexe.txt"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for RLO character")
}
if !strings.Contains(err.Message, "bidirectional override") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - LRO character", func(t *testing.T) {
str := "test\u202Dtext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for LRO character")
}
})
t.Run("invalid - zero-width space", func(t *testing.T) {
str := "test\u200Btext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for zero-width space")
}
if !strings.Contains(err.Message, "zero-width") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - zero-width non-joiner", func(t *testing.T) {
str := "test\u200Ctext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for zero-width non-joiner")
}
})
t.Run("invalid - zero-width joiner", func(t *testing.T) {
str := "test\u200Dtext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for zero-width joiner")
}
})
t.Run("invalid - BOM character", func(t *testing.T) {
str := "\uFEFFtest"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for BOM character")
}
})
t.Run("invalid - null byte", func(t *testing.T) {
str := "test\x00text"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for null byte")
}
if !strings.Contains(err.Message, "control character") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - tab character", func(t *testing.T) {
str := "test\ttext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for tab character")
}
})
t.Run("invalid - newline character", func(t *testing.T) {
str := "test\ntext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for newline character")
}
})
t.Run("invalid - carriage return", func(t *testing.T) {
str := "test\rtext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for carriage return")
}
})
t.Run("invalid - soft hyphen", func(t *testing.T) {
str := "test\u00ADtext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for soft hyphen")
}
if !strings.Contains(err.Message, "invisible formatting") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - word joiner", func(t *testing.T) {
str := "test\u2060text"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for word joiner")
}
})
t.Run("invalid - private use area character", func(t *testing.T) {
str := "test\uE000text"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for private use area")
}
if !strings.Contains(err.Message, "private use") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - replacement character", func(t *testing.T) {
str := "test\uFFFDtext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for replacement character")
}
if !strings.Contains(err.Message, "replacement character") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - DEL control character", func(t *testing.T) {
str := "test\x7Ftext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for DEL control character")
}
})
t.Run("invalid - C1 control character", func(t *testing.T) {
str := "test\u0080text"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for C1 control character")
}
})
t.Run("invalid - LTR mark", func(t *testing.T) {
str := "test\u200Etext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for LTR mark")
}
})
t.Run("invalid - RTL mark", func(t *testing.T) {
str := "test\u200Ftext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for RTL mark")
}
})
t.Run("valid with pointer", func(t *testing.T) {
str := "Valid Name"
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("empty string", func(t *testing.T) {
str := ""
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error for empty string, got: %v", err)
}
})
t.Run("nil pointer", func(t *testing.T) {
var str *string
err := PrintableText()(str)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
t.Run("not a string", func(t *testing.T) {
num := 123
err := PrintableText()(&num)
if err == nil {
t.Error("expected validation error for non-string")
}
if !strings.Contains(err.Message, "must be a string") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("combined with other validators", func(t *testing.T) {
v := New()
title := "Product Title 2024"
v.Check(&title, "title", Required(), PrintableText(), MinLen(3), MaxLen(100))
if v.HasErrors() {
t.Errorf("expected no errors, got: %v", v.Errors())
}
})
t.Run("position reported correctly", func(t *testing.T) {
str := "abc\x00def"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error")
}
if !strings.Contains(err.Message, "position 3") {
t.Errorf("expected position 3 in error message, got: %s", err.Message)
}
})
t.Run("UTF-8 position counting", func(t *testing.T) {
// Test that position is counted correctly with UTF-8 characters
// The range loop in Go iterates by runes, so position will be rune index
str := "abc\x00"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error")
}
// The null byte is at rune position 3 (after 'a', 'b', 'c')
if !strings.Contains(err.Message, "position 3") {
t.Errorf("expected position 3 in error message, got: %s", err.Message)
}
})
}
func TestSafeText(t *testing.T) {
t.Run("valid text", func(t *testing.T) {
str := "Product Name 2024"
err := SafeText(100)(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid UTF-8 text", func(t *testing.T) {
str := "José García"
err := SafeText(50)(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid text with emoji", func(t *testing.T) {
str := "Hello World 🌍"
err := SafeText(50)(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid text with apostrophe and hyphen", func(t *testing.T) {
str := "O'Brien-Smith"
err := SafeText(50)(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("invalid - empty string", func(t *testing.T) {
str := ""
err := SafeText(100)(&str)
if err == nil {
t.Error("expected validation error for empty string")
}
if !strings.Contains(err.Message, "empty") && !strings.Contains(err.Message, "required") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - exceeds max length", func(t *testing.T) {
str := "This is a very long string that exceeds the maximum length"
err := SafeText(10)(&str)
if err == nil {
t.Error("expected validation error for exceeding max length")
}
if !strings.Contains(err.Message, "at most") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - contains HTML tags", func(t *testing.T) {
str := "Hello <b>World</b>"
err := SafeText(100)(&str)
if err == nil {
t.Error("expected validation error for HTML tags")
}
if !strings.Contains(err.Message, "HTML tags") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - contains script tag", func(t *testing.T) {
str := "<script>alert('xss')</script>"
err := SafeText(100)(&str)
if err == nil {
t.Error("expected validation error for script tag")
}
})
t.Run("invalid - contains angle brackets", func(t *testing.T) {
str := "5 < 10"
err := SafeText(100)(&str)
if err == nil {
t.Error("expected validation error for angle brackets")
}
if !strings.Contains(err.Message, "angle brackets") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - contains null byte", func(t *testing.T) {
str := "test\x00text"
err := SafeText(100)(&str)
if err == nil {
t.Error("expected validation error for null byte")
}
if !strings.Contains(err.Message, "control character") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - contains tab character", func(t *testing.T) {
str := "test\ttext"
err := SafeText(100)(&str)
if err == nil {
t.Error("expected validation error for tab character")
}
})
t.Run("invalid - contains newline", func(t *testing.T) {
str := "test\ntext"
err := SafeText(100)(&str)
if err == nil {
t.Error("expected validation error for newline")
}
})
t.Run("invalid - contains zero-width space", func(t *testing.T) {
str := "test\u200Btext"
err := SafeText(100)(&str)
if err == nil {
t.Error("expected validation error for zero-width space")
}
if !strings.Contains(err.Message, "zero-width") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - contains RLO character", func(t *testing.T) {
str := "test\u202Eexe.txt"
err := SafeText(100)(&str)
if err == nil {
t.Error("expected validation error for RLO character")
}
if !strings.Contains(err.Message, "bidirectional override") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - contains private use area character", func(t *testing.T) {
str := "test\uE000text"
err := SafeText(100)(&str)
if err == nil {
t.Error("expected validation error for private use area")
}
if !strings.Contains(err.Message, "private use") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("nil pointer", func(t *testing.T) {
var str *string
err := SafeText(100)(str)
if err != nil {
t.Errorf("expected no error for nil pointer, got: %v", err)
}
})
t.Run("not a string", func(t *testing.T) {
num := 123
err := SafeText(100)(&num)
if err == nil {
t.Error("expected validation error for non-string")
}
if !strings.Contains(err.Message, "must be a string") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("combined with validator struct", func(t *testing.T) {
v := New()
title := "Product Title 2024"
v.Check(&title, "title", SafeText(100))
if v.HasErrors() {
t.Errorf("expected no errors, got: %v", v.Errors())
}
})
t.Run("combined with validator struct - invalid", func(t *testing.T) {
v := New()
malicious := "<script>alert('xss')</script>"
v.Check(&malicious, "content", SafeText(100))
if !v.HasErrors() {
t.Error("expected validation errors")
}
errors := v.Errors()
found := false
for _, err := range errors {
if strings.Contains(err.Message, "HTML tags") || strings.Contains(err.Message, "angle brackets") {
found = true
break
}
}
if !found {
t.Error("expected error about HTML tags or angle brackets")
}
})
t.Run("edge case - exactly at max length", func(t *testing.T) {
str := "12345"
err := SafeText(5)(&str)
if err != nil {
t.Errorf("expected no error for string at max length, got: %v", err)
}
})
t.Run("edge case - one character over max length", func(t *testing.T) {
str := "123456"
err := SafeText(5)(&str)
if err == nil {
t.Error("expected validation error for string over max length")
}
})
}

View File

@@ -0,0 +1,275 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"fmt"
"reflect"
"regexp"
"strings"
)
var (
alphaNumericRegex = regexp.MustCompile(`^[a-zA-Z0-9]+$`)
slugRegex = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)
)
// MinLen validates that a string has at least the specified minimum length.
func MinLen(minLength int) ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
}
if len(str) < minLength {
return newValidationError(
ErrorCodeTooShort,
fmt.Sprintf("must be at least %d characters", minLength),
)
}
return nil
}
}
// MaxLen validates that a string does not exceed the specified maximum length.
func MaxLen(maxLength int) ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
}
if len(str) > maxLength {
return newValidationError(
ErrorCodeTooLong,
fmt.Sprintf("must be at most %d characters", maxLength),
)
}
return nil
}
}
// Pattern validates that a string matches the specified regular expression pattern.
func Pattern(pattern string, message string) ValidatorFunc {
regex := regexp.MustCompile(pattern)
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
}
if !regex.MatchString(str) {
if message == "" {
message = fmt.Sprintf("must match pattern: %s", pattern)
}
return newValidationError(ErrorCodeInvalidFormat, message)
}
return nil
}
}
// AlphaNumeric validates that a string contains only letters and numbers.
func AlphaNumeric() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
}
if str == "" {
return nil
}
if !alphaNumericRegex.MatchString(str) {
return newValidationError(ErrorCodeInvalidFormat, "must contain only letters and numbers")
}
return nil
}
}
// NoSpaces validates that a string does not contain any spaces.
func NoSpaces() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
}
if str == "" {
return nil
}
if strings.Contains(str, " ") {
return newValidationError(ErrorCodeInvalidFormat, "must not contain spaces")
}
return nil
}
}
// Slug validates that a string is a valid URL slug (lowercase letters, numbers, and hyphens).
func Slug() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
}
if str == "" {
return nil
}
if !slugRegex.MatchString(str) {
return newValidationError(ErrorCodeInvalidFormat, "must be a valid slug (lowercase letters, numbers, and hyphens)")
}
return nil
}
}
// OneOfSlice validates that a value is one of the allowed values in the slice.
// Accepts a slice of any type. Compares by value first, then by string representation.
func OneOfSlice[T any](allowed []T) ValidatorFunc {
// Build allowed map with string keys for flexible comparison
allowedMap := make(map[string]bool)
allowedStrings := make([]string, 0, len(allowed))
for _, v := range allowed {
str := fmt.Sprint(v)
allowedMap[str] = true
allowedStrings = append(allowedStrings, str)
}
return func(value any) *ValidationError {
// Handle nil values first
if value == nil {
return nil
}
// Dereference all pointer levels
actualValue := value
val := reflect.ValueOf(value)
for val.Kind() == reflect.Ptr {
if val.IsNil() {
return nil
}
val = val.Elem()
actualValue = val.Interface()
}
// First try exact match with DeepEqual
for _, allowedVal := range allowed {
if reflect.DeepEqual(actualValue, allowedVal) {
return nil
}
}
// Then try string comparison (for custom string types)
valueStr := fmt.Sprint(actualValue)
if allowedMap[valueStr] {
return nil
}
return newValidationError(
ErrorCodeInvalidEnum,
fmt.Sprintf("must be one of: %s", strings.Join(allowedStrings, ", ")),
)
}
}
// OneOf validates that a value is one of the allowed values.
// Accepts strings or types that implement fmt.Stringer as variadic arguments.
func OneOf(allowed ...any) ValidatorFunc {
allowedMap := make(map[string]bool)
allowedStrings := make([]string, 0, len(allowed))
for _, v := range allowed {
var str string
switch val := v.(type) {
case string:
str = val
case fmt.Stringer:
str = val.String()
default:
str = fmt.Sprint(val)
}
allowedMap[str] = true
allowedStrings = append(allowedStrings, str)
}
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
var str string
switch v := actualValue.(type) {
case string:
str = v
default:
if stringer, ok := actualValue.(fmt.Stringer); ok {
str = stringer.String()
} else {
return newValidationError(ErrorCodeInvalidEnum, "value must be a string or implement fmt.Stringer")
}
}
if !allowedMap[str] {
return newValidationError(
ErrorCodeInvalidEnum,
fmt.Sprintf("must be one of: %s", strings.Join(allowedStrings, ", ")),
)
}
return nil
}
}

View File

@@ -0,0 +1,303 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"testing"
"go.gearno.de/x/ref"
)
func TestMinLen(t *testing.T) {
tests := []struct {
name string
value any
minLen int
wantError bool
}{
{"valid string", "hello", 3, false},
{"exact length", "hello", 5, false},
{"too short", "hi", 5, true},
{"nil pointer", (*string)(nil), 5, false}, // Skip validation
{"valid pointer", ref.Ref("hello"), 3, false},
{"non-string", 123, 5, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := MinLen(tt.minLen)(tt.value)
if (err != nil) != tt.wantError {
t.Errorf("MinLen() error = %v, wantError %v", err, tt.wantError)
}
})
}
}
func TestMaxLen(t *testing.T) {
tests := []struct {
name string
value any
maxLen int
wantError bool
}{
{"valid string", "hello", 10, false},
{"exact length", "hello", 5, false},
{"too long", "hello world", 5, true},
{"nil pointer", (*string)(nil), 5, false}, // Skip validation
{"valid pointer", ref.Ref("hi"), 5, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := MaxLen(tt.maxLen)(tt.value)
if (err != nil) != tt.wantError {
t.Errorf("MaxLen() error = %v, wantError %v", err, tt.wantError)
}
})
}
}
func TestPattern(t *testing.T) {
tests := []struct {
name string
value any
pattern string
message string
wantError bool
}{
{"valid pattern", "abc123", `^[a-z0-9]+$`, "", false},
{"invalid pattern", "ABC123", `^[a-z0-9]+$`, "", true},
{"custom message", "invalid", `^valid$`, "must be 'valid'", true},
{"nil pointer", (*string)(nil), `^test$`, "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := Pattern(tt.pattern, tt.message)(tt.value)
if (err != nil) != tt.wantError {
t.Errorf("Pattern() error = %v, wantError %v", err, tt.wantError)
}
if err != nil && tt.message != "" && err.Message != tt.message {
t.Errorf("Expected message '%s', got '%s'", tt.message, err.Message)
}
})
}
}
func TestAlphaNumeric(t *testing.T) {
t.Run("valid alphanumeric", func(t *testing.T) {
str := "abc123"
err := AlphaNumeric()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("only letters", func(t *testing.T) {
str := "abcDEF"
err := AlphaNumeric()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("only numbers", func(t *testing.T) {
str := "123456"
err := AlphaNumeric()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("contains spaces", func(t *testing.T) {
str := "abc 123"
err := AlphaNumeric()(&str)
if err == nil {
t.Error("expected validation error")
}
})
t.Run("contains special characters", func(t *testing.T) {
str := "abc-123"
err := AlphaNumeric()(&str)
if err == nil {
t.Error("expected validation error")
}
})
t.Run("empty string", func(t *testing.T) {
str := ""
err := AlphaNumeric()(&str)
if err != nil {
t.Errorf("expected no error for empty string, got: %v", err)
}
})
t.Run("nil pointer", func(t *testing.T) {
var str *string
err := AlphaNumeric()(str)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
}
func TestNoSpaces(t *testing.T) {
t.Run("no spaces", func(t *testing.T) {
str := "hello-world"
err := NoSpaces()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("contains spaces", func(t *testing.T) {
str := "hello world"
err := NoSpaces()(&str)
if err == nil {
t.Error("expected validation error")
}
})
t.Run("multiple spaces", func(t *testing.T) {
str := "hello world test"
err := NoSpaces()(&str)
if err == nil {
t.Error("expected validation error")
}
})
t.Run("empty string", func(t *testing.T) {
str := ""
err := NoSpaces()(&str)
if err != nil {
t.Errorf("expected no error for empty string, got: %v", err)
}
})
t.Run("nil pointer", func(t *testing.T) {
var str *string
err := NoSpaces()(str)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
}
func TestSlug(t *testing.T) {
t.Run("valid slug", func(t *testing.T) {
str := "hello-world"
err := Slug()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid slug with numbers", func(t *testing.T) {
str := "hello-world-123"
err := Slug()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("single word", func(t *testing.T) {
str := "hello"
err := Slug()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("contains uppercase", func(t *testing.T) {
str := "Hello-World"
err := Slug()(&str)
if err == nil {
t.Error("expected validation error for uppercase")
}
})
t.Run("contains spaces", func(t *testing.T) {
str := "hello world"
err := Slug()(&str)
if err == nil {
t.Error("expected validation error for spaces")
}
})
t.Run("contains underscores", func(t *testing.T) {
str := "hello_world"
err := Slug()(&str)
if err == nil {
t.Error("expected validation error for underscores")
}
})
t.Run("starts with hyphen", func(t *testing.T) {
str := "-hello"
err := Slug()(&str)
if err == nil {
t.Error("expected validation error for leading hyphen")
}
})
t.Run("ends with hyphen", func(t *testing.T) {
str := "hello-"
err := Slug()(&str)
if err == nil {
t.Error("expected validation error for trailing hyphen")
}
})
t.Run("empty string", func(t *testing.T) {
str := ""
err := Slug()(&str)
if err != nil {
t.Errorf("expected no error for empty string, got: %v", err)
}
})
t.Run("nil pointer", func(t *testing.T) {
var str *string
err := Slug()(str)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
}
func TestOneOf(t *testing.T) {
tests := []struct {
name string
value any
allowed []string
wantError bool
}{
{"valid value", "apple", []string{"apple", "banana", "orange"}, false},
{"invalid value", "grape", []string{"apple", "banana", "orange"}, true},
{"nil pointer", (*string)(nil), []string{"apple"}, false},
{"empty string", "", []string{"apple", ""}, false},
{"non-string", 123, []string{"apple"}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := OneOfSlice(tt.allowed)(tt.value)
if (err != nil) != tt.wantError {
t.Errorf("OneOfSlice() error = %v, wantError %v", err, tt.wantError)
}
})
}
}

Some files were not shown because too many files have changed in this diff Show More