@socialgouv/matomo-postgres 2.1.0 → 2.2.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.
@@ -0,0 +1,362 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.down = exports.up = void 0;
13
+ const kysely_1 = require("kysely");
14
+ const PARTITIONED_MATOMO_TABLE_NAME = process.env.PARTITIONED_MATOMO_TABLE_NAME || 'matomo_partitioned';
15
+ function up(db) {
16
+ return __awaiter(this, void 0, void 0, function* () {
17
+ // First, create the partitioned table structure as a partitioned table
18
+ yield (0, kysely_1.sql) `
19
+ CREATE TABLE ${kysely_1.sql.id(`${PARTITIONED_MATOMO_TABLE_NAME}`)} (
20
+ action_id text NOT NULL,
21
+ idsite text,
22
+ idvisit text,
23
+ actions text,
24
+ country text,
25
+ region text,
26
+ city text,
27
+ operatingsystemname text,
28
+ devicemodel text,
29
+ devicebrand text,
30
+ visitduration text,
31
+ dayssincefirstvisit text,
32
+ visitortype text,
33
+ sitename text,
34
+ userid text,
35
+ serverdateprettyfirstaction date,
36
+ action_type text,
37
+ action_eventcategory text,
38
+ action_eventaction text,
39
+ action_eventname text,
40
+ action_eventvalue numeric,
41
+ action_timespent text,
42
+ action_timestamp timestamptz NOT NULL DEFAULT now(),
43
+ usercustomproperties json,
44
+ usercustomdimensions json,
45
+ dimension1 text,
46
+ dimension2 text,
47
+ dimension3 text,
48
+ dimension4 text,
49
+ dimension5 text,
50
+ dimension6 text,
51
+ dimension7 text,
52
+ dimension8 text,
53
+ dimension9 text,
54
+ dimension10 text,
55
+ action_url text,
56
+ sitesearchkeyword text,
57
+ action_title text,
58
+ visitorid text,
59
+ referrertype text,
60
+ referrername text,
61
+ resolution text
62
+ ) PARTITION BY RANGE (action_timestamp);
63
+ `.execute(db);
64
+ // Add unique constraint that includes partition key
65
+ yield (0, kysely_1.sql) `
66
+ ALTER TABLE ${kysely_1.sql.id(`${PARTITIONED_MATOMO_TABLE_NAME}`)}
67
+ ADD CONSTRAINT ${kysely_1.sql.id(`${PARTITIONED_MATOMO_TABLE_NAME}_action_id_timestamp_unique`)}
68
+ UNIQUE (action_id, action_timestamp)
69
+ `.execute(db);
70
+ // Create function for automatic weekly partition creation
71
+ yield (0, kysely_1.sql) `
72
+ CREATE OR REPLACE FUNCTION create_weekly_partition_if_not_exists(table_name text, partition_date timestamptz)
73
+ RETURNS void AS $$
74
+ DECLARE
75
+ partition_name text;
76
+ start_date timestamptz;
77
+ end_date timestamptz;
78
+ year_week text;
79
+ BEGIN
80
+ -- Calculate the start of the week (Monday)
81
+ start_date := date_trunc('week', partition_date);
82
+ end_date := start_date + interval '1 week';
83
+
84
+ -- Generate partition name using ISO week format (YYYY-WW)
85
+ year_week := to_char(start_date, 'IYYY') || 'w' || to_char(start_date, 'IW');
86
+ partition_name := table_name || '_' || year_week;
87
+
88
+ -- Check if partition already exists
89
+ IF NOT EXISTS (
90
+ SELECT 1 FROM pg_class c
91
+ JOIN pg_namespace n ON n.oid = c.relnamespace
92
+ WHERE c.relname = partition_name
93
+ AND n.nspname = current_schema()
94
+ ) THEN
95
+ -- Create the partition
96
+ EXECUTE format('CREATE TABLE %I PARTITION OF %I FOR VALUES FROM (%L) TO (%L)',
97
+ partition_name, table_name, start_date, end_date);
98
+
99
+ RAISE NOTICE 'Created partition % for range % to %', partition_name, start_date, end_date;
100
+ END IF;
101
+ END;
102
+ $$ LANGUAGE plpgsql;
103
+ `.execute(db);
104
+ // Create trigger function that automatically creates partitions on insert
105
+ yield (0, kysely_1.sql) `
106
+ CREATE OR REPLACE FUNCTION ${kysely_1.sql.id(`${PARTITIONED_MATOMO_TABLE_NAME}_partition_trigger`)}()
107
+ RETURNS trigger AS $$
108
+ BEGIN
109
+ PERFORM create_weekly_partition_if_not_exists('${kysely_1.sql.raw(PARTITIONED_MATOMO_TABLE_NAME)}', NEW.action_timestamp);
110
+ RETURN NEW;
111
+ END;
112
+ $$ LANGUAGE plpgsql;
113
+ `.execute(db);
114
+ // Create trigger that fires before insert
115
+ yield (0, kysely_1.sql) `
116
+ CREATE TRIGGER ${kysely_1.sql.id(`${PARTITIONED_MATOMO_TABLE_NAME}_auto_partition`)}
117
+ BEFORE INSERT ON ${kysely_1.sql.id(`${PARTITIONED_MATOMO_TABLE_NAME}`)}
118
+ FOR EACH ROW EXECUTE FUNCTION ${kysely_1.sql.id(`${PARTITIONED_MATOMO_TABLE_NAME}_partition_trigger`)}();
119
+ `.execute(db);
120
+ // Create stored procedure for safe insertion with automatic partition creation
121
+ yield (0, kysely_1.sql) `
122
+ CREATE OR REPLACE FUNCTION insert_into_matomo_partitioned(
123
+ p_action_id text,
124
+ p_action_timestamp timestamptz,
125
+ p_idsite text DEFAULT '',
126
+ p_idvisit text DEFAULT '',
127
+ p_actions text DEFAULT NULL,
128
+ p_country text DEFAULT NULL,
129
+ p_region text DEFAULT NULL,
130
+ p_city text DEFAULT NULL,
131
+ p_operatingsystemname text DEFAULT NULL,
132
+ p_devicemodel text DEFAULT NULL,
133
+ p_devicebrand text DEFAULT NULL,
134
+ p_visitduration text DEFAULT NULL,
135
+ p_dayssincefirstvisit text DEFAULT NULL,
136
+ p_visitortype text DEFAULT NULL,
137
+ p_sitename text DEFAULT NULL,
138
+ p_userid text DEFAULT NULL,
139
+ p_serverdateprettyfirstaction date DEFAULT NULL,
140
+ p_action_type text DEFAULT '',
141
+ p_action_eventcategory text DEFAULT '',
142
+ p_action_eventaction text DEFAULT '',
143
+ p_action_eventname text DEFAULT '',
144
+ p_action_eventvalue numeric DEFAULT 0,
145
+ p_action_timespent text DEFAULT '0',
146
+ p_usercustomproperties json DEFAULT NULL,
147
+ p_usercustomdimensions json DEFAULT NULL,
148
+ p_dimension1 text DEFAULT NULL,
149
+ p_dimension2 text DEFAULT NULL,
150
+ p_dimension3 text DEFAULT NULL,
151
+ p_dimension4 text DEFAULT NULL,
152
+ p_dimension5 text DEFAULT NULL,
153
+ p_dimension6 text DEFAULT NULL,
154
+ p_dimension7 text DEFAULT NULL,
155
+ p_dimension8 text DEFAULT NULL,
156
+ p_dimension9 text DEFAULT NULL,
157
+ p_dimension10 text DEFAULT NULL,
158
+ p_action_url text DEFAULT NULL,
159
+ p_sitesearchkeyword text DEFAULT NULL,
160
+ p_action_title text DEFAULT NULL,
161
+ p_visitorid text DEFAULT NULL,
162
+ p_referrertype text DEFAULT NULL,
163
+ p_referrername text DEFAULT NULL,
164
+ p_resolution text DEFAULT NULL
165
+ )
166
+ RETURNS void
167
+ LANGUAGE plpgsql
168
+ SECURITY DEFINER
169
+ AS $$
170
+ BEGIN
171
+ -- Ensure partition exists for the given timestamp
172
+ PERFORM create_weekly_partition_if_not_exists('${kysely_1.sql.raw(PARTITIONED_MATOMO_TABLE_NAME)}', p_action_timestamp);
173
+
174
+ -- Insert the data with conflict handling
175
+ INSERT INTO ${kysely_1.sql.id(PARTITIONED_MATOMO_TABLE_NAME)} (
176
+ action_id,
177
+ action_timestamp,
178
+ idsite,
179
+ idvisit,
180
+ actions,
181
+ country,
182
+ region,
183
+ city,
184
+ operatingsystemname,
185
+ devicemodel,
186
+ devicebrand,
187
+ visitduration,
188
+ dayssincefirstvisit,
189
+ visitortype,
190
+ sitename,
191
+ userid,
192
+ serverdateprettyfirstaction,
193
+ action_type,
194
+ action_eventcategory,
195
+ action_eventaction,
196
+ action_eventname,
197
+ action_eventvalue,
198
+ action_timespent,
199
+ usercustomproperties,
200
+ usercustomdimensions,
201
+ dimension1,
202
+ dimension2,
203
+ dimension3,
204
+ dimension4,
205
+ dimension5,
206
+ dimension6,
207
+ dimension7,
208
+ dimension8,
209
+ dimension9,
210
+ dimension10,
211
+ action_url,
212
+ sitesearchkeyword,
213
+ action_title,
214
+ visitorid,
215
+ referrertype,
216
+ referrername,
217
+ resolution
218
+ ) VALUES (
219
+ p_action_id,
220
+ p_action_timestamp,
221
+ p_idsite,
222
+ p_idvisit,
223
+ p_actions,
224
+ p_country,
225
+ p_region,
226
+ p_city,
227
+ p_operatingsystemname,
228
+ p_devicemodel,
229
+ p_devicebrand,
230
+ p_visitduration,
231
+ p_dayssincefirstvisit,
232
+ p_visitortype,
233
+ p_sitename,
234
+ p_userid,
235
+ p_serverdateprettyfirstaction,
236
+ p_action_type,
237
+ p_action_eventcategory,
238
+ p_action_eventaction,
239
+ p_action_eventname,
240
+ p_action_eventvalue,
241
+ p_action_timespent,
242
+ p_usercustomproperties,
243
+ p_usercustomdimensions,
244
+ p_dimension1,
245
+ p_dimension2,
246
+ p_dimension3,
247
+ p_dimension4,
248
+ p_dimension5,
249
+ p_dimension6,
250
+ p_dimension7,
251
+ p_dimension8,
252
+ p_dimension9,
253
+ p_dimension10,
254
+ p_action_url,
255
+ p_sitesearchkeyword,
256
+ p_action_title,
257
+ p_visitorid,
258
+ p_referrertype,
259
+ p_referrername,
260
+ p_resolution
261
+ )
262
+ ON CONFLICT (action_id, action_timestamp) DO NOTHING;
263
+ END;
264
+ $$;
265
+ `.execute(db);
266
+ // Create indexes on the partitioned table
267
+ const indexes = [
268
+ {
269
+ name: `idx_action_eventaction_${PARTITIONED_MATOMO_TABLE_NAME}`,
270
+ columns: ['action_eventaction']
271
+ },
272
+ {
273
+ name: `idx_action_eventcategory_${PARTITIONED_MATOMO_TABLE_NAME}`,
274
+ columns: ['action_eventcategory']
275
+ },
276
+ {
277
+ name: `idx_action_id_${PARTITIONED_MATOMO_TABLE_NAME}`,
278
+ columns: ['action_id']
279
+ },
280
+ {
281
+ name: `idx_action_timestamp_${PARTITIONED_MATOMO_TABLE_NAME}`,
282
+ columns: ['action_timestamp']
283
+ },
284
+ {
285
+ name: `idx_action_type_${PARTITIONED_MATOMO_TABLE_NAME}`,
286
+ columns: ['action_type']
287
+ },
288
+ {
289
+ name: `idx_actionurl_${PARTITIONED_MATOMO_TABLE_NAME}`,
290
+ columns: ['action_url']
291
+ },
292
+ {
293
+ name: `idx_dimension1_${PARTITIONED_MATOMO_TABLE_NAME}`,
294
+ columns: ['dimension1']
295
+ },
296
+ {
297
+ name: `idx_dimension2_${PARTITIONED_MATOMO_TABLE_NAME}`,
298
+ columns: ['dimension2']
299
+ },
300
+ {
301
+ name: `idx_dimension3_${PARTITIONED_MATOMO_TABLE_NAME}`,
302
+ columns: ['dimension3']
303
+ },
304
+ {
305
+ name: `idx_dimension4_${PARTITIONED_MATOMO_TABLE_NAME}`,
306
+ columns: ['dimension4']
307
+ },
308
+ {
309
+ name: `idx_dimension5_${PARTITIONED_MATOMO_TABLE_NAME}`,
310
+ columns: ['dimension5']
311
+ },
312
+ {
313
+ name: `idx_idvisit_${PARTITIONED_MATOMO_TABLE_NAME}`,
314
+ columns: ['idvisit']
315
+ },
316
+ {
317
+ name: `idx_region_${PARTITIONED_MATOMO_TABLE_NAME}`,
318
+ columns: ['region']
319
+ },
320
+ {
321
+ name: `idx_userid_${PARTITIONED_MATOMO_TABLE_NAME}`,
322
+ columns: ['userid']
323
+ },
324
+ {
325
+ name: `idx_visitorid_${PARTITIONED_MATOMO_TABLE_NAME}`,
326
+ columns: ['visitorid']
327
+ },
328
+ {
329
+ name: `idx_category_timestamp_${PARTITIONED_MATOMO_TABLE_NAME}`,
330
+ columns: ['action_eventcategory', 'action_timestamp']
331
+ }
332
+ ];
333
+ // Create indexes
334
+ for (const index of indexes) {
335
+ yield db.schema
336
+ .createIndex(index.name)
337
+ .on(PARTITIONED_MATOMO_TABLE_NAME)
338
+ .using('btree')
339
+ .columns(index.columns)
340
+ .execute();
341
+ }
342
+ // Create the date-based index
343
+ yield db.schema
344
+ .createIndex(`actions_day_${PARTITIONED_MATOMO_TABLE_NAME}`)
345
+ .on(PARTITIONED_MATOMO_TABLE_NAME)
346
+ .expression((0, kysely_1.sql) `date(timezone('UTC', action_timestamp))`)
347
+ .execute();
348
+ });
349
+ }
350
+ exports.up = up;
351
+ function down(db) {
352
+ return __awaiter(this, void 0, void 0, function* () {
353
+ // Drop trigger and function
354
+ yield (0, kysely_1.sql) `DROP TRIGGER IF EXISTS ${kysely_1.sql.id(`${PARTITIONED_MATOMO_TABLE_NAME}_auto_partition`)} ON ${kysely_1.sql.id(PARTITIONED_MATOMO_TABLE_NAME)}`.execute(db);
355
+ yield (0, kysely_1.sql) `DROP FUNCTION IF EXISTS ${kysely_1.sql.id(`${PARTITIONED_MATOMO_TABLE_NAME}_partition_trigger`)}()`.execute(db);
356
+ yield (0, kysely_1.sql) `DROP FUNCTION IF EXISTS create_weekly_partition_if_not_exists(text, timestamptz)`.execute(db);
357
+ yield (0, kysely_1.sql) `DROP FUNCTION IF EXISTS insert_into_matomo_partitioned`.execute(db);
358
+ // Drop the partitioned table (this will also drop all partitions)
359
+ yield db.schema.dropTable(PARTITIONED_MATOMO_TABLE_NAME).ifExists().execute();
360
+ });
361
+ }
362
+ exports.down = down;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@socialgouv/matomo-postgres",
3
3
  "description": "Extract visitor events from Matomo API and push to Postgres",
