@socialgouv/matomo-postgres 2.3.12 → 2.3.13

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,214 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { sql } from 'kysely';
11
+ import { db, pool } from './db.js';
12
+ /**
13
+ *
14
+ * @param {Client} client
15
+ * @param {import("types").Event} event
16
+ *
17
+ * @return {Promise<void>}
18
+ */
19
+ export const importEvent = (event) => __awaiter(void 0, void 0, void 0, function* () {
20
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14;
21
+ // Build a sanitized, typed data object to reduce drift and ensure defaults in one place
22
+ const eventData = {
23
+ action_id: (_a = event.action_id) !== null && _a !== void 0 ? _a : '',
24
+ action_timestamp: event.action_timestamp
25
+ ? new Date(event.action_timestamp)
26
+ : new Date(),
27
+ idsite: (_b = event.idsite) !== null && _b !== void 0 ? _b : '',
28
+ idvisit: (_c = event.idvisit) !== null && _c !== void 0 ? _c : '',
29
+ actions: (_d = event.actions) !== null && _d !== void 0 ? _d : null,
30
+ country: (_e = event.country) !== null && _e !== void 0 ? _e : null,
31
+ region: (_f = event.region) !== null && _f !== void 0 ? _f : null,
32
+ city: (_g = event.city) !== null && _g !== void 0 ? _g : null,
33
+ operatingsystemname: (_h = event.operatingsystemname) !== null && _h !== void 0 ? _h : null,
34
+ devicemodel: (_j = event.devicemodel) !== null && _j !== void 0 ? _j : null,
35
+ devicebrand: (_k = event.devicebrand) !== null && _k !== void 0 ? _k : null,
36
+ visitduration: (_l = event.visitduration) !== null && _l !== void 0 ? _l : null,
37
+ dayssincefirstvisit: (_m = event.dayssincefirstvisit) !== null && _m !== void 0 ? _m : null,
38
+ visitortype: (_o = event.visitortype) !== null && _o !== void 0 ? _o : null,
39
+ sitename: (_p = event.sitename) !== null && _p !== void 0 ? _p : null,
40
+ userid: (_q = event.userid) !== null && _q !== void 0 ? _q : null,
41
+ serverdateprettyfirstaction: event.serverdateprettyfirstaction
42
+ ? new Date(event.serverdateprettyfirstaction)
43
+ : null,
44
+ action_type: (_r = event.action_type) !== null && _r !== void 0 ? _r : '',
45
+ action_eventcategory: (_s = event.action_eventcategory) !== null && _s !== void 0 ? _s : '',
46
+ action_eventaction: (_t = event.action_eventaction) !== null && _t !== void 0 ? _t : '',
47
+ action_eventname: (_u = event.action_eventname) !== null && _u !== void 0 ? _u : '',
48
+ action_eventvalue: event.action_eventvalue
49
+ ? Number(event.action_eventvalue)
50
+ : 0,
51
+ action_timespent: (_v = event.action_timespent) !== null && _v !== void 0 ? _v : '0',
52
+ usercustomproperties: (_w = event.usercustomproperties) !== null && _w !== void 0 ? _w : null,
53
+ usercustomdimensions: (_x = event.usercustomdimensions) !== null && _x !== void 0 ? _x : null,
54
+ dimension1: (_y = event.dimension1) !== null && _y !== void 0 ? _y : null,
55
+ dimension2: (_z = event.dimension2) !== null && _z !== void 0 ? _z : null,
56
+ dimension3: (_0 = event.dimension3) !== null && _0 !== void 0 ? _0 : null,
57
+ dimension4: (_1 = event.dimension4) !== null && _1 !== void 0 ? _1 : null,
58
+ dimension5: (_2 = event.dimension5) !== null && _2 !== void 0 ? _2 : null,
59
+ dimension6: (_3 = event.dimension6) !== null && _3 !== void 0 ? _3 : null,
60
+ dimension7: (_4 = event.dimension7) !== null && _4 !== void 0 ? _4 : null,
61
+ dimension8: (_5 = event.dimension8) !== null && _5 !== void 0 ? _5 : null,
62
+ dimension9: (_6 = event.dimension9) !== null && _6 !== void 0 ? _6 : null,
63
+ dimension10: (_7 = event.dimension10) !== null && _7 !== void 0 ? _7 : null,
64
+ action_url: (_8 = event.action_url) !== null && _8 !== void 0 ? _8 : null,
65
+ sitesearchkeyword: (_9 = event.sitesearchkeyword) !== null && _9 !== void 0 ? _9 : null,
66
+ action_title: (_10 = event.action_title) !== null && _10 !== void 0 ? _10 : null,
67
+ visitorid: (_11 = event.visitorid) !== null && _11 !== void 0 ? _11 : null,
68
+ referrertype: (_12 = event.referrertype) !== null && _12 !== void 0 ? _12 : null,
69
+ referrername: (_13 = event.referrername) !== null && _13 !== void 0 ? _13 : null,
70
+ resolution: (_14 = event.resolution) !== null && _14 !== void 0 ? _14 : null
71
+ };
72
+ // Minimal runtime validation for required fields
73
+ if (!eventData.action_id || eventData.action_id.trim().length === 0) {
74
+ throw new Error('importEvent(): action_id is required and cannot be empty');
75
+ }
76
+ if (!(eventData.action_timestamp instanceof Date) ||
77
+ isNaN(eventData.action_timestamp.getTime())) {
78
+ throw new Error('importEvent(): action_timestamp is invalid');
79
+ }
80
+ try {
81
+ if (!pool || typeof pool.connect !== 'function') {
82
+ throw new Error('Database connection pool is invalid or undefined');
83
+ }
84
+ // Keep the stored procedure but centralize mapping to avoid parameter mis-ordering
85
+ yield sql `
86
+ SELECT insert_into_matomo_partitioned(
87
+ ${eventData.action_id},
88
+ ${eventData.action_timestamp},
89
+ ${eventData.idsite},
90
+ ${eventData.idvisit},
91
+ ${eventData.actions},
92
+ ${eventData.country},
93
+ ${eventData.region},
94
+ ${eventData.city},
95
+ ${eventData.operatingsystemname},
96
+ ${eventData.devicemodel},
97
+ ${eventData.devicebrand},
98
+ ${eventData.visitduration},
99
+ ${eventData.dayssincefirstvisit},
100
+ ${eventData.visitortype},
101
+ ${eventData.sitename},
102
+ ${eventData.userid},
103
+ ${eventData.serverdateprettyfirstaction},
104
+ ${eventData.action_type},
105
+ ${eventData.action_eventcategory},
106
+ ${eventData.action_eventaction},
107
+ ${eventData.action_eventname},
108
+ ${eventData.action_eventvalue},
109
+ ${eventData.action_timespent},
110
+ ${eventData.usercustomproperties},
111
+ ${eventData.usercustomdimensions},
112
+ ${eventData.dimension1},
113
+ ${eventData.dimension2},
114
+ ${eventData.dimension3},
115
+ ${eventData.dimension4},
116
+ ${eventData.dimension5},
117
+ ${eventData.dimension6},
118
+ ${eventData.dimension7},
119
+ ${eventData.dimension8},
120
+ ${eventData.dimension9},
121
+ ${eventData.dimension10},
122
+ ${eventData.action_url},
123
+ ${eventData.sitesearchkeyword},
124
+ ${eventData.action_title},
125
+ ${eventData.visitorid},
126
+ ${eventData.referrertype},
127
+ ${eventData.referrername},
128
+ ${eventData.resolution}
129
+ )
130
+ `.execute(db);
131
+ }
132
+ catch (err) {
133
+ // Add context for troubleshooting
134
+ const minimalContext = {
135
+ action_id: eventData.action_id,
136
+ action_timestamp: eventData.action_timestamp,
137
+ idsite: eventData.idsite,
138
+ idvisit: eventData.idvisit
139
+ };
140
+ console.error('importEvent(): failed to insert event', minimalContext);
141
+ // Log error details but avoid exposing sensitive information
142
+ console.error('importEvent(): error', err instanceof Error ? err.message : 'Unknown error');
143
+ throw err;
144
+ }
145
+ });
146
+ const matomoProps = [
147
+ 'idSite',
148
+ 'idVisit',
149
+ 'actions',
150
+ 'country',
151
+ 'region',
152
+ 'city',
153
+ 'operatingSystemName',
154
+ 'deviceModel',
155
+ 'deviceBrand',
156
+ 'visitDuration',
157
+ 'daysSinceFirstVisit',
158
+ 'visitorType',
159
+ 'visitorId',
160
+ 'referrerType',
161
+ 'referrerName',
162
+ 'siteName',
163
+ 'userId',
164
+ 'resolution'
165
+ ];
166
+ /** @type Record<string, (a: import("types/matomo-api").ActionDetail) => string | number> */
167
+ const actionProps = {
168
+ action_type: (action) => action.type,
169
+ action_title: (action) => action.title,
170
+ action_eventcategory: (action) => action.eventCategory,
171
+ action_eventaction: (action) => action.eventAction,
172
+ action_eventname: (action) => action.eventName,
173
+ action_eventvalue: (action) => action.eventValue,
174
+ action_timespent: (action) => action.timeSpent,
175
+ action_timestamp: (action) => new Date(action.timestamp * 1000).toISOString(),
176
+ action_url: (action) => action.url,
177
+ sitesearchkeyword: (action) => action.siteSearchKeyword
178
+ };
179
+ export const getEventsFromMatomoVisit = (matomoVisit) => {
180
+ return matomoVisit.actionDetails.map((actionDetail, actionIndex) => {
181
+ var _a;
182
+ const usercustomproperties = {};
183
+ for (let k = 1; k < 10; k++) {
184
+ const property = (_a = actionDetail.customVariables) === null || _a === void 0 ? void 0 : _a[k];
185
+ if (!property)
186
+ continue; // max 10 custom variables
187
+ //@ts-expect-error implicit any type
188
+ usercustomproperties[property[`customVariableName${k}`]] =
189
+ //@ts-expect-error implicit any type
190
+ property[`customVariableValue${k}`];
191
+ }
192
+ /** @type {Record<string, string>} */
193
+ const usercustomdimensions = {};
194
+ for (let k = 1; k < 11; k++) {
195
+ const dimension = `dimension${k}`;
196
+ //@ts-expect-error implicit any type
197
+ const value = actionDetail[dimension] || matomoVisit[dimension];
198
+ if (!value)
199
+ continue; // max 10 custom variables
200
+ //@ts-expect-error implicit any type
201
+ usercustomdimensions[dimension] = value;
202
+ }
203
+ const event = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, matomoProps.reduce((a, prop) => (Object.assign(Object.assign({}, a), { [prop.toLowerCase()]: matomoVisit[prop] })), {})), { serverdateprettyfirstaction: new Date((matomoVisit.firstActionTimestamp || 0) * 1000).toISOString() }), Object.keys(actionProps).reduce((a, prop) => (Object.assign(Object.assign({}, a), { [prop.toLowerCase()]: actionProps[prop](actionDetail) })), {
204
+ action_id: `${matomoVisit.idVisit}_${actionIndex}`
205
+ })), {
206
+ // custom variables
207
+ usercustomproperties,
208
+ // custom dimensions
209
+ // We keep both for backwards compatibility.
210
+ // Current implementation is flat with one column for each dimension.
211
+ usercustomdimensions }), usercustomdimensions);
212
+ return event;
213
+ });
214
+ };
package/dist/index.js ADDED
@@ -0,0 +1,107 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { addDays } from 'date-fns';
11
+ import startDebug from 'debug';
12
+ import { sql } from 'kysely';
13
+ import { DESTINATION_TABLE, INITIAL_OFFSET, MATOMO_KEY, MATOMO_SITE, MATOMO_URL } from './config.js';
14
+ import { db } from './db.js';
15
+ import { importDate } from './importDate.js';
16
+ import PiwikClient from './PiwikClient.js';
17
+ const debug = startDebug('index');
18
+ function run(date) {
19
+ return __awaiter(this, void 0, void 0, function* () {
20
+ console.log(`🚀 Starting data import process`);
21
+ debug('run, date=' + date);
22
+ console.log(`🔗 Initializing Matomo client`);
23
+ console.log(` - Matomo URL: ${MATOMO_URL}`);
24
+ console.log(` - Matomo Site ID: ${MATOMO_SITE}`);
25
+ const piwik = new PiwikClient(MATOMO_URL, MATOMO_KEY);
26
+ console.log(`📅 Determining reference date for import...`);
27
+ // priority:
28
+ // - optional parameter date
29
+ // - STARTDATE (if FORCE_STARTDATE is true)
30
+ // - last event in the table
31
+ // - STARTDATE (if FORCE_STARTDATE is false)
32
+ // - today
33
+ let referenceDate;
34
+ if (!referenceDate && date) {
35
+ referenceDate = new Date(date);
36
+ console.log(`✅ Using provided date parameter: ${referenceDate.toISOString()}`);
37
+ }
38
+ // Check FORCE_STARTDATE mode first
39
+ const forceStartDate = process.env.FORCE_STARTDATE === 'true';
40
+ if (!referenceDate && forceStartDate && process.env.STARTDATE) {
41
+ referenceDate = new Date(process.env.STARTDATE);
42
+ console.log(`✅ FORCE_STARTDATE enabled - Using STARTDATE environment variable: ${referenceDate.toISOString()}`);
43
+ }
44
+ // Only query database if not forcing STARTDATE
45
+ if (!referenceDate && !forceStartDate) {
46
+ console.log(`🔍 Looking for last event in database...`);
47
+ referenceDate = yield findLastEventInMatomo(db);
48
+ if (referenceDate) {
49
+ console.log(`✅ Found last event, starting from: ${referenceDate.toISOString()}`);
50
+ }
51
+ else {
52
+ console.log(`ℹ️ No previous events found in database`);
53
+ }
54
+ }
55
+ // Fallback to STARTDATE if database had no results
56
+ if (!referenceDate && process.env.STARTDATE) {
57
+ referenceDate = new Date(process.env.STARTDATE);
58
+ console.log(`✅ Using STARTDATE environment variable: ${referenceDate.toISOString()}`);
59
+ }
60
+ if (!referenceDate) {
61
+ referenceDate = new Date(new Date().getTime() - +INITIAL_OFFSET * 24 * 60 * 60 * 1000);
62
+ console.log(`✅ Using default offset (${INITIAL_OFFSET} days ago): ${referenceDate.toISOString()}`);
63
+ }
64
+ const endDate = new Date(new Date().getTime() + 24 * 60 * 60 * 1000);
65
+ console.log(`📊 Import date range determined:`);
66
+ console.log(` - Start date: ${referenceDate.toISOString()}`);
67
+ console.log(` - End date: ${endDate.toISOString()}`);
68
+ debug(`import starting at : ${referenceDate.toISOString()}`);
69
+ console.log(`🔄 Starting sequential import for each date...`);
70
+ let daysProcessed = 0;
71
+ let totalEventsImported = 0;
72
+ for (let current = new Date(referenceDate); current <= endDate; current = addDays(current, 1)) {
73
+ daysProcessed += 1;
74
+ const dayIso = current.toISOString().split('T')[0];
75
+ console.log(`📅 Processing day ${daysProcessed}: ${dayIso}`);
76
+ const result = yield importDate(piwik.api.bind(piwik), current);
77
+ totalEventsImported += result.eventsImported;
78
+ console.log(`✅ Finished ${dayIso}: pages=${result.pages} visits=${result.visitsFetched} events=${result.eventsImported}`);
79
+ }
80
+ console.log(`✅ Import process completed`);
81
+ console.log(`📈 Summary:`);
82
+ console.log(` - Days processed: ${daysProcessed}`);
83
+ console.log(` - Total events imported: ${totalEventsImported}`);
84
+ debug('close');
85
+ return {
86
+ daysProcessed,
87
+ eventsImportedTotal: totalEventsImported
88
+ };
89
+ });
90
+ }
91
+ function findLastEventInMatomo(db) {
92
+ return __awaiter(this, void 0, void 0, function* () {
93
+ const latest = yield db
94
+ .selectFrom(DESTINATION_TABLE)
95
+ .select(sql `action_timestamp at time zone 'UTC'`.as('action_timestamp'))
96
+ .orderBy('action_timestamp', 'desc')
97
+ .limit(1)
98
+ .executeTakeFirst();
99
+ if (latest) {
100
+ // check from the day before just to be sure we have all events
101
+ const date = new Date(new Date(latest.action_timestamp).getTime() - 2 * 24 * 60 * 60 * 1000);
102
+ return date;
103
+ }
104
+ return null;
105
+ });
106
+ }
107
+ export default run;
@@ -0,0 +1,41 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { promises as fs } from 'fs';
11
+ import { FileMigrationProvider, Migrator } from 'kysely';
12
+ import * as path from 'path';
13
+ import { db } from './db.js';
14
+ function migrateDown() {
15
+ return __awaiter(this, void 0, void 0, function* () {
16
+ const migrator = new Migrator({
17
+ db,
18
+ provider: new FileMigrationProvider({
19
+ fs,
20
+ path,
21
+ migrationFolder: path.join(path.dirname(new URL(import.meta.url).pathname), 'migrations')
22
+ })
23
+ });
24
+ const { error, results } = yield migrator.migrateDown();
25
+ results === null || results === void 0 ? void 0 : results.forEach((it) => {
26
+ if (it.status === 'Success') {
27
+ console.log(`down migration "${it.migrationName}" was executed successfully`);
28
+ }
29
+ else if (it.status === 'Error') {
30
+ console.error(`failed to execute down migration "${it.migrationName}"`);
31
+ }
32
+ });
33
+ if (error) {
34
+ console.error('failed to down migrate');
35
+ console.error(error);
36
+ process.exit(1);
37
+ }
38
+ yield db.destroy();
39
+ });
40
+ }
41
+ migrateDown();
@@ -0,0 +1,67 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { promises as fs } from 'fs';
11
+ import { FileMigrationProvider, Migrator } from 'kysely';
12
+ import * as path from 'path';
13
+ import { MATOMO_TABLE_NAME } from './config.js';
14
+ import { db } from './db.js';
15
+ function migrateToLatest() {
16
+ return __awaiter(this, void 0, void 0, function* () {
17
+ console.log(`Starting migrate to latest`);
18
+ try {
19
+ const migrator = new Migrator({
20
+ db,
21
+ provider: new FileMigrationProvider({
22
+ fs,
23
+ path,
24
+ migrationFolder: path.join(path.dirname(new URL(import.meta.url).pathname), 'migrations')
25
+ }),
26
+ // allow to have mutliple migratable instances in a single schema
27
+ migrationTableName: `${MATOMO_TABLE_NAME}_migration`,
28
+ migrationLockTableName: `${MATOMO_TABLE_NAME}_migration_lock`
29
+ });
30
+ const { error, results } = yield migrator.migrateToLatest();
31
+ results === null || results === void 0 ? void 0 : results.forEach((it) => {
32
+ if (it.status === 'Success') {
33
+ console.log(`migration "${it.migrationName}" was executed successfully`);
34
+ }
35
+ else if (it.status === 'Error') {
36
+ console.error(`failed to execute migration "${it.migrationName}"`);
37
+ }
38
+ });
39
+ if (error) {
40
+ console.error('failed to migrate');
41
+ console.error(error);
42
+ process.exit(1);
43
+ }
44
+ else if (!(results === null || results === void 0 ? void 0 : results.length)) {
45
+ console.log('No migration to run');
46
+ }
47
+ }
48
+ catch (uncaughtError) {
49
+ console.error('UNCAUGHT ERROR during migration:');
50
+ console.error('Error message:', uncaughtError instanceof Error
51
+ ? uncaughtError.message
52
+ : String(uncaughtError));
53
+ console.error('Error stack:', uncaughtError instanceof Error
54
+ ? uncaughtError.stack
55
+ : 'No stack trace available');
56
+ console.error('Full error object:', uncaughtError);
57
+ process.exit(1);
58
+ }
59
+ });
60
+ }
61
+ export default migrateToLatest;
62
+ export function startMigration() {
63
+ return __awaiter(this, void 0, void 0, function* () {
64
+ yield migrateToLatest();
65
+ // Don't destroy the db connection here since the main application will need it
66
+ });
67
+ }
@@ -0,0 +1,65 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { sql } from 'kysely';
11
+ const MATOMO_TABLE_NAME = process.env.MATOMO_TABLE_NAME || 'matomo';
12
+ export function up(db) {
13
+ return __awaiter(this, void 0, void 0, function* () {
14
+ yield db.schema
15
+ .createTable(MATOMO_TABLE_NAME)
16
+ .ifNotExists()
17
+ .addColumn('action_id', 'text', (col) => col.unique().notNull())
18
+ .addColumn('idsite', 'text')
19
+ .addColumn('idvisit', 'text')
20
+ .addColumn('actions', 'text')
21
+ .addColumn('country', 'text')
22
+ .addColumn('region', 'text')
23
+ .addColumn('city', 'text')
24
+ .addColumn('operatingsystemname', 'text')
25
+ .addColumn('devicemodel', 'text')
26
+ .addColumn('devicebrand', 'text')
27
+ .addColumn('visitduration', 'text')
28
+ .addColumn('dayssincefirstvisit', 'text')
29
+ .addColumn('visitortype', 'text')
30
+ .addColumn('sitename', 'text')
31
+ .addColumn('userid', 'text')
32
+ .addColumn('serverdateprettyfirstaction', 'date')
33
+ .addColumn('action_type', 'text')
34
+ .addColumn('action_eventcategory', 'text')
35
+ .addColumn('action_eventaction', 'text')
36
+ .addColumn('action_eventname', 'text')
37
+ .addColumn('action_eventvalue', 'numeric')
38
+ .addColumn('action_timespent', 'text')
39
+ .addColumn('action_timestamp', 'timestamptz', (col) => col.defaultTo(sql `now()`))
40
+ .addColumn('usercustomproperties', 'json')
41
+ .addColumn('usercustomdimensions', 'json')
42
+ .addColumn('dimension1', 'text')
43
+ .addColumn('dimension2', 'text')
44
+ .addColumn('dimension3', 'text')
45
+ .addColumn('dimension4', 'text')
46
+ .addColumn('dimension5', 'text')
47
+ .addColumn('dimension6', 'text')
48
+ .addColumn('dimension7', 'text')
49
+ .addColumn('dimension8', 'text')
50
+ .addColumn('dimension9', 'text')
51
+ .addColumn('dimension10', 'text')
52
+ .addColumn('action_url', 'text')
53
+ .addColumn('sitesearchkeyword', 'text')
54
+ .addColumn('action_title', 'text')
55
+ .addColumn('visitorid', 'text')
56
+ .addColumn('referrertype', 'text')
57
+ .addColumn('referrername', 'text')
58
+ .execute();
59
+ });
60
+ }
61
+ export function down(db) {
62
+ return __awaiter(this, void 0, void 0, function* () {
63
+ yield db.schema.dropTable(MATOMO_TABLE_NAME).execute();
64
+ });
65
+ }
@@ -0,0 +1,100 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { sql } from 'kysely';
11
+ const MATOMO_TABLE_NAME = process.env.MATOMO_TABLE_NAME || 'matomo';
12
+ const indexes = [
13
+ {
14
+ name: 'idx_action_eventaction_matomo',
15
+ columns: ['action_eventaction']
16
+ },
17
+ {
18
+ name: 'idx_action_eventcategory_matomo',
19
+ columns: ['action_eventcategory']
20
+ },
21
+ {
22
+ name: 'idx_action_id',
23
+ columns: ['action_id']
24
+ },
25
+ {
26
+ name: 'idx_action_timestamp_matomo',
27
+ columns: ['action_timestamp']
28
+ },
29
+ {
30
+ name: 'idx_action_type_matomo',
31
+ columns: ['action_type']
32
+ },
33
+ {
34
+ name: 'idx_actionurl',
35
+ columns: ['action_url']
36
+ },
37
+ {
38
+ name: 'idx_dimension1',
39
+ columns: ['dimension1']
40
+ },
41
+ {
42
+ name: 'idx_dimension2',
43
+ columns: ['dimension2']
44
+ },
45
+ {
46
+ name: 'idx_dimension3',
47
+ columns: ['dimension3']
48
+ },
49
+ {
50
+ name: 'idx_dimension4',
51
+ columns: ['dimension4']
52
+ },
53
+ {
54
+ name: 'idx_dimension5',
55
+ columns: ['dimension5']
56
+ },
57
+ {
58
+ name: 'idx_idvisit_matomo',
59
+ columns: ['idvisit']
60
+ },
61
+ {
62
+ name: 'idx_region',
63
+ columns: ['region']
64
+ },
65
+ {
66
+ name: 'idx_userid',
67
+ columns: ['userid']
68
+ },
69
+ {
70
+ name: 'idx_visitorid',
71
+ columns: ['visitorid']
72
+ }
73
+ ];
74
+ export function up(db) {
75
+ return __awaiter(this, void 0, void 0, function* () {
76
+ indexes.forEach((index) => __awaiter(this, void 0, void 0, function* () {
77
+ yield db.schema
78
+ .createIndex(index.name)
79
+ .ifNotExists()
80
+ .on(MATOMO_TABLE_NAME)
81
+ .using('btree')
82
+ .columns(index.columns)
83
+ .execute();
84
+ }));
85
+ yield db.schema
86
+ .createIndex('actions_day')
87
+ .ifNotExists()
88
+ .on(MATOMO_TABLE_NAME)
89
+ .expression(sql `date(timezone('UTC', action_timestamp))`)
90
+ .execute();
91
+ });
92
+ }
93
+ export function down(db) {
94
+ return __awaiter(this, void 0, void 0, function* () {
95
+ indexes.forEach((index) => __awaiter(this, void 0, void 0, function* () {
96
+ yield db.schema.dropIndex(index.name).execute();
97
+ }));
98
+ db.schema.dropIndex('actions_day').execute();
99
+ });
100
+ }
@@ -0,0 +1,38 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { sql } from 'kysely';
11
+ const MATOMO_TABLE_NAME = process.env.MATOMO_TABLE_NAME || 'matomo';
12
+ export function up(db) {
13
+ return __awaiter(this, void 0, void 0, function* () {
14
+ // Check if the column already exists before trying to add it
15
+ const columnExists = yield sql `
16
+ SELECT EXISTS (
17
+ SELECT 1
18
+ FROM information_schema.columns
19
+ WHERE table_name = ${MATOMO_TABLE_NAME}
20
+ AND column_name = 'resolution'
21
+ ) as exists
22
+ `.execute(db);
23
+ if (!columnExists.rows[0].exists) {
24
+ yield db.schema
25
+ .alterTable(MATOMO_TABLE_NAME)
26
+ .addColumn('resolution', 'text')
27
+ .execute();
28
+ }
29
+ });
30
+ }
31
+ export function down(db) {
32
+ return __awaiter(this, void 0, void 0, function* () {
33
+ yield db.schema
34
+ .alterTable(MATOMO_TABLE_NAME)
35
+ .dropColumn('resolution')
36
+ .execute();
37
+ });
38
+ }