hvp-shared 7.6.0 → 7.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/constants/document.enums.d.ts +113 -0
- package/dist/constants/document.enums.js +175 -0
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.js +1 -0
- package/dist/contracts/document/index.d.ts +6 -0
- package/dist/contracts/document/index.js +22 -0
- package/dist/contracts/document/requests.d.ts +184 -0
- package/dist/contracts/document/requests.js +10 -0
- package/dist/contracts/document/responses.d.ts +129 -0
- package/dist/contracts/document/responses.js +9 -0
- package/dist/contracts/index.d.ts +1 -0
- package/dist/contracts/index.js +1 -0
- package/dist/validation/document.validation.d.ts +50 -0
- package/dist/validation/document.validation.js +109 -0
- package/dist/validation/document.validation.test.d.ts +1 -0
- package/dist/validation/document.validation.test.js +132 -0
- package/dist/validation/index.d.ts +1 -0
- package/dist/validation/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document Enums — new document management system (GH#21).
|
|
3
|
+
*
|
|
4
|
+
* Replaces the legacy `documentation.enums.ts` (which keeps serving the legacy
|
|
5
|
+
* `documentations` collection during the 60-day retention window). Do not mix
|
|
6
|
+
* the two — `documentation.*` = legacy, `document.*` = new system.
|
|
7
|
+
*
|
|
8
|
+
* Conventions:
|
|
9
|
+
* - snake_case values per `.claude/rules/enums.md`
|
|
10
|
+
* - `*_LABELS: Record<Enum, string>` Spanish labels alongside each enum
|
|
11
|
+
*
|
|
12
|
+
* @see .claude/plans/active/20260427-GH21-documentation-system-redesign.md
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Sentinel value for `audience: WebAppRole[]` meaning "visible to everyone".
|
|
16
|
+
* Stored as a literal string in the audience array; resolved at query time to
|
|
17
|
+
* skip the role filter.
|
|
18
|
+
*/
|
|
19
|
+
export declare const AUDIENCE_ALL = "all";
|
|
20
|
+
/**
|
|
21
|
+
* High-level reason a document exists. The first axis of the 3-tier
|
|
22
|
+
* classification (purpose → type → tags).
|
|
23
|
+
*/
|
|
24
|
+
export declare enum Purpose {
|
|
25
|
+
operational = "operational",
|
|
26
|
+
resource = "resource",
|
|
27
|
+
archive = "archive"
|
|
28
|
+
}
|
|
29
|
+
export declare const PURPOSE_LABELS: Record<Purpose, string>;
|
|
30
|
+
/**
|
|
31
|
+
* Specific kind of document within a purpose. Second axis of classification.
|
|
32
|
+
*
|
|
33
|
+
* Mapping of legacy `DocumentationType` is documented in the migration script
|
|
34
|
+
* (`migrate-documentations-to-documents.ts`). Some values exist purely so
|
|
35
|
+
* legacy docs map cleanly (e.g. `meeting_recording` for `admon` videos).
|
|
36
|
+
*/
|
|
37
|
+
export declare enum DocumentType {
|
|
38
|
+
protocol = "protocol",
|
|
39
|
+
guidance = "guidance",
|
|
40
|
+
tutorial = "tutorial",
|
|
41
|
+
format = "format",
|
|
42
|
+
tool = "tool",
|
|
43
|
+
policy = "policy",
|
|
44
|
+
reference_data = "reference_data",
|
|
45
|
+
training_material = "training_material",
|
|
46
|
+
legal_document = "legal_document",
|
|
47
|
+
brand_asset = "brand_asset",
|
|
48
|
+
meeting_recording = "meeting_recording",
|
|
49
|
+
historical = "historical"
|
|
50
|
+
}
|
|
51
|
+
export declare const DOCUMENT_TYPE_LABELS: Record<DocumentType, string>;
|
|
52
|
+
/**
|
|
53
|
+
* Lifecycle state of the document.
|
|
54
|
+
*
|
|
55
|
+
* Per workflow decision (master plan §Decision 3): there are NO automatic
|
|
56
|
+
* direct-publish rules per criticality — author chooses to publish directly
|
|
57
|
+
* or send to in_review. Reviewer (different person from author) approves.
|
|
58
|
+
*/
|
|
59
|
+
export declare enum DocumentStatus {
|
|
60
|
+
draft = "draft",
|
|
61
|
+
in_review = "in_review",
|
|
62
|
+
published = "published",
|
|
63
|
+
archived = "archived"
|
|
64
|
+
}
|
|
65
|
+
export declare const DOCUMENT_STATUS_LABELS: Record<DocumentStatus, string>;
|
|
66
|
+
/**
|
|
67
|
+
* How critical the document is. Used for ranking, badge color, and (in SP4)
|
|
68
|
+
* acknowledgment urgency.
|
|
69
|
+
*/
|
|
70
|
+
export declare enum Criticality {
|
|
71
|
+
informational = "informational",
|
|
72
|
+
standard = "standard",
|
|
73
|
+
important = "important",
|
|
74
|
+
critical = "critical"
|
|
75
|
+
}
|
|
76
|
+
export declare const CRITICALITY_LABELS: Record<Criticality, string>;
|
|
77
|
+
/**
|
|
78
|
+
* Semver-style classification of a new version's changes.
|
|
79
|
+
*
|
|
80
|
+
* Drives ack invalidation logic in SP4: `major`/`minor` invalidate prior acks;
|
|
81
|
+
* `patch` does not (per master plan §4.3).
|
|
82
|
+
*/
|
|
83
|
+
export declare enum ChangeType {
|
|
84
|
+
patch = "patch",
|
|
85
|
+
minor = "minor",
|
|
86
|
+
major = "major"
|
|
87
|
+
}
|
|
88
|
+
export declare const CHANGE_TYPE_LABELS: Record<ChangeType, string>;
|
|
89
|
+
/**
|
|
90
|
+
* How the document body is stored.
|
|
91
|
+
*
|
|
92
|
+
* - `markdown`: body is in `DocumentVersion.markdown`
|
|
93
|
+
* - `external_link`: body lives outside the system (Dropbox/YouTube/etc.)
|
|
94
|
+
* - `mixed`: both — markdown wraps an embedded link
|
|
95
|
+
*/
|
|
96
|
+
export declare enum ContentType {
|
|
97
|
+
markdown = "markdown",
|
|
98
|
+
external_link = "external_link",
|
|
99
|
+
mixed = "mixed"
|
|
100
|
+
}
|
|
101
|
+
export declare const CONTENT_TYPE_LABELS: Record<ContentType, string>;
|
|
102
|
+
/**
|
|
103
|
+
* Known providers for `external_link` content. Drives icon + preview card +
|
|
104
|
+
* disclaimer in the doc viewer.
|
|
105
|
+
*/
|
|
106
|
+
export declare enum ExternalLinkProvider {
|
|
107
|
+
youtube = "youtube",
|
|
108
|
+
dropbox = "dropbox",
|
|
109
|
+
google_drive = "google_drive",
|
|
110
|
+
dailymotion = "dailymotion",
|
|
111
|
+
other = "other"
|
|
112
|
+
}
|
|
113
|
+
export declare const EXTERNAL_LINK_PROVIDER_LABELS: Record<ExternalLinkProvider, string>;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Document Enums — new document management system (GH#21).
|
|
4
|
+
*
|
|
5
|
+
* Replaces the legacy `documentation.enums.ts` (which keeps serving the legacy
|
|
6
|
+
* `documentations` collection during the 60-day retention window). Do not mix
|
|
7
|
+
* the two — `documentation.*` = legacy, `document.*` = new system.
|
|
8
|
+
*
|
|
9
|
+
* Conventions:
|
|
10
|
+
* - snake_case values per `.claude/rules/enums.md`
|
|
11
|
+
* - `*_LABELS: Record<Enum, string>` Spanish labels alongside each enum
|
|
12
|
+
*
|
|
13
|
+
* @see .claude/plans/active/20260427-GH21-documentation-system-redesign.md
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.EXTERNAL_LINK_PROVIDER_LABELS = exports.ExternalLinkProvider = exports.CONTENT_TYPE_LABELS = exports.ContentType = exports.CHANGE_TYPE_LABELS = exports.ChangeType = exports.CRITICALITY_LABELS = exports.Criticality = exports.DOCUMENT_STATUS_LABELS = exports.DocumentStatus = exports.DOCUMENT_TYPE_LABELS = exports.DocumentType = exports.PURPOSE_LABELS = exports.Purpose = exports.AUDIENCE_ALL = void 0;
|
|
17
|
+
// ─── Audience sentinel ──────────────────────────────────────────────────────
|
|
18
|
+
/**
|
|
19
|
+
* Sentinel value for `audience: WebAppRole[]` meaning "visible to everyone".
|
|
20
|
+
* Stored as a literal string in the audience array; resolved at query time to
|
|
21
|
+
* skip the role filter.
|
|
22
|
+
*/
|
|
23
|
+
exports.AUDIENCE_ALL = "all";
|
|
24
|
+
// ─── Purpose ────────────────────────────────────────────────────────────────
|
|
25
|
+
/**
|
|
26
|
+
* High-level reason a document exists. The first axis of the 3-tier
|
|
27
|
+
* classification (purpose → type → tags).
|
|
28
|
+
*/
|
|
29
|
+
var Purpose;
|
|
30
|
+
(function (Purpose) {
|
|
31
|
+
Purpose["operational"] = "operational";
|
|
32
|
+
Purpose["resource"] = "resource";
|
|
33
|
+
Purpose["archive"] = "archive";
|
|
34
|
+
})(Purpose || (exports.Purpose = Purpose = {}));
|
|
35
|
+
exports.PURPOSE_LABELS = {
|
|
36
|
+
[Purpose.operational]: "Operativo",
|
|
37
|
+
[Purpose.resource]: "Recurso",
|
|
38
|
+
[Purpose.archive]: "Archivo",
|
|
39
|
+
};
|
|
40
|
+
// ─── DocumentType ───────────────────────────────────────────────────────────
|
|
41
|
+
/**
|
|
42
|
+
* Specific kind of document within a purpose. Second axis of classification.
|
|
43
|
+
*
|
|
44
|
+
* Mapping of legacy `DocumentationType` is documented in the migration script
|
|
45
|
+
* (`migrate-documentations-to-documents.ts`). Some values exist purely so
|
|
46
|
+
* legacy docs map cleanly (e.g. `meeting_recording` for `admon` videos).
|
|
47
|
+
*/
|
|
48
|
+
var DocumentType;
|
|
49
|
+
(function (DocumentType) {
|
|
50
|
+
// Operational
|
|
51
|
+
DocumentType["protocol"] = "protocol";
|
|
52
|
+
DocumentType["guidance"] = "guidance";
|
|
53
|
+
DocumentType["tutorial"] = "tutorial";
|
|
54
|
+
DocumentType["format"] = "format";
|
|
55
|
+
DocumentType["tool"] = "tool";
|
|
56
|
+
DocumentType["policy"] = "policy";
|
|
57
|
+
// Resource
|
|
58
|
+
DocumentType["reference_data"] = "reference_data";
|
|
59
|
+
DocumentType["training_material"] = "training_material";
|
|
60
|
+
DocumentType["legal_document"] = "legal_document";
|
|
61
|
+
DocumentType["brand_asset"] = "brand_asset";
|
|
62
|
+
// Archive
|
|
63
|
+
DocumentType["meeting_recording"] = "meeting_recording";
|
|
64
|
+
DocumentType["historical"] = "historical";
|
|
65
|
+
})(DocumentType || (exports.DocumentType = DocumentType = {}));
|
|
66
|
+
exports.DOCUMENT_TYPE_LABELS = {
|
|
67
|
+
[DocumentType.protocol]: "Protocolo",
|
|
68
|
+
[DocumentType.guidance]: "Lineamiento",
|
|
69
|
+
[DocumentType.tutorial]: "Tutorial",
|
|
70
|
+
[DocumentType.format]: "Formato",
|
|
71
|
+
[DocumentType.tool]: "Herramienta",
|
|
72
|
+
[DocumentType.policy]: "Política",
|
|
73
|
+
[DocumentType.reference_data]: "Datos de referencia",
|
|
74
|
+
[DocumentType.training_material]: "Material de capacitación",
|
|
75
|
+
[DocumentType.legal_document]: "Documento legal",
|
|
76
|
+
[DocumentType.brand_asset]: "Activo de marca",
|
|
77
|
+
[DocumentType.meeting_recording]: "Grabación de reunión",
|
|
78
|
+
[DocumentType.historical]: "Histórico",
|
|
79
|
+
};
|
|
80
|
+
// ─── DocumentStatus ─────────────────────────────────────────────────────────
|
|
81
|
+
/**
|
|
82
|
+
* Lifecycle state of the document.
|
|
83
|
+
*
|
|
84
|
+
* Per workflow decision (master plan §Decision 3): there are NO automatic
|
|
85
|
+
* direct-publish rules per criticality — author chooses to publish directly
|
|
86
|
+
* or send to in_review. Reviewer (different person from author) approves.
|
|
87
|
+
*/
|
|
88
|
+
var DocumentStatus;
|
|
89
|
+
(function (DocumentStatus) {
|
|
90
|
+
DocumentStatus["draft"] = "draft";
|
|
91
|
+
DocumentStatus["in_review"] = "in_review";
|
|
92
|
+
DocumentStatus["published"] = "published";
|
|
93
|
+
DocumentStatus["archived"] = "archived";
|
|
94
|
+
})(DocumentStatus || (exports.DocumentStatus = DocumentStatus = {}));
|
|
95
|
+
exports.DOCUMENT_STATUS_LABELS = {
|
|
96
|
+
[DocumentStatus.draft]: "Borrador",
|
|
97
|
+
[DocumentStatus.in_review]: "En revisión",
|
|
98
|
+
[DocumentStatus.published]: "Publicado",
|
|
99
|
+
[DocumentStatus.archived]: "Archivado",
|
|
100
|
+
};
|
|
101
|
+
// ─── Criticality ────────────────────────────────────────────────────────────
|
|
102
|
+
/**
|
|
103
|
+
* How critical the document is. Used for ranking, badge color, and (in SP4)
|
|
104
|
+
* acknowledgment urgency.
|
|
105
|
+
*/
|
|
106
|
+
var Criticality;
|
|
107
|
+
(function (Criticality) {
|
|
108
|
+
Criticality["informational"] = "informational";
|
|
109
|
+
Criticality["standard"] = "standard";
|
|
110
|
+
Criticality["important"] = "important";
|
|
111
|
+
Criticality["critical"] = "critical";
|
|
112
|
+
})(Criticality || (exports.Criticality = Criticality = {}));
|
|
113
|
+
exports.CRITICALITY_LABELS = {
|
|
114
|
+
[Criticality.informational]: "Informativo",
|
|
115
|
+
[Criticality.standard]: "Estándar",
|
|
116
|
+
[Criticality.important]: "Importante",
|
|
117
|
+
[Criticality.critical]: "Crítico",
|
|
118
|
+
};
|
|
119
|
+
// ─── ChangeType ─────────────────────────────────────────────────────────────
|
|
120
|
+
/**
|
|
121
|
+
* Semver-style classification of a new version's changes.
|
|
122
|
+
*
|
|
123
|
+
* Drives ack invalidation logic in SP4: `major`/`minor` invalidate prior acks;
|
|
124
|
+
* `patch` does not (per master plan §4.3).
|
|
125
|
+
*/
|
|
126
|
+
var ChangeType;
|
|
127
|
+
(function (ChangeType) {
|
|
128
|
+
ChangeType["patch"] = "patch";
|
|
129
|
+
ChangeType["minor"] = "minor";
|
|
130
|
+
ChangeType["major"] = "major";
|
|
131
|
+
})(ChangeType || (exports.ChangeType = ChangeType = {}));
|
|
132
|
+
exports.CHANGE_TYPE_LABELS = {
|
|
133
|
+
[ChangeType.patch]: "Corrección menor",
|
|
134
|
+
[ChangeType.minor]: "Cambio menor",
|
|
135
|
+
[ChangeType.major]: "Cambio mayor",
|
|
136
|
+
};
|
|
137
|
+
// ─── ContentType ────────────────────────────────────────────────────────────
|
|
138
|
+
/**
|
|
139
|
+
* How the document body is stored.
|
|
140
|
+
*
|
|
141
|
+
* - `markdown`: body is in `DocumentVersion.markdown`
|
|
142
|
+
* - `external_link`: body lives outside the system (Dropbox/YouTube/etc.)
|
|
143
|
+
* - `mixed`: both — markdown wraps an embedded link
|
|
144
|
+
*/
|
|
145
|
+
var ContentType;
|
|
146
|
+
(function (ContentType) {
|
|
147
|
+
ContentType["markdown"] = "markdown";
|
|
148
|
+
ContentType["external_link"] = "external_link";
|
|
149
|
+
ContentType["mixed"] = "mixed";
|
|
150
|
+
})(ContentType || (exports.ContentType = ContentType = {}));
|
|
151
|
+
exports.CONTENT_TYPE_LABELS = {
|
|
152
|
+
[ContentType.markdown]: "Markdown",
|
|
153
|
+
[ContentType.external_link]: "Enlace externo",
|
|
154
|
+
[ContentType.mixed]: "Mixto",
|
|
155
|
+
};
|
|
156
|
+
// ─── ExternalLinkProvider ───────────────────────────────────────────────────
|
|
157
|
+
/**
|
|
158
|
+
* Known providers for `external_link` content. Drives icon + preview card +
|
|
159
|
+
* disclaimer in the doc viewer.
|
|
160
|
+
*/
|
|
161
|
+
var ExternalLinkProvider;
|
|
162
|
+
(function (ExternalLinkProvider) {
|
|
163
|
+
ExternalLinkProvider["youtube"] = "youtube";
|
|
164
|
+
ExternalLinkProvider["dropbox"] = "dropbox";
|
|
165
|
+
ExternalLinkProvider["google_drive"] = "google_drive";
|
|
166
|
+
ExternalLinkProvider["dailymotion"] = "dailymotion";
|
|
167
|
+
ExternalLinkProvider["other"] = "other";
|
|
168
|
+
})(ExternalLinkProvider || (exports.ExternalLinkProvider = ExternalLinkProvider = {}));
|
|
169
|
+
exports.EXTERNAL_LINK_PROVIDER_LABELS = {
|
|
170
|
+
[ExternalLinkProvider.youtube]: "YouTube",
|
|
171
|
+
[ExternalLinkProvider.dropbox]: "Dropbox",
|
|
172
|
+
[ExternalLinkProvider.google_drive]: "Google Drive",
|
|
173
|
+
[ExternalLinkProvider.dailymotion]: "Dailymotion",
|
|
174
|
+
[ExternalLinkProvider.other]: "Otro",
|
|
175
|
+
};
|
|
@@ -23,6 +23,7 @@ export * from './hris.constants';
|
|
|
23
23
|
export * from './payroll-features.constants';
|
|
24
24
|
export * from './inventory-session.enums';
|
|
25
25
|
export * from './documentation.enums';
|
|
26
|
+
export * from './document.enums';
|
|
26
27
|
export * from './settlement.enums';
|
|
27
28
|
export * from './client-billing.enums';
|
|
28
29
|
export * from './sat-income-invoice';
|
package/dist/constants/index.js
CHANGED
|
@@ -39,6 +39,7 @@ __exportStar(require("./hris.constants"), exports);
|
|
|
39
39
|
__exportStar(require("./payroll-features.constants"), exports);
|
|
40
40
|
__exportStar(require("./inventory-session.enums"), exports);
|
|
41
41
|
__exportStar(require("./documentation.enums"), exports);
|
|
42
|
+
__exportStar(require("./document.enums"), exports);
|
|
42
43
|
__exportStar(require("./settlement.enums"), exports);
|
|
43
44
|
__exportStar(require("./client-billing.enums"), exports);
|
|
44
45
|
__exportStar(require("./sat-income-invoice"), exports);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Document API Contracts (GH#21)
|
|
4
|
+
* Request and Response types for the new document management system.
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
18
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
__exportStar(require("./requests"), exports);
|
|
22
|
+
__exportStar(require("./responses"), exports);
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document API Request Contracts (GH#21)
|
|
3
|
+
*
|
|
4
|
+
* Shapes the frontend (and migration script) sends to the backend.
|
|
5
|
+
* Per `api-contracts.md`:
|
|
6
|
+
* - Date fields are ISO 8601 strings, never `Date` objects.
|
|
7
|
+
* - Update requests are partial; Create requests are complete.
|
|
8
|
+
*/
|
|
9
|
+
import { ChangeType, ContentType, Criticality, DocumentStatus, DocumentType, ExternalLinkProvider, Purpose } from "../../constants/document.enums";
|
|
10
|
+
import { WebAppRole } from "../../constants/collaborator.constants";
|
|
11
|
+
/**
|
|
12
|
+
* One element of `audience[]`. Either a role or the `"all"` sentinel.
|
|
13
|
+
*
|
|
14
|
+
* Stored as plain strings in DB (e.g. `["Administrador", "Gerente"]` or
|
|
15
|
+
* `["all"]`); resolved at query time.
|
|
16
|
+
*/
|
|
17
|
+
export type DocumentAudienceEntry = WebAppRole | "all";
|
|
18
|
+
/**
|
|
19
|
+
* Body for a document version's content. Exactly one of `markdown` /
|
|
20
|
+
* `externalLink` is required for `markdown` / `external_link` contentType;
|
|
21
|
+
* BOTH are required for `mixed`.
|
|
22
|
+
*/
|
|
23
|
+
export interface DocumentVersionContent {
|
|
24
|
+
contentType: ContentType;
|
|
25
|
+
markdown?: string;
|
|
26
|
+
externalLink?: {
|
|
27
|
+
url: string;
|
|
28
|
+
provider: ExternalLinkProvider;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Create Document Request
|
|
33
|
+
*
|
|
34
|
+
* Creates a new document AND its initial version (always status=draft,
|
|
35
|
+
* versionNumber="1.0.0"). Author becomes `createdBy`.
|
|
36
|
+
*
|
|
37
|
+
* @example POST /api/documents
|
|
38
|
+
*/
|
|
39
|
+
export interface CreateDocumentRequest {
|
|
40
|
+
title: string;
|
|
41
|
+
/** Optional — auto-generated from title via slugify if omitted. */
|
|
42
|
+
slug?: string;
|
|
43
|
+
/** Optional human-readable code (e.g. "PROT-EGO-01"). */
|
|
44
|
+
code?: string;
|
|
45
|
+
purpose: Purpose;
|
|
46
|
+
type: DocumentType;
|
|
47
|
+
tags?: string[];
|
|
48
|
+
audience: DocumentAudienceEntry[];
|
|
49
|
+
criticality: Criticality;
|
|
50
|
+
requiredForOnboarding?: boolean;
|
|
51
|
+
requiresAcknowledgment?: boolean;
|
|
52
|
+
/** ObjectIds (as strings) of related documents. */
|
|
53
|
+
references?: string[];
|
|
54
|
+
/** Initial version content. */
|
|
55
|
+
initialVersion: DocumentVersionContent;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Update Document Metadata Request
|
|
59
|
+
*
|
|
60
|
+
* Updates metadata only (no version change). For content edits use
|
|
61
|
+
* SaveDraftVersionRequest or CreateNewVersionRequest.
|
|
62
|
+
*
|
|
63
|
+
* @example PATCH /api/documents/:id
|
|
64
|
+
*/
|
|
65
|
+
export interface UpdateDocumentMetadataRequest {
|
|
66
|
+
title?: string;
|
|
67
|
+
slug?: string;
|
|
68
|
+
code?: string | null;
|
|
69
|
+
purpose?: Purpose;
|
|
70
|
+
type?: DocumentType;
|
|
71
|
+
tags?: string[];
|
|
72
|
+
audience?: DocumentAudienceEntry[];
|
|
73
|
+
criticality?: Criticality;
|
|
74
|
+
requiredForOnboarding?: boolean;
|
|
75
|
+
requiresAcknowledgment?: boolean;
|
|
76
|
+
references?: string[];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Save Draft Version Request
|
|
80
|
+
*
|
|
81
|
+
* Updates the existing draft version row in place (no new row created).
|
|
82
|
+
* Only valid while a draft version exists.
|
|
83
|
+
*
|
|
84
|
+
* @example PATCH /api/documents/:id/versions/draft
|
|
85
|
+
*/
|
|
86
|
+
export interface SaveDraftVersionRequest {
|
|
87
|
+
content: DocumentVersionContent;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Create New Version Request
|
|
91
|
+
*
|
|
92
|
+
* Branches a new draft version from the currently published version.
|
|
93
|
+
* Used when editing an already-published document.
|
|
94
|
+
*
|
|
95
|
+
* @example POST /api/documents/:id/versions
|
|
96
|
+
*/
|
|
97
|
+
export interface CreateNewVersionRequest {
|
|
98
|
+
changeType: ChangeType;
|
|
99
|
+
changeNotes: string;
|
|
100
|
+
content: DocumentVersionContent;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Submit For Review Request
|
|
104
|
+
*
|
|
105
|
+
* Transitions a draft to `in_review`. Empty body — context comes from URL.
|
|
106
|
+
*
|
|
107
|
+
* @example POST /api/documents/:id/submit-for-review
|
|
108
|
+
*/
|
|
109
|
+
export type SubmitForReviewRequest = Record<string, never>;
|
|
110
|
+
/**
|
|
111
|
+
* Publish Document Request
|
|
112
|
+
*
|
|
113
|
+
* Transitions a draft or in_review version to `published`. The current
|
|
114
|
+
* published version (if any) becomes superseded.
|
|
115
|
+
*
|
|
116
|
+
* Per master plan §Decision 3: no automatic enforcement of review-required
|
|
117
|
+
* rules per criticality — backend trusts the caller's authorization.
|
|
118
|
+
*
|
|
119
|
+
* @example POST /api/documents/:id/publish
|
|
120
|
+
*/
|
|
121
|
+
export interface PublishDocumentRequest {
|
|
122
|
+
/** Required if publishing without prior in_review (audit trail). */
|
|
123
|
+
changeNotes?: string;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Archive Document Request
|
|
127
|
+
*
|
|
128
|
+
* Marks the document as `archived`. Archived docs are hidden from default
|
|
129
|
+
* lists but data is preserved. Restricted to admin for `legal_document` and
|
|
130
|
+
* `brand_asset` types.
|
|
131
|
+
*
|
|
132
|
+
* @example POST /api/documents/:id/archive
|
|
133
|
+
*/
|
|
134
|
+
export interface ArchiveDocumentRequest {
|
|
135
|
+
reason?: string;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Unarchive Document Request
|
|
139
|
+
*
|
|
140
|
+
* Returns an archived doc to its prior `published` state. Admin only.
|
|
141
|
+
*
|
|
142
|
+
* @example POST /api/documents/:id/unarchive
|
|
143
|
+
*/
|
|
144
|
+
export type UnarchiveDocumentRequest = Record<string, never>;
|
|
145
|
+
/**
|
|
146
|
+
* Acknowledge Version Request
|
|
147
|
+
*
|
|
148
|
+
* Records that the calling user has read the current version of the document.
|
|
149
|
+
* Idempotent — second call is a no-op.
|
|
150
|
+
*
|
|
151
|
+
* @example POST /api/documents/:id/acknowledge
|
|
152
|
+
*/
|
|
153
|
+
export type AcknowledgeVersionRequest = Record<string, never>;
|
|
154
|
+
/**
|
|
155
|
+
* List Documents Query Filters
|
|
156
|
+
*
|
|
157
|
+
* Query string parameters for `GET /api/documents`. All optional. Audience
|
|
158
|
+
* filtering by current user role is implicit (handled server-side).
|
|
159
|
+
*
|
|
160
|
+
* @example GET /api/documents?purpose=operational&status=published
|
|
161
|
+
*/
|
|
162
|
+
export interface ListDocumentsQuery {
|
|
163
|
+
purpose?: Purpose;
|
|
164
|
+
type?: DocumentType;
|
|
165
|
+
status?: DocumentStatus;
|
|
166
|
+
criticality?: Criticality;
|
|
167
|
+
/** Comma-separated tag list, OR semantics. */
|
|
168
|
+
tags?: string;
|
|
169
|
+
/** Substring match on title (server may also $text-search). */
|
|
170
|
+
search?: string;
|
|
171
|
+
requiredForOnboarding?: boolean;
|
|
172
|
+
requiresAcknowledgment?: boolean;
|
|
173
|
+
limit?: number;
|
|
174
|
+
skip?: number;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Search Documents Query
|
|
178
|
+
*
|
|
179
|
+
* @example GET /api/documents/search?q=comisiones
|
|
180
|
+
*/
|
|
181
|
+
export interface SearchDocumentsQuery {
|
|
182
|
+
q: string;
|
|
183
|
+
limit?: number;
|
|
184
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Document API Request Contracts (GH#21)
|
|
4
|
+
*
|
|
5
|
+
* Shapes the frontend (and migration script) sends to the backend.
|
|
6
|
+
* Per `api-contracts.md`:
|
|
7
|
+
* - Date fields are ISO 8601 strings, never `Date` objects.
|
|
8
|
+
* - Update requests are partial; Create requests are complete.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document API Response Contracts (GH#21)
|
|
3
|
+
*
|
|
4
|
+
* Shapes the backend returns. Per `api-contracts.md`:
|
|
5
|
+
* - Date fields are ISO 8601 strings (server converts Date → ISO at the mapper layer).
|
|
6
|
+
* - Use Public → View → Admin inheritance where it adds value.
|
|
7
|
+
*/
|
|
8
|
+
import { ChangeType, ContentType, Criticality, DocumentStatus, DocumentType, ExternalLinkProvider, Purpose } from "../../constants/document.enums";
|
|
9
|
+
import { DocumentAudienceEntry } from "./requests";
|
|
10
|
+
/**
|
|
11
|
+
* Public Document Response
|
|
12
|
+
*
|
|
13
|
+
* Minimal fields. Used for related-doc previews in the viewer sidebar.
|
|
14
|
+
*/
|
|
15
|
+
export interface PublicDocumentResponse {
|
|
16
|
+
id: string;
|
|
17
|
+
slug: string;
|
|
18
|
+
title: string;
|
|
19
|
+
code: string | null;
|
|
20
|
+
purpose: Purpose;
|
|
21
|
+
type: DocumentType;
|
|
22
|
+
status: DocumentStatus;
|
|
23
|
+
criticality: Criticality;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Document View Response
|
|
27
|
+
*
|
|
28
|
+
* Standard fields shown in lists and the viewer header. Used by all
|
|
29
|
+
* authenticated endpoints unless admin-only fields are needed.
|
|
30
|
+
*/
|
|
31
|
+
export interface DocumentViewResponse extends PublicDocumentResponse {
|
|
32
|
+
tags: string[];
|
|
33
|
+
audience: DocumentAudienceEntry[];
|
|
34
|
+
requiredForOnboarding: boolean;
|
|
35
|
+
requiresAcknowledgment: boolean;
|
|
36
|
+
references: string[];
|
|
37
|
+
/** ObjectId of the current published version (or null for draft-only docs). */
|
|
38
|
+
currentVersionId: string | null;
|
|
39
|
+
createdAt: string;
|
|
40
|
+
updatedAt: string;
|
|
41
|
+
createdBy: string;
|
|
42
|
+
updatedBy: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Admin Document Response
|
|
46
|
+
*
|
|
47
|
+
* Adds audit metadata. Used in admin tooling.
|
|
48
|
+
*/
|
|
49
|
+
export interface AdminDocumentResponse extends DocumentViewResponse {
|
|
50
|
+
/** Total versions ever created (current + superseded + drafts). */
|
|
51
|
+
versionCount: number;
|
|
52
|
+
/** Count of users who have acked the current version (null if requiresAck=false). */
|
|
53
|
+
currentVersionAckCount: number | null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Document Summary Response
|
|
57
|
+
*
|
|
58
|
+
* Compact card used in list views and Cmd+K search results. Excludes heavy
|
|
59
|
+
* fields like `references`, `audience`, audit metadata.
|
|
60
|
+
*/
|
|
61
|
+
export interface DocumentSummaryResponse {
|
|
62
|
+
id: string;
|
|
63
|
+
slug: string;
|
|
64
|
+
title: string;
|
|
65
|
+
code: string | null;
|
|
66
|
+
purpose: Purpose;
|
|
67
|
+
type: DocumentType;
|
|
68
|
+
status: DocumentStatus;
|
|
69
|
+
criticality: Criticality;
|
|
70
|
+
tags: string[];
|
|
71
|
+
updatedAt: string;
|
|
72
|
+
/** Match snippet — only present in search responses. */
|
|
73
|
+
snippet?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Document Version Response
|
|
77
|
+
*
|
|
78
|
+
* Full version row. Returned by the viewer, version history, and
|
|
79
|
+
* acknowledgment lookups.
|
|
80
|
+
*/
|
|
81
|
+
export interface DocumentVersionResponse {
|
|
82
|
+
id: string;
|
|
83
|
+
documentId: string;
|
|
84
|
+
versionNumber: string;
|
|
85
|
+
status: "draft" | "in_review" | "current" | "superseded";
|
|
86
|
+
contentType: ContentType;
|
|
87
|
+
markdown: string | null;
|
|
88
|
+
externalLink: {
|
|
89
|
+
url: string;
|
|
90
|
+
provider: ExternalLinkProvider;
|
|
91
|
+
} | null;
|
|
92
|
+
changeType: ChangeType;
|
|
93
|
+
changeNotes: string | null;
|
|
94
|
+
createdAt: string;
|
|
95
|
+
createdBy: string;
|
|
96
|
+
publishedAt: string | null;
|
|
97
|
+
publishedBy: string | null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Document Acknowledgment Response
|
|
101
|
+
*
|
|
102
|
+
* One ack record. Used in the admin "who acknowledged" view.
|
|
103
|
+
*/
|
|
104
|
+
export interface DocumentAcknowledgmentResponse {
|
|
105
|
+
id: string;
|
|
106
|
+
documentId: string;
|
|
107
|
+
documentVersionId: string;
|
|
108
|
+
userId: string;
|
|
109
|
+
acknowledgedAt: string;
|
|
110
|
+
/** Distinguishes onboarding acks from voluntary ones. */
|
|
111
|
+
source: "voluntary" | "onboarding";
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Pending Acknowledgment Response
|
|
115
|
+
*
|
|
116
|
+
* One row in the user-facing "pendientes de acuse" widget. Combines doc + version
|
|
117
|
+
* snapshot needed to render the card.
|
|
118
|
+
*/
|
|
119
|
+
export interface PendingAcknowledgmentResponse {
|
|
120
|
+
documentId: string;
|
|
121
|
+
documentSlug: string;
|
|
122
|
+
documentTitle: string;
|
|
123
|
+
documentVersionId: string;
|
|
124
|
+
versionNumber: string;
|
|
125
|
+
criticality: Criticality;
|
|
126
|
+
/** Days since the version was published (used for the 7-day red badge). */
|
|
127
|
+
pendingDays: number;
|
|
128
|
+
isFromOnboarding: boolean;
|
|
129
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Document API Response Contracts (GH#21)
|
|
4
|
+
*
|
|
5
|
+
* Shapes the backend returns. Per `api-contracts.md`:
|
|
6
|
+
* - Date fields are ISO 8601 strings (server converts Date → ISO at the mapper layer).
|
|
7
|
+
* - Use Public → View → Admin inheritance where it adds value.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
package/dist/contracts/index.js
CHANGED
|
@@ -36,3 +36,4 @@ __exportStar(require("./study-type-catalog"), exports);
|
|
|
36
36
|
__exportStar(require("./supplier-overlay"), exports);
|
|
37
37
|
__exportStar(require("./external-study"), exports);
|
|
38
38
|
__exportStar(require("./pending-dashboard"), exports);
|
|
39
|
+
__exportStar(require("./document"), exports);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document Validation (GH#21)
|
|
3
|
+
*
|
|
4
|
+
* Pure validation functions for document fields. Mirrors the pattern of
|
|
5
|
+
* `rfc.validation.ts`, `email.validation.ts`, etc. — returns
|
|
6
|
+
* `ValidationResult` so callers (Value Objects in backend, frontend Zod
|
|
7
|
+
* schemas, migration scripts) all share one source of truth.
|
|
8
|
+
*/
|
|
9
|
+
import { ValidationResult } from "./rfc.validation";
|
|
10
|
+
/**
|
|
11
|
+
* Validates a document slug.
|
|
12
|
+
*
|
|
13
|
+
* Rules:
|
|
14
|
+
* - 3–80 chars
|
|
15
|
+
* - lowercase alphanumeric + hyphens only
|
|
16
|
+
* - no leading/trailing/consecutive hyphens
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* validateDocumentSlug("protocolo-ego") // { isValid: true }
|
|
20
|
+
* validateDocumentSlug("Protocolo EGO") // { isValid: false, error: ... }
|
|
21
|
+
* validateDocumentSlug("ab") // { isValid: false, error: ... }
|
|
22
|
+
* validateDocumentSlug("foo--bar") // { isValid: false, error: ... }
|
|
23
|
+
*/
|
|
24
|
+
export declare function validateDocumentSlug(value: string | null | undefined): ValidationResult;
|
|
25
|
+
/**
|
|
26
|
+
* Generates a slug candidate from arbitrary text. Caller is responsible for
|
|
27
|
+
* resolving collisions (typically by appending `-2`, `-3`, ...).
|
|
28
|
+
*
|
|
29
|
+
* NOTE: this is a best-effort transliteration. Spanish accents are mapped to
|
|
30
|
+
* their unaccented form. Other unicode is dropped.
|
|
31
|
+
*/
|
|
32
|
+
export declare function slugifyTitle(title: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Validates a semver-style version number `major.minor.patch`.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* validateVersionNumber("1.0.0") // { isValid: true }
|
|
38
|
+
* validateVersionNumber("1.0") // { isValid: false }
|
|
39
|
+
* validateVersionNumber("v1.0.0") // { isValid: false }
|
|
40
|
+
*/
|
|
41
|
+
export declare function validateVersionNumber(value: string | null | undefined): ValidationResult;
|
|
42
|
+
/**
|
|
43
|
+
* Parses a validated version string into its three numeric components.
|
|
44
|
+
* Throws if the input is invalid — call `validateVersionNumber` first.
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseVersionNumber(value: string): {
|
|
47
|
+
major: number;
|
|
48
|
+
minor: number;
|
|
49
|
+
patch: number;
|
|
50
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Document Validation (GH#21)
|
|
4
|
+
*
|
|
5
|
+
* Pure validation functions for document fields. Mirrors the pattern of
|
|
6
|
+
* `rfc.validation.ts`, `email.validation.ts`, etc. — returns
|
|
7
|
+
* `ValidationResult` so callers (Value Objects in backend, frontend Zod
|
|
8
|
+
* schemas, migration scripts) all share one source of truth.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.validateDocumentSlug = validateDocumentSlug;
|
|
12
|
+
exports.slugifyTitle = slugifyTitle;
|
|
13
|
+
exports.validateVersionNumber = validateVersionNumber;
|
|
14
|
+
exports.parseVersionNumber = parseVersionNumber;
|
|
15
|
+
// ─── Slug ───────────────────────────────────────────────────────────────────
|
|
16
|
+
const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
17
|
+
const SLUG_MIN_LENGTH = 3;
|
|
18
|
+
const SLUG_MAX_LENGTH = 80;
|
|
19
|
+
/**
|
|
20
|
+
* Validates a document slug.
|
|
21
|
+
*
|
|
22
|
+
* Rules:
|
|
23
|
+
* - 3–80 chars
|
|
24
|
+
* - lowercase alphanumeric + hyphens only
|
|
25
|
+
* - no leading/trailing/consecutive hyphens
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* validateDocumentSlug("protocolo-ego") // { isValid: true }
|
|
29
|
+
* validateDocumentSlug("Protocolo EGO") // { isValid: false, error: ... }
|
|
30
|
+
* validateDocumentSlug("ab") // { isValid: false, error: ... }
|
|
31
|
+
* validateDocumentSlug("foo--bar") // { isValid: false, error: ... }
|
|
32
|
+
*/
|
|
33
|
+
function validateDocumentSlug(value) {
|
|
34
|
+
if (!value || !value.trim()) {
|
|
35
|
+
return { isValid: false, error: "El slug es requerido" };
|
|
36
|
+
}
|
|
37
|
+
const trimmed = value.trim();
|
|
38
|
+
if (trimmed.length < SLUG_MIN_LENGTH) {
|
|
39
|
+
return {
|
|
40
|
+
isValid: false,
|
|
41
|
+
error: `El slug debe tener al menos ${SLUG_MIN_LENGTH} caracteres`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (trimmed.length > SLUG_MAX_LENGTH) {
|
|
45
|
+
return {
|
|
46
|
+
isValid: false,
|
|
47
|
+
error: `El slug no puede exceder ${SLUG_MAX_LENGTH} caracteres`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (!SLUG_PATTERN.test(trimmed)) {
|
|
51
|
+
return {
|
|
52
|
+
isValid: false,
|
|
53
|
+
error: "El slug solo puede contener minúsculas, números y guiones (sin espacios ni guiones consecutivos)",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return { isValid: true };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Generates a slug candidate from arbitrary text. Caller is responsible for
|
|
60
|
+
* resolving collisions (typically by appending `-2`, `-3`, ...).
|
|
61
|
+
*
|
|
62
|
+
* NOTE: this is a best-effort transliteration. Spanish accents are mapped to
|
|
63
|
+
* their unaccented form. Other unicode is dropped.
|
|
64
|
+
*/
|
|
65
|
+
function slugifyTitle(title) {
|
|
66
|
+
return title
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
.normalize("NFD")
|
|
69
|
+
.replace(/[̀-ͯ]/g, "") // strip accents
|
|
70
|
+
.replace(/ñ/g, "n")
|
|
71
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
72
|
+
.replace(/^-+|-+$/g, "")
|
|
73
|
+
.slice(0, SLUG_MAX_LENGTH)
|
|
74
|
+
.replace(/-+$/g, ""); // re-trim if slice landed on a hyphen
|
|
75
|
+
}
|
|
76
|
+
// ─── Version Number ─────────────────────────────────────────────────────────
|
|
77
|
+
const VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
78
|
+
/**
|
|
79
|
+
* Validates a semver-style version number `major.minor.patch`.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* validateVersionNumber("1.0.0") // { isValid: true }
|
|
83
|
+
* validateVersionNumber("1.0") // { isValid: false }
|
|
84
|
+
* validateVersionNumber("v1.0.0") // { isValid: false }
|
|
85
|
+
*/
|
|
86
|
+
function validateVersionNumber(value) {
|
|
87
|
+
if (!value || !value.trim()) {
|
|
88
|
+
return { isValid: false, error: "El número de versión es requerido" };
|
|
89
|
+
}
|
|
90
|
+
if (!VERSION_PATTERN.test(value.trim())) {
|
|
91
|
+
return {
|
|
92
|
+
isValid: false,
|
|
93
|
+
error: "Versión inválida. Formato esperado: MAJOR.MINOR.PATCH (ej. 1.0.0)",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return { isValid: true };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Parses a validated version string into its three numeric components.
|
|
100
|
+
* Throws if the input is invalid — call `validateVersionNumber` first.
|
|
101
|
+
*/
|
|
102
|
+
function parseVersionNumber(value) {
|
|
103
|
+
const result = validateVersionNumber(value);
|
|
104
|
+
if (!result.isValid) {
|
|
105
|
+
throw new Error(result.error ?? "Versión inválida");
|
|
106
|
+
}
|
|
107
|
+
const [major, minor, patch] = value.trim().split(".").map(Number);
|
|
108
|
+
return { major, minor, patch };
|
|
109
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const document_validation_1 = require("./document.validation");
|
|
4
|
+
describe("validateDocumentSlug", () => {
|
|
5
|
+
describe("valid", () => {
|
|
6
|
+
it("accepts a kebab-case word", () => {
|
|
7
|
+
expect((0, document_validation_1.validateDocumentSlug)("protocolo")).toEqual({ isValid: true });
|
|
8
|
+
});
|
|
9
|
+
it("accepts hyphens between segments", () => {
|
|
10
|
+
expect((0, document_validation_1.validateDocumentSlug)("protocolo-ego")).toEqual({ isValid: true });
|
|
11
|
+
});
|
|
12
|
+
it("accepts numbers", () => {
|
|
13
|
+
expect((0, document_validation_1.validateDocumentSlug)("manual-mpp-v20")).toEqual({ isValid: true });
|
|
14
|
+
});
|
|
15
|
+
it("accepts the minimum length", () => {
|
|
16
|
+
expect((0, document_validation_1.validateDocumentSlug)("abc")).toEqual({ isValid: true });
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
describe("invalid", () => {
|
|
20
|
+
it("rejects empty", () => {
|
|
21
|
+
expect((0, document_validation_1.validateDocumentSlug)("")).toMatchObject({ isValid: false });
|
|
22
|
+
expect((0, document_validation_1.validateDocumentSlug)(" ")).toMatchObject({ isValid: false });
|
|
23
|
+
expect((0, document_validation_1.validateDocumentSlug)(null)).toMatchObject({ isValid: false });
|
|
24
|
+
expect((0, document_validation_1.validateDocumentSlug)(undefined)).toMatchObject({ isValid: false });
|
|
25
|
+
});
|
|
26
|
+
it("rejects too short", () => {
|
|
27
|
+
expect((0, document_validation_1.validateDocumentSlug)("ab")).toMatchObject({ isValid: false });
|
|
28
|
+
});
|
|
29
|
+
it("rejects too long", () => {
|
|
30
|
+
expect((0, document_validation_1.validateDocumentSlug)("a".repeat(81))).toMatchObject({
|
|
31
|
+
isValid: false,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
it("rejects uppercase", () => {
|
|
35
|
+
expect((0, document_validation_1.validateDocumentSlug)("Protocolo")).toMatchObject({
|
|
36
|
+
isValid: false,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
it("rejects spaces", () => {
|
|
40
|
+
expect((0, document_validation_1.validateDocumentSlug)("protocolo ego")).toMatchObject({
|
|
41
|
+
isValid: false,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
it("rejects underscores", () => {
|
|
45
|
+
expect((0, document_validation_1.validateDocumentSlug)("protocolo_ego")).toMatchObject({
|
|
46
|
+
isValid: false,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
it("rejects leading/trailing hyphens", () => {
|
|
50
|
+
expect((0, document_validation_1.validateDocumentSlug)("-protocolo")).toMatchObject({
|
|
51
|
+
isValid: false,
|
|
52
|
+
});
|
|
53
|
+
expect((0, document_validation_1.validateDocumentSlug)("protocolo-")).toMatchObject({
|
|
54
|
+
isValid: false,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
it("rejects consecutive hyphens", () => {
|
|
58
|
+
expect((0, document_validation_1.validateDocumentSlug)("foo--bar")).toMatchObject({
|
|
59
|
+
isValid: false,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
it("rejects accented characters", () => {
|
|
63
|
+
expect((0, document_validation_1.validateDocumentSlug)("protocolo-ñ")).toMatchObject({
|
|
64
|
+
isValid: false,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe("slugifyTitle", () => {
|
|
70
|
+
it("converts spaces to hyphens", () => {
|
|
71
|
+
expect((0, document_validation_1.slugifyTitle)("Protocolo EGO")).toBe("protocolo-ego");
|
|
72
|
+
});
|
|
73
|
+
it("strips Spanish accents", () => {
|
|
74
|
+
expect((0, document_validation_1.slugifyTitle)("Anestésica común")).toBe("anestesica-comun");
|
|
75
|
+
});
|
|
76
|
+
it("converts ñ to n", () => {
|
|
77
|
+
expect((0, document_validation_1.slugifyTitle)("Compañeros")).toBe("companeros");
|
|
78
|
+
});
|
|
79
|
+
it("collapses repeated hyphens", () => {
|
|
80
|
+
expect((0, document_validation_1.slugifyTitle)("foo bar — baz")).toBe("foo-bar-baz");
|
|
81
|
+
});
|
|
82
|
+
it("trims leading/trailing hyphens", () => {
|
|
83
|
+
expect((0, document_validation_1.slugifyTitle)("--foo--")).toBe("foo");
|
|
84
|
+
});
|
|
85
|
+
it("preserves digits", () => {
|
|
86
|
+
expect((0, document_validation_1.slugifyTitle)("Manual MPP v20")).toBe("manual-mpp-v20");
|
|
87
|
+
});
|
|
88
|
+
it("returns a slug that passes validateDocumentSlug for typical titles", () => {
|
|
89
|
+
const cases = [
|
|
90
|
+
"Protocolo para Examen General de Orina (EGO)",
|
|
91
|
+
"Reunión enero 2020",
|
|
92
|
+
"Política interna de venta de medicamentos",
|
|
93
|
+
"HVP Web - Módulo documentación",
|
|
94
|
+
];
|
|
95
|
+
for (const title of cases) {
|
|
96
|
+
const slug = (0, document_validation_1.slugifyTitle)(title);
|
|
97
|
+
expect((0, document_validation_1.validateDocumentSlug)(slug).isValid).toBe(true);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe("validateVersionNumber", () => {
|
|
102
|
+
it("accepts valid semver", () => {
|
|
103
|
+
expect((0, document_validation_1.validateVersionNumber)("1.0.0")).toEqual({ isValid: true });
|
|
104
|
+
expect((0, document_validation_1.validateVersionNumber)("12.34.56")).toEqual({ isValid: true });
|
|
105
|
+
});
|
|
106
|
+
it("rejects empty", () => {
|
|
107
|
+
expect((0, document_validation_1.validateVersionNumber)("")).toMatchObject({ isValid: false });
|
|
108
|
+
expect((0, document_validation_1.validateVersionNumber)(null)).toMatchObject({ isValid: false });
|
|
109
|
+
expect((0, document_validation_1.validateVersionNumber)(undefined)).toMatchObject({ isValid: false });
|
|
110
|
+
});
|
|
111
|
+
it("rejects 2-part versions", () => {
|
|
112
|
+
expect((0, document_validation_1.validateVersionNumber)("1.0")).toMatchObject({ isValid: false });
|
|
113
|
+
});
|
|
114
|
+
it("rejects v-prefix", () => {
|
|
115
|
+
expect((0, document_validation_1.validateVersionNumber)("v1.0.0")).toMatchObject({ isValid: false });
|
|
116
|
+
});
|
|
117
|
+
it("rejects non-numeric segments", () => {
|
|
118
|
+
expect((0, document_validation_1.validateVersionNumber)("1.0.x")).toMatchObject({ isValid: false });
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe("parseVersionNumber", () => {
|
|
122
|
+
it("parses each segment", () => {
|
|
123
|
+
expect((0, document_validation_1.parseVersionNumber)("1.2.3")).toEqual({
|
|
124
|
+
major: 1,
|
|
125
|
+
minor: 2,
|
|
126
|
+
patch: 3,
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
it("throws on invalid input", () => {
|
|
130
|
+
expect(() => (0, document_validation_1.parseVersionNumber)("1.0")).toThrow();
|
|
131
|
+
});
|
|
132
|
+
});
|
package/dist/validation/index.js
CHANGED
|
@@ -24,3 +24,4 @@ __exportStar(require("./nss.validation"), exports);
|
|
|
24
24
|
__exportStar(require("./email.validation"), exports);
|
|
25
25
|
__exportStar(require("./phone.validation"), exports);
|
|
26
26
|
__exportStar(require("./address.validation"), exports);
|
|
27
|
+
__exportStar(require("./document.validation"), exports);
|