@lucaapp/service-utils 5.2.0 → 5.4.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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/atomicAuditLog/index.d.ts +148 -0
- package/dist/lib/atomicAuditLog/index.js +313 -0
- package/dist/lib/iso3166/iso3166Mapping.d.ts +118 -2
- package/dist/lib/iso3166/iso3166Mapping.js +23 -7
- package/dist/lib/negotiator/language.d.ts +1 -1
- package/dist/lib/validation.d.ts +4 -4
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./lib/api"), exports);
|
|
18
|
+
__exportStar(require("./lib/atomicAuditLog"), exports);
|
|
18
19
|
__exportStar(require("./lib/kafka"), exports);
|
|
19
20
|
__exportStar(require("./lib/serviceIdentity"), exports);
|
|
20
21
|
__exportStar(require("./lib/urlEncoded"), exports);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { Model, ModelStatic, Sequelize } from 'sequelize';
|
|
2
|
+
import { DataTypes } from 'sequelize';
|
|
3
|
+
/**
|
|
4
|
+
* Represents a partition table with its timestamp.
|
|
5
|
+
*/
|
|
6
|
+
export interface PartitionTable {
|
|
7
|
+
tableName: string;
|
|
8
|
+
timestamp: Date;
|
|
9
|
+
}
|
|
10
|
+
interface AddHistoryOptions {
|
|
11
|
+
exclude?: string[];
|
|
12
|
+
}
|
|
13
|
+
interface AtomicAuditLogOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Name of the audit log table.
|
|
16
|
+
* Default: 'AuditLogs'
|
|
17
|
+
*/
|
|
18
|
+
attributeRevisionModelTableName?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Type of the primary key for the tracked models.
|
|
21
|
+
* Default: DataTypes.INTEGER
|
|
22
|
+
*/
|
|
23
|
+
primaryKeyType?: typeof DataTypes.UUID | typeof DataTypes.INTEGER;
|
|
24
|
+
/**
|
|
25
|
+
* Number of partitions to include in the UNION ALL view.
|
|
26
|
+
* Default: 6 (current table + 5 archives = ~6 weeks of data)
|
|
27
|
+
*/
|
|
28
|
+
viewPartitionCount?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Result of a partition operation.
|
|
32
|
+
*/
|
|
33
|
+
export interface PartitionResult {
|
|
34
|
+
archiveTableName: string;
|
|
35
|
+
timestamp: Date;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* AtomicAuditLog provides audit logging with atomic revision assignment.
|
|
39
|
+
*
|
|
40
|
+
* This is a drop-in replacement for sequelize-central-log that solves the
|
|
41
|
+
* race condition issue where concurrent updates could result in duplicate
|
|
42
|
+
* revision numbers.
|
|
43
|
+
*
|
|
44
|
+
* Key differences from sequelize-central-log:
|
|
45
|
+
* - Uses PostgreSQL advisory locks to serialize inserts per entity
|
|
46
|
+
* - Calculates revision using MAX(revision)+1 subquery for atomicity
|
|
47
|
+
* - Runs within the same transaction as entity updates (when transaction provided)
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const auditLog = new AtomicAuditLog(sequelize, {
|
|
52
|
+
* attributeRevisionModelTableName: 'StayReservationAuditLogs',
|
|
53
|
+
* primaryKeyType: DataTypes.UUID,
|
|
54
|
+
* });
|
|
55
|
+
*
|
|
56
|
+
* await auditLog.addHistory(StayReservation, {
|
|
57
|
+
* exclude: ['primaryGuest'],
|
|
58
|
+
* });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare class AtomicAuditLog {
|
|
62
|
+
private sequelize;
|
|
63
|
+
private tableName;
|
|
64
|
+
private primaryKeyType;
|
|
65
|
+
private viewPartitionCount;
|
|
66
|
+
constructor(sequelize: Sequelize, options?: AtomicAuditLogOptions);
|
|
67
|
+
/**
|
|
68
|
+
* Gets the table name for this audit log.
|
|
69
|
+
*/
|
|
70
|
+
getTableName(): string;
|
|
71
|
+
/**
|
|
72
|
+
* Gets the view name that combines all partitions.
|
|
73
|
+
*/
|
|
74
|
+
getViewName(): string;
|
|
75
|
+
/**
|
|
76
|
+
* Adds audit logging hooks to a Sequelize model.
|
|
77
|
+
*
|
|
78
|
+
* @param model - The Sequelize model to track
|
|
79
|
+
* @param options - Configuration options
|
|
80
|
+
* @returns Promise that resolves when hooks are added
|
|
81
|
+
*/
|
|
82
|
+
addHistory<T extends Model>(model: ModelStatic<T>, options?: AddHistoryOptions): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Creates an audit log entry with atomically assigned revision number.
|
|
85
|
+
*/
|
|
86
|
+
private createAuditLogEntry;
|
|
87
|
+
/**
|
|
88
|
+
* Creates an audit hook handler for a specific operation.
|
|
89
|
+
*/
|
|
90
|
+
private createAuditHook;
|
|
91
|
+
/**
|
|
92
|
+
* Gets the last N partition tables for this audit log.
|
|
93
|
+
*
|
|
94
|
+
* Partition tables follow the naming pattern: `{tableName}_{YYYY_MM_DD}T{HH_MM_SS}`
|
|
95
|
+
*
|
|
96
|
+
* @param count - Number of partitions to retrieve (default: viewPartitionCount - 1)
|
|
97
|
+
* @returns Array of partition tables sorted by timestamp descending
|
|
98
|
+
*/
|
|
99
|
+
getLastNPartitionTables(count?: number): Promise<PartitionTable[]>;
|
|
100
|
+
/**
|
|
101
|
+
* Partitions the audit log table by creating a new empty table and
|
|
102
|
+
* archiving the current data.
|
|
103
|
+
*
|
|
104
|
+
* This operation:
|
|
105
|
+
* 1. Creates a new table with the same schema (including indexes/constraints)
|
|
106
|
+
* 2. Renames the current table to an archive table with timestamp suffix
|
|
107
|
+
* 3. Renames the new table to the original table name
|
|
108
|
+
* 4. Recreates the UNION ALL view
|
|
109
|
+
*
|
|
110
|
+
* The entire operation runs in a transaction for atomicity.
|
|
111
|
+
*
|
|
112
|
+
* @returns The name of the archive table and timestamp
|
|
113
|
+
* @example
|
|
114
|
+
* ```typescript
|
|
115
|
+
* const result = await auditLog.partitionTable();
|
|
116
|
+
* console.log(`Archived to: ${result.archiveTableName}`);
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
partitionTable(): Promise<PartitionResult>;
|
|
120
|
+
/**
|
|
121
|
+
* Recreates the UNION ALL view that combines the current table with archive partitions.
|
|
122
|
+
*
|
|
123
|
+
* The view includes:
|
|
124
|
+
* - The current (active) table
|
|
125
|
+
* - The last N-1 archive tables (where N = viewPartitionCount)
|
|
126
|
+
*
|
|
127
|
+
* Each row includes a `partition_table` column indicating its source.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```typescript
|
|
131
|
+
* await auditLog.recreateView();
|
|
132
|
+
* // Creates view: StayReservationAuditLogs_View
|
|
133
|
+
* // Combining: StayReservationAuditLogs + last 5 archives
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
recreateView(): Promise<void>;
|
|
137
|
+
/**
|
|
138
|
+
* Drops old partition tables beyond the retention count.
|
|
139
|
+
*
|
|
140
|
+
* This is useful for cleaning up old audit data while maintaining
|
|
141
|
+
* the view with recent partitions.
|
|
142
|
+
*
|
|
143
|
+
* @param retentionCount - Number of archive partitions to keep (default: viewPartitionCount * 2)
|
|
144
|
+
* @returns Array of dropped table names
|
|
145
|
+
*/
|
|
146
|
+
dropOldPartitions(retentionCount?: number): Promise<string[]>;
|
|
147
|
+
}
|
|
148
|
+
export {};
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AtomicAuditLog = void 0;
|
|
4
|
+
const sequelize_1 = require("sequelize");
|
|
5
|
+
/**
|
|
6
|
+
* Default columns to exclude from audit logging.
|
|
7
|
+
*/
|
|
8
|
+
const DEFAULT_EXCLUDE = [
|
|
9
|
+
'id',
|
|
10
|
+
'uuid',
|
|
11
|
+
'createdAt',
|
|
12
|
+
'updatedAt',
|
|
13
|
+
'deletedAt',
|
|
14
|
+
'revision',
|
|
15
|
+
];
|
|
16
|
+
const isValidValue = (value) => value !== undefined && value !== null;
|
|
17
|
+
const computeCreateDiff = (currentValues, excludeSet) => {
|
|
18
|
+
const diff = [];
|
|
19
|
+
for (const [key, value] of Object.entries(currentValues)) {
|
|
20
|
+
if (!excludeSet.has(key) && isValidValue(value)) {
|
|
21
|
+
diff.push({ key, values: { new: value } });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return diff;
|
|
25
|
+
};
|
|
26
|
+
const computeDestroyDiff = (previousValues, excludeSet) => {
|
|
27
|
+
const diff = [];
|
|
28
|
+
for (const [key, value] of Object.entries(previousValues)) {
|
|
29
|
+
if (!excludeSet.has(key) && isValidValue(value)) {
|
|
30
|
+
diff.push({ key, values: { old: value } });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return diff;
|
|
34
|
+
};
|
|
35
|
+
const computeUpdateDiff = (instance, previousValues, currentValues, excludeSet) => {
|
|
36
|
+
const diff = [];
|
|
37
|
+
const changedKeys = instance.changed();
|
|
38
|
+
if (!changedKeys || !Array.isArray(changedKeys)) {
|
|
39
|
+
return diff;
|
|
40
|
+
}
|
|
41
|
+
for (const key of changedKeys) {
|
|
42
|
+
if (!excludeSet.has(key)) {
|
|
43
|
+
diff.push({
|
|
44
|
+
key,
|
|
45
|
+
values: { old: previousValues[key], new: currentValues[key] },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return diff;
|
|
50
|
+
};
|
|
51
|
+
const computeDiff = (instance, operation, exclude) => {
|
|
52
|
+
const previousValues = instance.previous();
|
|
53
|
+
const currentValues = instance.dataValues;
|
|
54
|
+
const excludeSet = new Set([...DEFAULT_EXCLUDE, ...exclude]);
|
|
55
|
+
if (operation === 'create') {
|
|
56
|
+
return computeCreateDiff(currentValues, excludeSet);
|
|
57
|
+
}
|
|
58
|
+
if (operation === 'destroy') {
|
|
59
|
+
return computeDestroyDiff(previousValues, excludeSet);
|
|
60
|
+
}
|
|
61
|
+
return computeUpdateDiff(instance, previousValues, currentValues, excludeSet);
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* AtomicAuditLog provides audit logging with atomic revision assignment.
|
|
65
|
+
*
|
|
66
|
+
* This is a drop-in replacement for sequelize-central-log that solves the
|
|
67
|
+
* race condition issue where concurrent updates could result in duplicate
|
|
68
|
+
* revision numbers.
|
|
69
|
+
*
|
|
70
|
+
* Key differences from sequelize-central-log:
|
|
71
|
+
* - Uses PostgreSQL advisory locks to serialize inserts per entity
|
|
72
|
+
* - Calculates revision using MAX(revision)+1 subquery for atomicity
|
|
73
|
+
* - Runs within the same transaction as entity updates (when transaction provided)
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const auditLog = new AtomicAuditLog(sequelize, {
|
|
78
|
+
* attributeRevisionModelTableName: 'StayReservationAuditLogs',
|
|
79
|
+
* primaryKeyType: DataTypes.UUID,
|
|
80
|
+
* });
|
|
81
|
+
*
|
|
82
|
+
* await auditLog.addHistory(StayReservation, {
|
|
83
|
+
* exclude: ['primaryGuest'],
|
|
84
|
+
* });
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
class AtomicAuditLog {
|
|
88
|
+
constructor(sequelize, options = {}) {
|
|
89
|
+
this.sequelize = sequelize;
|
|
90
|
+
this.tableName = options.attributeRevisionModelTableName ?? 'AuditLogs';
|
|
91
|
+
this.primaryKeyType = options.primaryKeyType ?? sequelize_1.DataTypes.INTEGER;
|
|
92
|
+
this.viewPartitionCount = options.viewPartitionCount ?? 6;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Gets the table name for this audit log.
|
|
96
|
+
*/
|
|
97
|
+
getTableName() {
|
|
98
|
+
return this.tableName;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Gets the view name that combines all partitions.
|
|
102
|
+
*/
|
|
103
|
+
getViewName() {
|
|
104
|
+
return `${this.tableName}_View`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Adds audit logging hooks to a Sequelize model.
|
|
108
|
+
*
|
|
109
|
+
* @param model - The Sequelize model to track
|
|
110
|
+
* @param options - Configuration options
|
|
111
|
+
* @returns Promise that resolves when hooks are added
|
|
112
|
+
*/
|
|
113
|
+
async addHistory(model, options = {}) {
|
|
114
|
+
const { exclude = [] } = options;
|
|
115
|
+
const modelName = model.name;
|
|
116
|
+
const primaryKeyAttribute = model.primaryKeyAttribute || 'uuid';
|
|
117
|
+
const createHook = this.createAuditHook(modelName, primaryKeyAttribute, exclude, 'create');
|
|
118
|
+
const updateHook = this.createAuditHook(modelName, primaryKeyAttribute, exclude, 'update');
|
|
119
|
+
const destroyHook = this.createAuditHook(modelName, primaryKeyAttribute, exclude, 'destroy');
|
|
120
|
+
model.addHook('afterCreate', createHook);
|
|
121
|
+
model.addHook('afterUpdate', updateHook);
|
|
122
|
+
model.addHook('afterDestroy', destroyHook);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Creates an audit log entry with atomically assigned revision number.
|
|
126
|
+
*/
|
|
127
|
+
async createAuditLogEntry(modelId, modelName, operation, diff, transaction) {
|
|
128
|
+
// Use advisory lock to serialize inserts for the same entity
|
|
129
|
+
// pg_advisory_xact_lock automatically releases at transaction end
|
|
130
|
+
const lockKey = `hashtext('${this.tableName}:${modelId}')`;
|
|
131
|
+
await this.sequelize.query(`SELECT pg_advisory_xact_lock(${lockKey})`, {
|
|
132
|
+
transaction,
|
|
133
|
+
type: sequelize_1.QueryTypes.SELECT,
|
|
134
|
+
});
|
|
135
|
+
// Insert with subquery for atomic revision assignment
|
|
136
|
+
const query = `
|
|
137
|
+
INSERT INTO "${this.tableName}" ("modelId", "model", "operation", "diff", "revision", "createdAt")
|
|
138
|
+
SELECT
|
|
139
|
+
:modelId,
|
|
140
|
+
:modelName,
|
|
141
|
+
:operation,
|
|
142
|
+
:diff::jsonb,
|
|
143
|
+
COALESCE(
|
|
144
|
+
(SELECT MAX("revision") + 1 FROM "${this.tableName}" WHERE "modelId" = :modelId),
|
|
145
|
+
0
|
|
146
|
+
),
|
|
147
|
+
NOW()
|
|
148
|
+
`;
|
|
149
|
+
await this.sequelize.query(query, {
|
|
150
|
+
replacements: {
|
|
151
|
+
modelId,
|
|
152
|
+
modelName,
|
|
153
|
+
operation,
|
|
154
|
+
diff: JSON.stringify(diff),
|
|
155
|
+
},
|
|
156
|
+
type: sequelize_1.QueryTypes.INSERT,
|
|
157
|
+
transaction,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Creates an audit hook handler for a specific operation.
|
|
162
|
+
*/
|
|
163
|
+
createAuditHook(modelName, primaryKeyAttribute, exclude, operation) {
|
|
164
|
+
return async (instance, options) => {
|
|
165
|
+
try {
|
|
166
|
+
const diff = computeDiff(instance, operation, exclude);
|
|
167
|
+
if (diff.length === 0) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const modelId = instance.getDataValue(primaryKeyAttribute);
|
|
171
|
+
await this.createAuditLogEntry(modelId, modelName, operation, diff, options.transaction);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
// Log error but don't fail the main operation
|
|
175
|
+
// eslint-disable-next-line no-console
|
|
176
|
+
console.warn(`Error creating audit log entry for ${modelName}:`, error);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Gets the last N partition tables for this audit log.
|
|
182
|
+
*
|
|
183
|
+
* Partition tables follow the naming pattern: `{tableName}_{YYYY_MM_DD}T{HH_MM_SS}`
|
|
184
|
+
*
|
|
185
|
+
* @param count - Number of partitions to retrieve (default: viewPartitionCount - 1)
|
|
186
|
+
* @returns Array of partition tables sorted by timestamp descending
|
|
187
|
+
*/
|
|
188
|
+
async getLastNPartitionTables(count) {
|
|
189
|
+
const partitionCount = count ?? this.viewPartitionCount - 1;
|
|
190
|
+
const archiveTables = await this.sequelize.query(`SELECT tablename FROM pg_tables
|
|
191
|
+
WHERE schemaname = 'public'
|
|
192
|
+
AND tablename LIKE $1
|
|
193
|
+
AND tablename != $2
|
|
194
|
+
ORDER BY tablename DESC`, {
|
|
195
|
+
type: sequelize_1.QueryTypes.SELECT,
|
|
196
|
+
bind: [`${this.tableName}_%`, this.tableName],
|
|
197
|
+
});
|
|
198
|
+
const partitionTables = [];
|
|
199
|
+
const timestampRegex = new RegExp(`${this.tableName}_(\\d{4}(?:_\\d{2}){5})`);
|
|
200
|
+
for (const table of archiveTables) {
|
|
201
|
+
const timestampMatch = table.tablename.match(timestampRegex);
|
|
202
|
+
if (timestampMatch) {
|
|
203
|
+
const timestampString = timestampMatch[1];
|
|
204
|
+
// Convert YYYY_MM_DD_HH_MM_SS to YYYY-MM-DD HH:MM:SS
|
|
205
|
+
const formattedTimestamp = timestampString
|
|
206
|
+
.replaceAll('_', '-')
|
|
207
|
+
.replace(/^(\d{4}-\d{2}-\d{2})-(\d{2})-(\d{2})-(\d{2})$/, '$1 $2:$3:$4');
|
|
208
|
+
const timestamp = new Date(formattedTimestamp);
|
|
209
|
+
if (!Number.isNaN(timestamp.getTime())) {
|
|
210
|
+
partitionTables.push({
|
|
211
|
+
tableName: table.tablename,
|
|
212
|
+
timestamp,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return partitionTables
|
|
218
|
+
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
|
|
219
|
+
.slice(0, partitionCount);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Partitions the audit log table by creating a new empty table and
|
|
223
|
+
* archiving the current data.
|
|
224
|
+
*
|
|
225
|
+
* This operation:
|
|
226
|
+
* 1. Creates a new table with the same schema (including indexes/constraints)
|
|
227
|
+
* 2. Renames the current table to an archive table with timestamp suffix
|
|
228
|
+
* 3. Renames the new table to the original table name
|
|
229
|
+
* 4. Recreates the UNION ALL view
|
|
230
|
+
*
|
|
231
|
+
* The entire operation runs in a transaction for atomicity.
|
|
232
|
+
*
|
|
233
|
+
* @returns The name of the archive table and timestamp
|
|
234
|
+
* @example
|
|
235
|
+
* ```typescript
|
|
236
|
+
* const result = await auditLog.partitionTable();
|
|
237
|
+
* console.log(`Archived to: ${result.archiveTableName}`);
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
async partitionTable() {
|
|
241
|
+
const now = new Date();
|
|
242
|
+
const timestamp = now.toISOString().replaceAll(/[.:-]/g, '_').slice(0, 19);
|
|
243
|
+
const archiveTableName = `${this.tableName}_${timestamp}`;
|
|
244
|
+
const newTableName = `${this.tableName}_New`;
|
|
245
|
+
await this.sequelize.transaction(async (transaction) => {
|
|
246
|
+
// Create a new empty table by copying the schema from the existing table
|
|
247
|
+
await this.sequelize.query(`CREATE TABLE "${newTableName}" (LIKE "${this.tableName}" INCLUDING ALL)`, { transaction });
|
|
248
|
+
// Rename current table to archive
|
|
249
|
+
await this.sequelize.query(`ALTER TABLE "${this.tableName}" RENAME TO "${archiveTableName}"`, { transaction });
|
|
250
|
+
// Rename new table to original name
|
|
251
|
+
await this.sequelize.query(`ALTER TABLE "${newTableName}" RENAME TO "${this.tableName}"`, { transaction });
|
|
252
|
+
});
|
|
253
|
+
// Recreate the view to include the new archive
|
|
254
|
+
await this.recreateView();
|
|
255
|
+
return { archiveTableName, timestamp: now };
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Recreates the UNION ALL view that combines the current table with archive partitions.
|
|
259
|
+
*
|
|
260
|
+
* The view includes:
|
|
261
|
+
* - The current (active) table
|
|
262
|
+
* - The last N-1 archive tables (where N = viewPartitionCount)
|
|
263
|
+
*
|
|
264
|
+
* Each row includes a `partition_table` column indicating its source.
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* ```typescript
|
|
268
|
+
* await auditLog.recreateView();
|
|
269
|
+
* // Creates view: StayReservationAuditLogs_View
|
|
270
|
+
* // Combining: StayReservationAuditLogs + last 5 archives
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
async recreateView() {
|
|
274
|
+
const viewName = this.getViewName();
|
|
275
|
+
const archiveTables = await this.getLastNPartitionTables();
|
|
276
|
+
const unionQueries = [
|
|
277
|
+
`SELECT *, '${this.tableName}' as partition_table FROM "${this.tableName}"`,
|
|
278
|
+
];
|
|
279
|
+
for (const table of archiveTables) {
|
|
280
|
+
unionQueries.push(`SELECT *, '${table.tableName}' as partition_table FROM "${table.tableName}"`);
|
|
281
|
+
}
|
|
282
|
+
const viewQuery = `
|
|
283
|
+
CREATE OR REPLACE VIEW "${viewName}" AS
|
|
284
|
+
${unionQueries.join('\nUNION ALL\n')}
|
|
285
|
+
`;
|
|
286
|
+
await this.sequelize.query(viewQuery, { type: sequelize_1.QueryTypes.RAW });
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Drops old partition tables beyond the retention count.
|
|
290
|
+
*
|
|
291
|
+
* This is useful for cleaning up old audit data while maintaining
|
|
292
|
+
* the view with recent partitions.
|
|
293
|
+
*
|
|
294
|
+
* @param retentionCount - Number of archive partitions to keep (default: viewPartitionCount * 2)
|
|
295
|
+
* @returns Array of dropped table names
|
|
296
|
+
*/
|
|
297
|
+
async dropOldPartitions(retentionCount) {
|
|
298
|
+
const keepCount = retentionCount ?? this.viewPartitionCount * 2;
|
|
299
|
+
// Get all partitions (more than we want to keep)
|
|
300
|
+
const allPartitions = await this.getLastNPartitionTables(keepCount + 100);
|
|
301
|
+
// Tables to drop are those beyond the retention count
|
|
302
|
+
const tablesToDrop = allPartitions.slice(keepCount);
|
|
303
|
+
const droppedTables = [];
|
|
304
|
+
for (const table of tablesToDrop) {
|
|
305
|
+
await this.sequelize.query(`DROP TABLE IF EXISTS "${table.tableName}"`, {
|
|
306
|
+
type: sequelize_1.QueryTypes.RAW,
|
|
307
|
+
});
|
|
308
|
+
droppedTables.push(table.tableName);
|
|
309
|
+
}
|
|
310
|
+
return droppedTables;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
exports.AtomicAuditLog = AtomicAuditLog;
|
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
* Use this for accurate language detection and future language support planning
|
|
5
5
|
* https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
|
|
6
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* Mapping of ISO 3166-1 alpha-2 country codes to their ACTUAL primary language
|
|
9
|
+
* COUNTRY_TO_LANGUAGE_MAP[countryCode] = languageCode
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* COUNTRY_TO_LANGUAGE_MAP['DE'] // returns 'de'
|
|
13
|
+
* COUNTRY_TO_LANGUAGE_MAP['FR'] // returns 'fr'
|
|
14
|
+
* COUNTRY_TO_LANGUAGE_MAP['US'] // returns 'en'
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
7
17
|
export declare const COUNTRY_TO_LANGUAGE_MAP: Record<string, string>;
|
|
8
18
|
/**
|
|
9
19
|
* Gets the ACTUAL primary language for a given country code
|
|
@@ -68,8 +78,114 @@ export declare const getLocaleFromCountry: (countryCode?: string | null) => stri
|
|
|
68
78
|
/**
|
|
69
79
|
* Comprehensive mapping of language codes to their English names
|
|
70
80
|
* Includes currently supported languages and potential future additions
|
|
81
|
+
* @example
|
|
82
|
+
* ```typescript
|
|
83
|
+
* ENGLISH_LANGUAGE_NAMES['de'] // returns 'German'
|
|
84
|
+
* ENGLISH_LANGUAGE_NAMES['en'] // returns 'English'
|
|
85
|
+
* ENGLISH_LANGUAGE_NAMES['fr'] // returns 'French'
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export declare const ENGLISH_LANGUAGE_NAMES: {
|
|
89
|
+
de: string;
|
|
90
|
+
en: string;
|
|
91
|
+
fr: string;
|
|
92
|
+
it: string;
|
|
93
|
+
es: string;
|
|
94
|
+
nl: string;
|
|
95
|
+
pl: string;
|
|
96
|
+
cs: string;
|
|
97
|
+
hu: string;
|
|
98
|
+
hr: string;
|
|
99
|
+
ro: string;
|
|
100
|
+
el: string;
|
|
101
|
+
pt: string;
|
|
102
|
+
ar: string;
|
|
103
|
+
zh: string;
|
|
104
|
+
ja: string;
|
|
105
|
+
ko: string;
|
|
106
|
+
ru: string;
|
|
107
|
+
tr: string;
|
|
108
|
+
hi: string;
|
|
109
|
+
sv: string;
|
|
110
|
+
no: string;
|
|
111
|
+
da: string;
|
|
112
|
+
fi: string;
|
|
113
|
+
uk: string;
|
|
114
|
+
he: string;
|
|
115
|
+
th: string;
|
|
116
|
+
vi: string;
|
|
117
|
+
id: string;
|
|
118
|
+
ms: string;
|
|
119
|
+
tl: string;
|
|
120
|
+
is: string;
|
|
121
|
+
et: string;
|
|
122
|
+
lv: string;
|
|
123
|
+
lt: string;
|
|
124
|
+
sk: string;
|
|
125
|
+
sl: string;
|
|
126
|
+
sq: string;
|
|
127
|
+
mk: string;
|
|
128
|
+
sr: string;
|
|
129
|
+
bs: string;
|
|
130
|
+
bg: string;
|
|
131
|
+
be: string;
|
|
132
|
+
mt: string;
|
|
133
|
+
ga: string;
|
|
134
|
+
cy: string;
|
|
135
|
+
gd: string;
|
|
136
|
+
fo: string;
|
|
137
|
+
lb: string;
|
|
138
|
+
bn: string;
|
|
139
|
+
ur: string;
|
|
140
|
+
fa: string;
|
|
141
|
+
ps: string;
|
|
142
|
+
ku: string;
|
|
143
|
+
hy: string;
|
|
144
|
+
ka: string;
|
|
145
|
+
az: string;
|
|
146
|
+
kk: string;
|
|
147
|
+
uz: string;
|
|
148
|
+
tk: string;
|
|
149
|
+
ky: string;
|
|
150
|
+
tg: string;
|
|
151
|
+
mn: string;
|
|
152
|
+
km: string;
|
|
153
|
+
lo: string;
|
|
154
|
+
my: string;
|
|
155
|
+
ne: string;
|
|
156
|
+
si: string;
|
|
157
|
+
ta: string;
|
|
158
|
+
dv: string;
|
|
159
|
+
dz: string;
|
|
160
|
+
sw: string;
|
|
161
|
+
am: string;
|
|
162
|
+
ti: string;
|
|
163
|
+
so: string;
|
|
164
|
+
ha: string;
|
|
165
|
+
yo: string;
|
|
166
|
+
ig: string;
|
|
167
|
+
zu: string;
|
|
168
|
+
xh: string;
|
|
169
|
+
af: string;
|
|
170
|
+
mg: string;
|
|
171
|
+
mi: string;
|
|
172
|
+
sm: string;
|
|
173
|
+
to: string;
|
|
174
|
+
fj: string;
|
|
175
|
+
ht: string;
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Type representing all supported language codes (ISO 639-1)
|
|
179
|
+
* This includes all languages that are mapped in the system, providing comprehensive language code support
|
|
180
|
+
* Use this type instead of InternalLanguages for broader language support
|
|
181
|
+
* @example
|
|
182
|
+
* ```typescript
|
|
183
|
+
* const langName: string = ENGLISH_LANGUAGE_NAMES['de']; // returns 'German'
|
|
184
|
+
* const langName2: string = ENGLISH_LANGUAGE_NAMES['en']; // returns 'English'
|
|
185
|
+
* const langName3: string = ENGLISH_LANGUAGE_NAMES['fr']; // returns 'French'
|
|
186
|
+
* ```
|
|
71
187
|
*/
|
|
72
|
-
export
|
|
188
|
+
export type LanguageCode = keyof typeof ENGLISH_LANGUAGE_NAMES;
|
|
73
189
|
/**
|
|
74
190
|
* Gets the language name in English for a given language code
|
|
75
191
|
*
|
|
@@ -82,7 +198,7 @@ export declare const ENGLISH_LANGUAGE_NAMES: Record<string, string>;
|
|
|
82
198
|
* getLanguageName('fr') // returns 'French'
|
|
83
199
|
* ```
|
|
84
200
|
*/
|
|
85
|
-
export declare const getLanguageName: (lang:
|
|
201
|
+
export declare const getLanguageName: (lang: LanguageCode) => (typeof ENGLISH_LANGUAGE_NAMES)[LanguageCode];
|
|
86
202
|
/**
|
|
87
203
|
* Comprehensive mapping of language codes to their native names
|
|
88
204
|
* Includes currently supported languages and potential future additions
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getCountryName = exports.COUNTRY_NAMES = exports.getNativeLanguageName = exports.NATIVE_LANGUAGE_NAMES = exports.getLanguageName = exports.ENGLISH_LANGUAGE_NAMES = exports.getLocaleFromCountry = exports.getLocalesFromCountry = exports.COUNTRY_TO_LOCALES_MAP = exports.getLanguageFromCountry = exports.COUNTRY_TO_LANGUAGE_MAP = void 0;
|
|
4
2
|
/**
|
|
5
3
|
* Comprehensive mapping of ISO 3166-1 alpha-2 country codes to their ACTUAL primary language
|
|
6
4
|
* This maps each country to its real primary language, regardless of whether we support it
|
|
7
5
|
* Use this for accurate language detection and future language support planning
|
|
8
6
|
* https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
|
|
9
7
|
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.getCountryName = exports.COUNTRY_NAMES = exports.getNativeLanguageName = exports.NATIVE_LANGUAGE_NAMES = exports.getLanguageName = exports.ENGLISH_LANGUAGE_NAMES = exports.getLocaleFromCountry = exports.getLocalesFromCountry = exports.COUNTRY_TO_LOCALES_MAP = exports.getLanguageFromCountry = exports.COUNTRY_TO_LANGUAGE_MAP = void 0;
|
|
10
|
+
/**
|
|
11
|
+
* Mapping of ISO 3166-1 alpha-2 country codes to their ACTUAL primary language
|
|
12
|
+
* COUNTRY_TO_LANGUAGE_MAP[countryCode] = languageCode
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* COUNTRY_TO_LANGUAGE_MAP['DE'] // returns 'de'
|
|
16
|
+
* COUNTRY_TO_LANGUAGE_MAP['FR'] // returns 'fr'
|
|
17
|
+
* COUNTRY_TO_LANGUAGE_MAP['US'] // returns 'en'
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
10
20
|
exports.COUNTRY_TO_LANGUAGE_MAP = {
|
|
11
21
|
// Europe
|
|
12
22
|
AD: 'ca', // Andorra - Catalan
|
|
@@ -483,6 +493,12 @@ exports.getLocaleFromCountry = getLocaleFromCountry;
|
|
|
483
493
|
/**
|
|
484
494
|
* Comprehensive mapping of language codes to their English names
|
|
485
495
|
* Includes currently supported languages and potential future additions
|
|
496
|
+
* @example
|
|
497
|
+
* ```typescript
|
|
498
|
+
* ENGLISH_LANGUAGE_NAMES['de'] // returns 'German'
|
|
499
|
+
* ENGLISH_LANGUAGE_NAMES['en'] // returns 'English'
|
|
500
|
+
* ENGLISH_LANGUAGE_NAMES['fr'] // returns 'French'
|
|
501
|
+
* ```
|
|
486
502
|
*/
|
|
487
503
|
exports.ENGLISH_LANGUAGE_NAMES = {
|
|
488
504
|
de: 'German',
|
|
@@ -537,7 +553,7 @@ exports.ENGLISH_LANGUAGE_NAMES = {
|
|
|
537
553
|
gd: 'Scottish Gaelic',
|
|
538
554
|
fo: 'Faroese',
|
|
539
555
|
lb: 'Luxembourgish',
|
|
540
|
-
//
|
|
556
|
+
// Asian & Middle Eastern Languages
|
|
541
557
|
bn: 'Bengali',
|
|
542
558
|
ur: 'Urdu',
|
|
543
559
|
fa: 'Persian',
|
|
@@ -593,7 +609,7 @@ exports.ENGLISH_LANGUAGE_NAMES = {
|
|
|
593
609
|
* ```
|
|
594
610
|
*/
|
|
595
611
|
const getLanguageName = (lang) => {
|
|
596
|
-
return exports.ENGLISH_LANGUAGE_NAMES[lang
|
|
612
|
+
return exports.ENGLISH_LANGUAGE_NAMES[lang];
|
|
597
613
|
};
|
|
598
614
|
exports.getLanguageName = getLanguageName;
|
|
599
615
|
/**
|
|
@@ -613,7 +629,7 @@ exports.NATIVE_LANGUAGE_NAMES = {
|
|
|
613
629
|
hr: 'Hrvatski', // Croatian
|
|
614
630
|
ro: 'Română', // Romanian
|
|
615
631
|
el: 'Ελληνικά', // Greek
|
|
616
|
-
//
|
|
632
|
+
// Additional
|
|
617
633
|
pt: 'Português', // Portuguese
|
|
618
634
|
ar: 'العربية', // Arabic
|
|
619
635
|
zh: '中文', // Chinese
|
|
@@ -622,14 +638,14 @@ exports.NATIVE_LANGUAGE_NAMES = {
|
|
|
622
638
|
ru: 'Русский', // Russian
|
|
623
639
|
tr: 'Türkçe', // Turkish
|
|
624
640
|
hi: 'हिन्दी', // Hindi
|
|
625
|
-
//
|
|
641
|
+
// Nordic & Regional
|
|
626
642
|
sv: 'Svenska', // Swedish
|
|
627
643
|
no: 'Norsk', // Norwegian
|
|
628
644
|
da: 'Dansk', // Danish
|
|
629
645
|
fi: 'Suomi', // Finnish
|
|
630
646
|
uk: 'Українська', // Ukrainian
|
|
631
647
|
he: 'עברית', // Hebrew
|
|
632
|
-
//
|
|
648
|
+
// Asian Markets
|
|
633
649
|
th: 'ไทย', // Thai
|
|
634
650
|
vi: 'Tiếng Việt', // Vietnamese
|
|
635
651
|
id: 'Bahasa Indonesia', // Indonesian
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Request } from 'express';
|
|
2
|
-
export type InternalLanguages = 'de' | 'en'
|
|
2
|
+
export type InternalLanguages = 'de' | 'en';
|
|
3
3
|
export declare const DEFAULT_LANGUAGE_WHEN_NO_HEADER = "de";
|
|
4
4
|
export declare const DEFAULT_LANGUAGE_WHEN_HEADER = "en";
|
|
5
5
|
export declare const AVAILABLE_LANGUAGES: string[];
|
package/dist/lib/validation.d.ts
CHANGED
|
@@ -86,16 +86,16 @@ declare const z: {
|
|
|
86
86
|
thumbprint: zod.ZodString;
|
|
87
87
|
timestamp: zod.ZodString;
|
|
88
88
|
}, "strip", zod.ZodTypeAny, {
|
|
89
|
-
kid: string;
|
|
90
89
|
timestamp: string;
|
|
90
|
+
kid: string;
|
|
91
91
|
signature: string;
|
|
92
92
|
certificateType: string;
|
|
93
93
|
country: string;
|
|
94
94
|
rawData: string;
|
|
95
95
|
thumbprint: string;
|
|
96
96
|
}, {
|
|
97
|
-
kid: string;
|
|
98
97
|
timestamp: string;
|
|
98
|
+
kid: string;
|
|
99
99
|
signature: string;
|
|
100
100
|
certificateType: string;
|
|
101
101
|
country: string;
|
|
@@ -104,8 +104,8 @@ declare const z: {
|
|
|
104
104
|
}>, "many">;
|
|
105
105
|
}, "strip", zod.ZodTypeAny, {
|
|
106
106
|
certificates: {
|
|
107
|
-
kid: string;
|
|
108
107
|
timestamp: string;
|
|
108
|
+
kid: string;
|
|
109
109
|
signature: string;
|
|
110
110
|
certificateType: string;
|
|
111
111
|
country: string;
|
|
@@ -114,8 +114,8 @@ declare const z: {
|
|
|
114
114
|
}[];
|
|
115
115
|
}, {
|
|
116
116
|
certificates: {
|
|
117
|
-
kid: string;
|
|
118
117
|
timestamp: string;
|
|
118
|
+
kid: string;
|
|
119
119
|
signature: string;
|
|
120
120
|
certificateType: string;
|
|
121
121
|
country: string;
|