4
- "version": "2.1.0",
4
+ "version": "2.2.0",
5
5
  "types": "types/index.d.ts",
6
6
  "license": "Apache-2.0",
7
7
  "main": "dist/index.js",
@@ -21,7 +21,9 @@
21
21
  "build": "tsc",
22
22
  "prepublish": "yarn build",
23
23
  "migrate": "node ./dist/migrate-latest.js",
24
- "test": "jest --verbose"
24
+ "test": "jest --verbose",
25
+ "lint": "eslint .",
26
+ "lint:fix": "eslint . --fix"
25
27
  },
26
28
  "prettier": {
27
29
  "printWidth": 120
@@ -35,11 +37,22 @@
35
37
  "pg": "^8.9.0"
36
38
  },
37
39
  "devDependencies": {
40
+ "@eslint/eslintrc": "^3.3.1",
41
+ "@eslint/js": "^9.31.0",
38
42
  "@types/debug": "^4.1.7",
39
43
  "@types/jest": "^29.4.0",
40
44
  "@types/node": "^18.14.4",
41
45
  "@types/pg": "^8.6.6",
46
+ "@typescript-eslint/eslint-plugin": "^8.37.0",
47
+ "@typescript-eslint/parser": "^8.37.0",
48
+ "eslint": "^9.31.0",
49
+ "eslint-config-prettier": "^10.1.5",
50
+ "eslint-plugin-prettier": "^5.5.1",
51
+ "eslint-plugin-simple-import-sort": "^12.1.1",
52
+ "globals": "^16.3.0",
42
53
  "jest": "^29.4.3",
54
+ "knip": "^5.61.3",
55
+ "prettier": "^3.6.2",
43
56
  "ts-jest": "^29.0.5",
44
57
  "ts-node": "^10.9.1",
45
58
  "typescript": "^4.9.5"