Skip to content

Commit f258608

Browse files
refactor(validation): extract document normalization service
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent e294cdc commit f258608

1 file changed

Lines changed: 291 additions & 0 deletions

File tree

src/services/validationDocument.ts

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 LibreSign contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { FILE_STATUS, SIGN_REQUEST_STATUS } from '../constants.js'
7+
import type {
8+
SignerDetailRecord,
9+
ValidatedChildFileRecord,
10+
ValidationFileRecord,
11+
} from '../types/index'
12+
13+
type ValidationStatus = ValidationFileRecord['status']
14+
type UnknownRecord = Record<string, unknown>
15+
16+
export type ValidationStatusInfo = {
17+
id?: number
18+
label?: string
19+
}
20+
21+
export const MODIFICATION_UNMODIFIED = 1
22+
export const MODIFICATION_ALLOWED = 2
23+
export const MODIFICATION_VIOLATION = 3
24+
25+
export type ModificationValidationStatus =
26+
typeof MODIFICATION_UNMODIFIED
27+
| typeof MODIFICATION_ALLOWED
28+
| typeof MODIFICATION_VIOLATION
29+
30+
export type ValidationModificationInfo = {
31+
status?: ModificationValidationStatus
32+
valid?: boolean
33+
}
34+
35+
type ValidationMetadataDimension = {
36+
w: number
37+
h: number
38+
}
39+
40+
export type ValidationDocumentState = ValidationFileRecord & {
41+
signers: SignerDetailRecord[]
42+
metadata: NonNullable<ValidationFileRecord['metadata']>
43+
settings: NonNullable<ValidationFileRecord['settings']>
44+
}
45+
46+
export type LoadedValidationEnvelopeDocumentState = ValidationDocumentState & {
47+
nodeType: 'envelope'
48+
}
49+
50+
export type LoadedValidationFileDocumentState = ValidationDocumentState & {
51+
nodeType: 'file'
52+
}
53+
54+
function isRecord(value: unknown): value is UnknownRecord {
55+
return typeof value === 'object' && value !== null
56+
}
57+
58+
function hasOwn(record: UnknownRecord, key: string): boolean {
59+
return Object.prototype.hasOwnProperty.call(record, key)
60+
}
61+
62+
function isOptionalField(record: UnknownRecord, key: string, guard: (value: unknown) => boolean): boolean {
63+
return !hasOwn(record, key) || guard(record[key])
64+
}
65+
66+
function toNumber(value: unknown): number | null {
67+
return typeof value === 'number' && Number.isFinite(value) ? value : null
68+
}
69+
70+
function isString(value: unknown): value is string {
71+
return typeof value === 'string'
72+
}
73+
74+
function isNullableString(value: unknown): value is string | null {
75+
return value === null || typeof value === 'string'
76+
}
77+
78+
function isValidationStatus(value: unknown): value is ValidationStatus {
79+
const normalizedValue = toNumber(value)
80+
return normalizedValue === FILE_STATUS.DRAFT
81+
|| normalizedValue === FILE_STATUS.ABLE_TO_SIGN
82+
|| normalizedValue === FILE_STATUS.PARTIAL_SIGNED
83+
|| normalizedValue === FILE_STATUS.SIGNED
84+
|| normalizedValue === FILE_STATUS.DELETED
85+
}
86+
87+
function isSignerStatus(value: unknown): value is SignerDetailRecord['status'] {
88+
const normalizedValue = toNumber(value)
89+
return normalizedValue === SIGN_REQUEST_STATUS.DRAFT
90+
|| normalizedValue === SIGN_REQUEST_STATUS.ABLE_TO_SIGN
91+
|| normalizedValue === SIGN_REQUEST_STATUS.SIGNED
92+
}
93+
94+
function isValidationStatusInfo(value: unknown): value is ValidationStatusInfo {
95+
if (!isRecord(value)) {
96+
return false
97+
}
98+
99+
return isOptionalField(value, 'id', fieldValue => typeof fieldValue === 'number')
100+
&& isOptionalField(value, 'label', isString)
101+
}
102+
103+
function isModificationValidationStatus(value: unknown): value is ModificationValidationStatus {
104+
return value === MODIFICATION_UNMODIFIED
105+
|| value === MODIFICATION_ALLOWED
106+
|| value === MODIFICATION_VIOLATION
107+
}
108+
109+
function isValidationModificationInfo(value: unknown): value is ValidationModificationInfo {
110+
if (!isRecord(value)) {
111+
return false
112+
}
113+
114+
return isOptionalField(value, 'status', isModificationValidationStatus)
115+
&& isOptionalField(value, 'valid', fieldValue => typeof fieldValue === 'boolean')
116+
}
117+
118+
function isValidationMetadataDimension(value: unknown): value is ValidationMetadataDimension {
119+
if (!isRecord(value)) {
120+
return false
121+
}
122+
123+
return typeof value.w === 'number' && Number.isFinite(value.w)
124+
&& typeof value.h === 'number' && Number.isFinite(value.h)
125+
}
126+
127+
function isRequestedBy(value: unknown): value is ValidationFileRecord['requested_by'] {
128+
if (!isRecord(value)) {
129+
return false
130+
}
131+
return isString(value.userId) && isString(value.displayName)
132+
}
133+
134+
function isValidationMetadata(value: unknown): value is NonNullable<ValidationFileRecord['metadata']> {
135+
if (!isRecord(value)) {
136+
return false
137+
}
138+
139+
if (!isString(value.extension) || typeof value.p !== 'number') {
140+
return false
141+
}
142+
143+
return isOptionalField(value, 'd', fieldValue => Array.isArray(fieldValue) && fieldValue.every(isValidationMetadataDimension))
144+
&& isOptionalField(value, 'original_file_deleted', fieldValue => typeof fieldValue === 'boolean')
145+
&& isOptionalField(value, 'pdfVersion', isString)
146+
&& isOptionalField(value, 'status_changed_at', isString)
147+
}
148+
149+
function isValidationSettings(value: unknown): value is NonNullable<ValidationFileRecord['settings']> {
150+
if (!isRecord(value)) {
151+
return false
152+
}
153+
return typeof value.canSign === 'boolean'
154+
&& typeof value.canRequestSign === 'boolean'
155+
&& typeof value.phoneNumber === 'string'
156+
&& typeof value.hasSignatureFile === 'boolean'
157+
&& typeof value.needIdentificationDocuments === 'boolean'
158+
&& typeof value.identificationDocumentsWaitingApproval === 'boolean'
159+
&& isOptionalField(value, 'isApprover', fieldValue => typeof fieldValue === 'boolean')
160+
}
161+
162+
function isSignerDetailRecord(value: unknown): value is SignerDetailRecord {
163+
if (!isRecord(value)) {
164+
return false
165+
}
166+
167+
return typeof value.signRequestId === 'number'
168+
&& isString(value.displayName)
169+
&& isString(value.email)
170+
&& isNullableString(value.signed)
171+
&& isSignerStatus(value.status)
172+
&& isString(value.statusText)
173+
&& isNullableString(value.description)
174+
&& isString(value.request_sign_date)
175+
&& typeof value.me === 'boolean'
176+
&& Array.isArray(value.visibleElements)
177+
&& isOptionalField(value, 'signature_validation', isValidationStatusInfo)
178+
&& isOptionalField(value, 'certificate_validation', isValidationStatusInfo)
179+
&& isOptionalField(value, 'modification_validation', isValidationModificationInfo)
180+
&& isOptionalField(value, 'crl_validation', isString)
181+
&& isOptionalField(value, 'isLibreSignRootCA', fieldValue => typeof fieldValue === 'boolean')
182+
}
183+
184+
function isValidatedChildFileRecord(value: unknown): value is ValidatedChildFileRecord {
185+
if (!isRecord(value)) {
186+
return false
187+
}
188+
189+
return typeof value.id === 'number'
190+
&& isString(value.uuid)
191+
&& isString(value.name)
192+
&& isValidationStatus(value.status)
193+
&& isString(value.statusText)
194+
&& typeof value.nodeId === 'number'
195+
&& typeof value.size === 'number'
196+
&& Array.isArray(value.signers)
197+
&& isString(value.file)
198+
&& isValidationMetadata(value.metadata)
199+
}
200+
201+
function isValidationDocumentRecord(data: unknown): data is ValidationFileRecord {
202+
if (!isRecord(data)) {
203+
return false
204+
}
205+
if (
206+
typeof data.id !== 'number'
207+
|| !isString(data.uuid)
208+
|| !isString(data.name)
209+
|| !isValidationStatus(data.status)
210+
|| !isString(data.statusText)
211+
|| typeof data.nodeId !== 'number'
212+
|| (data.nodeType !== 'file' && data.nodeType !== 'envelope')
213+
|| typeof data.signatureFlow !== 'number'
214+
|| typeof data.docmdpLevel !== 'number'
215+
|| typeof data.filesCount !== 'number'
216+
|| !Array.isArray(data.files)
217+
|| typeof data.totalPages !== 'number'
218+
|| typeof data.size !== 'number'
219+
|| !isString(data.pdfVersion)
220+
|| !isString(data.created_at)
221+
|| !isRequestedBy(data.requested_by)
222+
) {
223+
return false
224+
}
225+
226+
if (!data.files.every(isValidatedChildFileRecord)) {
227+
return false
228+
}
229+
230+
if (hasOwn(data, 'signers') && (!Array.isArray(data.signers) || !data.signers.every(isSignerDetailRecord))) {
231+
return false
232+
}
233+
234+
if (hasOwn(data, 'metadata') && !isValidationMetadata(data.metadata)) {
235+
return false
236+
}
237+
238+
if (hasOwn(data, 'settings') && !isValidationSettings(data.settings)) {
239+
return false
240+
}
241+
242+
return true
243+
}
244+
245+
const DEFAULT_VALIDATION_METADATA: NonNullable<ValidationFileRecord['metadata']> = {
246+
extension: 'pdf',
247+
p: 0,
248+
}
249+
250+
const DEFAULT_VALIDATION_SETTINGS: NonNullable<ValidationFileRecord['settings']> = {
251+
canSign: false,
252+
canRequestSign: false,
253+
phoneNumber: '',
254+
hasSignatureFile: false,
255+
needIdentificationDocuments: false,
256+
identificationDocumentsWaitingApproval: false,
257+
}
258+
259+
export function toValidationDocument(data: unknown): ValidationDocumentState | null {
260+
if (!isValidationDocumentRecord(data)) {
261+
return null
262+
}
263+
264+
const metadata = isValidationMetadata(data.metadata)
265+
? data.metadata
266+
: {
267+
...DEFAULT_VALIDATION_METADATA,
268+
p: data.totalPages,
269+
}
270+
271+
const settings = isValidationSettings(data.settings)
272+
? data.settings
273+
: DEFAULT_VALIDATION_SETTINGS
274+
275+
const signers = Array.isArray(data.signers) ? data.signers : []
276+
277+
return {
278+
...data,
279+
metadata,
280+
settings,
281+
signers,
282+
}
283+
}
284+
285+
export function isLoadedValidationEnvelopeDocument(document: ValidationDocumentState | null): document is LoadedValidationEnvelopeDocumentState {
286+
return document?.nodeType === 'envelope'
287+
}
288+
289+
export function isLoadedValidationFileDocument(document: ValidationDocumentState | null): document is LoadedValidationFileDocumentState {
290+
return document?.nodeType === 'file'
291+
}

0 commit comments

Comments
 (0)