/**
* This module provides code a set of schemas for params passed to
* the nunjuck templates in `./src/views`
*/
import * as v from 'valibot'
import { MiddlewareError } from '../utils/errors.js'
export const EmptyParams = v.object({})
export const ErrorPageParams = v.object({
err: v.instance(MiddlewareError),
env: v.string(),
supportEmail: v.pipe(v.string(), v.email()),
uptime: v.optional(v.string()),
downtime: v.optional(v.string())
})
export const NonEmptyString = v.pipe(v.string(), v.nonEmpty())
export const Base = v.object({
// serviceName: NonEmptyString,
// pageTitle: NonEmptyString,
pageName: v.optional(NonEmptyString)
})
export const StartPage = v.object({
...Base.entries
})
export const PaginationItem = v.variant('type', [
v.strictObject({
type: v.literal('number'),
number: v.pipe(v.number(), v.integer()),
href: v.string(),
current: v.boolean()
}),
v.strictObject({
type: v.literal('ellipsis'),
ellipsis: v.literal(true),
href: v.string()
})
])
export const PaginationParams = v.optional(v.strictObject({
previous: v.optional(v.strictObject({
href: v.string()
})),
next: v.optional(v.strictObject({
href: v.string()
})),
items: v.array(PaginationItem)
}))
export const dataRangeParams = v.object({
minRow: v.pipe(v.number(), v.integer(), v.minValue(0)),
maxRow: v.pipe(v.number(), v.integer(), v.minValue(0)),
totalRows: v.pipe(v.number(), v.integer(), v.minValue(0)),
maxPageNumber: v.pipe(v.number(), v.integer(), v.minValue(0)),
pageLength: v.pipe(v.number(), v.integer(), v.minValue(1)),
offset: v.pipe(v.number(), v.integer(), v.minValue(0))
})
export const errorSummaryParams = v.strictObject({
heading: v.optional(v.string()),
items: v.array(v.strictObject({
html: v.string(),
href: v.optional(v.string())
}))
})
export const tableParams = v.strictObject({
columns: v.array(NonEmptyString),
rows: v.array(v.strictObject({
columns: v.objectWithRest(
{},
v.strictObject({
error: v.optional(v.object({
message: v.string()
})),
value: v.nullish(v.string()),
html: v.optional(v.string()),
classes: v.optional(v.string())
})
)
})),
fields: v.array(NonEmptyString)
})
/**
* The values of this enum should match values of the 'status' column
* in the query in `fetchLpaOverview` middleware
*/
export const datasetStatusEnum = {
Live: 'Live',
'Needs fixing': 'Needs fixing',
Warning: 'Warning',
Error: 'Error',
'Not submitted': 'Not submitted'
}
export const DeadlineNoticeField = v.strictObject({
type: v.union([
v.literal('due'),
v.literal('overdue')
]),
deadline: v.string()
})
const IssueSpecification = v.optional(v.strictObject({
datasetField: NonEmptyString,
field: NonEmptyString,
description: v.optional(NonEmptyString),
dataset: v.optional(NonEmptyString),
guidance: v.optional(NonEmptyString)
}))
const OrgField = v.strictObject({ name: NonEmptyString, organisation: NonEmptyString, statistical_geography: v.optional(v.string()), entity: v.optional(v.integer()) })
const DatasetNameField = v.strictObject({ name: NonEmptyString, dataset: NonEmptyString, collection: NonEmptyString })
const DatasetItem = v.strictObject({
endpointCount: v.optional(v.number()),
status: v.enum(datasetStatusEnum),
dataset: NonEmptyString,
issueCount: v.optional(v.number()),
error: v.optional(v.nullable(NonEmptyString)),
issue: v.optional(NonEmptyString),
entityCount: v.optional(v.number()),
project: v.optional(v.string()),
// synthetic entry, represents a user friendly count (e.g. count missing value in a column as 1 issue)
numIssues: v.optional(v.number()),
notice: v.optional(DeadlineNoticeField),
endpointErrorCount: v.optional(v.number())
})
export const OrgOverviewPage = v.strictObject({
organisation: OrgField,
datasets: v.object({
statutory: v.optional(v.array(DatasetItem)),
other: v.optional(v.array(DatasetItem))
}),
totalDatasets: v.integer(),
datasetsWithEndpoints: v.integer(),
datasetsWithIssues: v.integer(),
datasetsWithErrors: v.integer(),
isODPMember: v.boolean()
})
export const OrgFindPage = v.strictObject({
alphabetisedOrgs: v.record(NonEmptyString, v.array(OrgField))
})
export const OrgGetStarted = v.strictObject({
organisation: OrgField,
dataset: DatasetNameField
})
export const OrgDatasetOverview = v.strictObject({
organisation: OrgField,
dataset: DatasetNameField,
taskCount: v.integer(),
stats: v.strictObject({
numberOfRecords: v.integer(),
endpoints: v.array(v.strictObject({
name: v.string(),
documentation_url: v.nullable(v.optional(v.string())),
endpoint_url: v.string(),
endpoint: NonEmptyString,
lastAccessed: v.string(),
lastUpdated: v.nullable(v.string()),
entryDate: v.optional(v.nullable(v.string())),
error: v.optional(v.strictObject({
code: v.integer(),
exception: v.string()
}))
}))
}),
notice: v.optional(DeadlineNoticeField)
})
export const OrgDataView = v.strictObject({
organisation: OrgField,
dataset: DatasetNameField,
taskCount: v.integer(),
tableParams,
pagination: PaginationParams,
dataRange: dataRangeParams
})
export const OrgDatasetTaskList = v.strictObject({
taskList: v.array(v.strictObject({
title: v.strictObject({ text: NonEmptyString }),
href: v.url(),
status: v.strictObject({
tag: v.strictObject({
classes: NonEmptyString,
text: NonEmptyString
})
})
})),
organisation: OrgField,
dataset: v.strictObject({
dataset: v.optional(NonEmptyString),
name: NonEmptyString,
collection: NonEmptyString
})
})
export const OrgEndpointError = v.strictObject({
organisation: OrgField,
dataset: DatasetNameField,
errorData: v.strictObject({
endpoint_url: v.url(),
http_status: v.optional(v.integer()),
latest_log_entry_date: v.isoDateTime(),
latest_200_date: v.optional(v.isoDateTime())
})
})
const MapGeometry = v.union([
v.string(),
v.number(),
v.object({
type: v.string(),
reference: NonEmptyString,
geo: v.nullable(v.string())
})
])
const MapGeometries = v.array(MapGeometry)
export const OrgIssueTable = v.strictObject({
organisation: OrgField,
dataset: DatasetNameField,
errorSummary: errorSummaryParams,
issueType: v.string(),
issueSpecification: IssueSpecification,
tableParams,
pagination: PaginationParams,
dataRange: dataRangeParams,
geometries: v.optional(MapGeometries)
})
export const OrgIssueDetails = v.strictObject({
organisation: OrgField,
dataset: DatasetNameField,
errorSummary: errorSummaryParams,
issueType: NonEmptyString,
issueField: NonEmptyString,
entry: v.strictObject({
title: NonEmptyString,
fields: v.array(v.strictObject({
key: v.strictObject({ text: NonEmptyString }),
value: v.strictObject({ html: v.string(), originalValue: v.optional(v.string()) }),
classes: v.string()
})),
geometries: v.optional(MapGeometries)
}),
pagination: PaginationParams,
pageNumber: v.pipe(v.number(), v.integer()),
dataRange: dataRangeParams,
issueSpecification: IssueSpecification
})
export const CheckAnswers = v.strictObject({
values: v.strictObject({
lpa: NonEmptyString,
name: NonEmptyString,
email: v.pipe(v.string(), v.email()),
dataset: NonEmptyString,
'endpoint-url': v.url(),
'documentation-url': v.url(),
hasLicence: NonEmptyString,
errors: v.optional(v.array(v.strictObject({
text: NonEmptyString
})))
})
})
export const ChooseDataset = v.strictObject({
errors: v.strictObject({
dataset: v.optional(v.strictObject({
type: v.enum({
required: 'required'
})
}))
})
})
export const DatasetDetails = v.strictObject({
organisation: OrgField,
dataset: DatasetNameField,
values: v.strictObject({
dataset: NonEmptyString
}),
errors: v.record(NonEmptyString, v.strictObject({
type: NonEmptyString
}))
})
const SubmitEndpointConfirmation = v.strictObject({
values: v.object({
dataset: NonEmptyString,
email: NonEmptyString
})
})
/**
* This acts as a registry of template -> schema for convenience.
*/
export const templateSchema = new Map([
['dataset-details.html', DatasetDetails],
['check-answers.html', CheckAnswers],
['choose-dataset.html', ChooseDataset],
['lpa-details.html', v.any()],
['submit/confirmation.html', SubmitEndpointConfirmation],
['organisations/overview.html', OrgOverviewPage],
['organisations/find.html', OrgFindPage],
['organisations/get-started.html', OrgGetStarted],
['organisations/dataset-overview.html', OrgDatasetOverview],
['organisations/dataview.html', OrgDataView],
['organisations/datasetTaskList.html', OrgDatasetTaskList],
['organisations/http-error.html', OrgEndpointError],
['organisations/issueTable.html', OrgIssueTable],
['organisations/issueDetails.html', OrgIssueDetails],
['errorPages/error.njk', ErrorPageParams],
['privacy-notice.html', EmptyParams],
['landing.html', EmptyParams],
['cookies.html', EmptyParams],
['accessibility.html', EmptyParams]
])