@spinajs/session-provider-db 2.0.489 → 2.0.490
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/lib/cjs/index.d.ts +2 -0
- package/lib/cjs/index.d.ts.map +1 -1
- package/lib/cjs/index.js +33 -1
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/migration-support.d.ts +43 -0
- package/lib/cjs/migration-support.d.ts.map +1 -0
- package/lib/cjs/migration-support.js +97 -0
- package/lib/cjs/migration-support.js.map +1 -0
- package/lib/cjs/migrations/UserSessionDBSqlMigration_2022_06_28_01_20_00.d.ts.map +1 -1
- package/lib/cjs/migrations/UserSessionDBSqlMigration_2022_06_28_01_20_00.js +8 -0
- package/lib/cjs/migrations/UserSessionDBSqlMigration_2022_06_28_01_20_00.js.map +1 -1
- package/lib/cjs/migrations/UserSessionDataJson_2026_07_31_00_00_00.d.ts +101 -0
- package/lib/cjs/migrations/UserSessionDataJson_2026_07_31_00_00_00.d.ts.map +1 -0
- package/lib/cjs/migrations/UserSessionDataJson_2026_07_31_00_00_00.js +144 -0
- package/lib/cjs/migrations/UserSessionDataJson_2026_07_31_00_00_00.js.map +1 -0
- package/lib/cjs/migrations/UserSessionTimestamps_2026_07_31_00_00_01.d.ts +68 -0
- package/lib/cjs/migrations/UserSessionTimestamps_2026_07_31_00_00_01.d.ts.map +1 -0
- package/lib/cjs/migrations/UserSessionTimestamps_2026_07_31_00_00_01.js +95 -0
- package/lib/cjs/migrations/UserSessionTimestamps_2026_07_31_00_00_01.js.map +1 -0
- package/lib/cjs/models/DbSession.d.ts +15 -0
- package/lib/cjs/models/DbSession.d.ts.map +1 -1
- package/lib/cjs/models/DbSession.js.map +1 -1
- package/lib/mjs/index.d.ts +2 -0
- package/lib/mjs/index.d.ts.map +1 -1
- package/lib/mjs/index.js +33 -1
- package/lib/mjs/index.js.map +1 -1
- package/lib/mjs/migration-support.d.ts +43 -0
- package/lib/mjs/migration-support.d.ts.map +1 -0
- package/lib/mjs/migration-support.js +91 -0
- package/lib/mjs/migration-support.js.map +1 -0
- package/lib/mjs/migrations/UserSessionDBSqlMigration_2022_06_28_01_20_00.d.ts.map +1 -1
- package/lib/mjs/migrations/UserSessionDBSqlMigration_2022_06_28_01_20_00.js +8 -0
- package/lib/mjs/migrations/UserSessionDBSqlMigration_2022_06_28_01_20_00.js.map +1 -1
- package/lib/mjs/migrations/UserSessionDataJson_2026_07_31_00_00_00.d.ts +101 -0
- package/lib/mjs/migrations/UserSessionDataJson_2026_07_31_00_00_00.d.ts.map +1 -0
- package/lib/mjs/migrations/UserSessionDataJson_2026_07_31_00_00_00.js +141 -0
- package/lib/mjs/migrations/UserSessionDataJson_2026_07_31_00_00_00.js.map +1 -0
- package/lib/mjs/migrations/UserSessionTimestamps_2026_07_31_00_00_01.d.ts +68 -0
- package/lib/mjs/migrations/UserSessionTimestamps_2026_07_31_00_00_01.d.ts.map +1 -0
- package/lib/mjs/migrations/UserSessionTimestamps_2026_07_31_00_00_01.js +92 -0
- package/lib/mjs/migrations/UserSessionTimestamps_2026_07_31_00_00_01.js.map +1 -0
- package/lib/mjs/models/DbSession.d.ts +15 -0
- package/lib/mjs/models/DbSession.d.ts.map +1 -1
- package/lib/mjs/models/DbSession.js.map +1 -1
- package/lib/tsconfig.cjs.tsbuildinfo +1 -1
- package/lib/tsconfig.mjs.tsbuildinfo +1 -1
- package/package.json +10 -10
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { Migration, OrmMigration } from '@spinajs/orm';
|
|
8
|
+
import { columnNativeType, isMySqlDialect, isTextFamily } from '../migration-support.js';
|
|
9
|
+
/**
|
|
10
|
+
* Converts an EXISTING text-typed `user_sessions.Data` to a native MySQL `json`
|
|
11
|
+
* column, so the read path receives a parsed object graph instead of text.
|
|
12
|
+
*
|
|
13
|
+
* Databases created before the create-path migration
|
|
14
|
+
* (`UserSessionDBSqlMigration_2022_06_28_01_20_00`) declared `json` - i.e. every
|
|
15
|
+
* deployment - carry `Data text NOT NULL`. The create migration returns early
|
|
16
|
+
* when the table is already there, so the type it declares has never reached
|
|
17
|
+
* them. Fresh installs already get `json`. Two shapes of the same table exist in
|
|
18
|
+
* the wild and nothing detected which one you had; this migration ends that.
|
|
19
|
+
*
|
|
20
|
+
* mysql2 parses a json column on the way out, so the read path receives the
|
|
21
|
+
* payload as a live object graph and rebuilds the session `Map` from it directly
|
|
22
|
+
* (`decodeSessionData`). The write path is unchanged - `encodeSessionData` still
|
|
23
|
+
* produces JSON text, which a json column stores verbatim.
|
|
24
|
+
*
|
|
25
|
+
* Everything below is a guard. Read them in order.
|
|
26
|
+
*
|
|
27
|
+
* 1. Table missing: nothing to converge
|
|
28
|
+
* ---------------------------------------
|
|
29
|
+
* It normally IS there - the create migration carries an earlier timestamp and
|
|
30
|
+
* runs first in the same batch - but a connection that hosts only part of the
|
|
31
|
+
* schema must not blow up here.
|
|
32
|
+
*
|
|
33
|
+
* 2. Column state probe: skip unless it is text-family
|
|
34
|
+
* ------------------------------------------------------
|
|
35
|
+
* The previous version of this migration was unconditional, on the argument that
|
|
36
|
+
* `MODIFY` to a type a column already has is a harmless rewrite. True on MySQL,
|
|
37
|
+
* and irrelevant everywhere else: it reached the schema builder's `alterTable()`,
|
|
38
|
+
* which resolves `AlterTableQueryCompiler` - and `orm-mssql` registers no
|
|
39
|
+
* implementation for it, so DI hands back the ABSTRACT class, whose `compile()`
|
|
40
|
+
* does not exist. That throw is recorded as a FAILED migration, and
|
|
41
|
+
* `assertNoFailed()` then refuses to run ANY migration on that connection ever
|
|
42
|
+
* again: the application never boots without manual surgery on the tracking
|
|
43
|
+
* table. Probing the target state first and returning early is the same shape
|
|
44
|
+
* `@spinajs/rbac`'s `RBACRegisteredAt_2026_07_28_00_00_00` already uses, and it
|
|
45
|
+
* makes the no-op case genuinely free rather than merely survivable.
|
|
46
|
+
*
|
|
47
|
+
* An unreadable probe skips too - see `columnNativeType`.
|
|
48
|
+
*
|
|
49
|
+
* 3. Dialect gate: MySQL only
|
|
50
|
+
* ------------------------------
|
|
51
|
+
* `MODIFY` and the `JSON` type are MySQL's. A text-family `Data` on MSSQL
|
|
52
|
+
* (`nvarchar`) or sqlite (`TEXT`) passes guard 2 and would then be handed
|
|
53
|
+
* syntax its server rejects - the same failed-migration outage guard 2 exists to
|
|
54
|
+
* prevent. Those dialects keep a text column and the read path keeps decoding a
|
|
55
|
+
* string, which is exactly what `decodeSessionData` does with it.
|
|
56
|
+
*
|
|
57
|
+
* 4. The wipe
|
|
58
|
+
* -----------
|
|
59
|
+
* Reached only when an actual `text -> json` conversion is about to happen.
|
|
60
|
+
* MySQL validates EVERY existing row during that conversion, and a single
|
|
61
|
+
* malformed / truncated / empty payload (a row from a crashed write, a row
|
|
62
|
+
* written by an older codec, a row truncated by TEXT's 64 KiB limit) fails the
|
|
63
|
+
* statement:
|
|
64
|
+
*
|
|
65
|
+
* ERROR 3140 (22032): Invalid JSON text: "Invalid value." at position 1
|
|
66
|
+
*
|
|
67
|
+
* Emptying the table first makes the conversion unconditionally safe. Sessions
|
|
68
|
+
* are disposable state: the worst a user sees is one forced re-login, and that
|
|
69
|
+
* is now paid ONLY by the deployments actually being converted - a fresh install
|
|
70
|
+
* skips at guard 2 and never deletes anything.
|
|
71
|
+
*
|
|
72
|
+
* `DELETE`, not `TRUNCATE`: truncate is not transactional, is refused outright
|
|
73
|
+
* by some engines for a table involved in foreign keys, and there is no volume
|
|
74
|
+
* here to make the difference worth having.
|
|
75
|
+
*
|
|
76
|
+
* 5. ONE statement, not three
|
|
77
|
+
* ----------------------------
|
|
78
|
+
* `alterTable()` compiles one `ICompilerOutput` per modified column and
|
|
79
|
+
* `SqlDriver.execute` wraps the array in `Promise.all` - three concurrent
|
|
80
|
+
* `ALTER TABLE`s on three pooled connections against one table. A partial
|
|
81
|
+
* failure leaves the table half-converted AND the migration marked failed. The
|
|
82
|
+
* raw escape hatch (`schema().raw`, compiled by `SqlRawSchemaQueryCompiler`,
|
|
83
|
+
* registered by every SQL driver via `orm-sql`) sends exactly one statement.
|
|
84
|
+
* The `CreatedAt`/`Expiration` widening that used to ride along here now lives
|
|
85
|
+
* in its own migration (`UserSessionTimestamps_2026_07_31_00_00_01`), because it
|
|
86
|
+
* is a cast that cannot fail and must stay independently applicable.
|
|
87
|
+
*/
|
|
88
|
+
let UserSessionDataJson_2026_07_31_00_00_00 = class UserSessionDataJson_2026_07_31_00_00_00 extends OrmMigration {
|
|
89
|
+
async up(connection) {
|
|
90
|
+
if (!(await connection.schema().tableExists('user_sessions'))) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const dataType = await columnNativeType(connection, 'user_sessions', 'Data');
|
|
94
|
+
// already `json` (fresh install / already converged), unreadable, or a type
|
|
95
|
+
// nobody anticipated - all three mean "leave it alone"
|
|
96
|
+
if (dataType === null || !isTextFamily(dataType)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (!isMySqlDialect(connection)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
await connection.schema().raw('DELETE FROM user_sessions');
|
|
103
|
+
// Identifiers are backtick-quoted: this statement is MySQL-only by
|
|
104
|
+
// construction (guard 3), so portability of the quoting style is not a
|
|
105
|
+
// constraint, and quoting removes any question about `Data` being a MySQL
|
|
106
|
+
// keyword. `NOT NULL` is restated because MODIFY replaces the WHOLE column
|
|
107
|
+
// definition - omitting it would silently make the column nullable.
|
|
108
|
+
await connection.schema().raw('ALTER TABLE `user_sessions` MODIFY `Data` JSON NOT NULL');
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Restores the text column. The sessions `up()` deleted are gone for good -
|
|
112
|
+
* that part is not reversible and nothing pretends otherwise - but the SHAPE
|
|
113
|
+
* is, and a package rollback that leaves a `json` column behind for a
|
|
114
|
+
* `2.0.488` reader is its own outage.
|
|
115
|
+
*
|
|
116
|
+
* Guarded by the inverse probe: only a column that is currently `json` is
|
|
117
|
+
* touched, so running `down()` twice, or against a database this migration
|
|
118
|
+
* never converted, does nothing.
|
|
119
|
+
*
|
|
120
|
+
* `TEXT` because `TEXT` is what the deployed databases had. Note this is a
|
|
121
|
+
* NARROWING cast - a stored payload above TEXT's 64 KiB limit would fail the
|
|
122
|
+
* statement in strict mode. That is acceptable here in a way it would not be
|
|
123
|
+
* in `up()`: `down()` runs only on an explicit, interactive rollback, never at
|
|
124
|
+
* boot, so a failure is loud and immediate rather than an unbootable service.
|
|
125
|
+
*/
|
|
126
|
+
async down(connection) {
|
|
127
|
+
if (!(await connection.schema().tableExists('user_sessions'))) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const dataType = await columnNativeType(connection, 'user_sessions', 'Data');
|
|
131
|
+
if (dataType !== 'json' || !isMySqlDialect(connection)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
await connection.schema().raw('ALTER TABLE `user_sessions` MODIFY `Data` TEXT NOT NULL');
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
UserSessionDataJson_2026_07_31_00_00_00 = __decorate([
|
|
138
|
+
Migration('session-provider-connection')
|
|
139
|
+
], UserSessionDataJson_2026_07_31_00_00_00);
|
|
140
|
+
export { UserSessionDataJson_2026_07_31_00_00_00 };
|
|
141
|
+
//# sourceMappingURL=UserSessionDataJson_2026_07_31_00_00_00.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UserSessionDataJson_2026_07_31_00_00_00.js","sourceRoot":"","sources":["../../../src/migrations/UserSessionDataJson_2026_07_31_00_00_00.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,SAAS,EAAE,YAAY,EAAa,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAEzF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8EG;AAEI,IAAM,uCAAuC,GAA7C,MAAM,uCAAwC,SAAQ,YAAY;IAChE,KAAK,CAAC,EAAE,CAAC,UAAqB;QACnC,IAAI,CAAC,CAAC,MAAM,UAAU,CAAC,MAAM,EAAE,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC;YAC9D,OAAO;QACT,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,gBAAgB,CAAC,UAAU,EAAE,eAAe,EAAE,MAAM,CAAC,CAAC;QAE7E,4EAA4E;QAC5E,uDAAuD;QACvD,IAAI,QAAQ,KAAK,IAAI,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE,CAAC;YACjD,OAAO;QACT,CAAC;QAED,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,CAAC;YAChC,OAAO;QACT,CAAC;QAED,MAAM,UAAU,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;QAE3D,mEAAmE;QACnE,uEAAuE;QACvE,0EAA0E;QAC1E,2EAA2E;QAC3E,oEAAoE;QACpE,MAAM,UAAU,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,yDAAyD,CAAC,CAAC;IAC3F,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACI,KAAK,CAAC,IAAI,CAAC,UAAqB;QACrC,IAAI,CAAC,CAAC,MAAM,UAAU,CAAC,MAAM,EAAE,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC;YAC9D,OAAO;QACT,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,gBAAgB,CAAC,UAAU,EAAE,eAAe,EAAE,MAAM,CAAC,CAAC;QAE7E,IAAI,QAAQ,KAAK,MAAM,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,CAAC;YACvD,OAAO;QACT,CAAC;QAED,MAAM,UAAU,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,yDAAyD,CAAC,CAAC;IAC3F,CAAC;CACF,CAAA;AAzDY,uCAAuC;IADnD,SAAS,CAAC,6BAA6B,CAAC;GAC5B,uCAAuC,CAyDnD"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { OrmMigration, OrmDriver } from '@spinajs/orm';
|
|
2
|
+
/**
|
|
3
|
+
* Widens `user_sessions.CreatedAt` / `Expiration` from `date` to `datetime`.
|
|
4
|
+
*
|
|
5
|
+
* !! This is a correctness fix in its own right and has nothing to do with the
|
|
6
|
+
* JSON conversion it used to be bundled with. It is a separate migration
|
|
7
|
+
* precisely so it can be applied, skipped, reviewed and reverted on its own.
|
|
8
|
+
*
|
|
9
|
+
* The bug
|
|
10
|
+
* -------
|
|
11
|
+
* A MySQL `date` column stores only the calendar day - MySQL truncates the time
|
|
12
|
+
* component on write, silently:
|
|
13
|
+
*
|
|
14
|
+
* INSERT INTO probe VALUES ('2026-07-31 23:45:12', '2026-07-31 23:45:12');
|
|
15
|
+
* asDate asDateTime
|
|
16
|
+
* 2026-07-31 2026-07-31 23:45:12
|
|
17
|
+
*
|
|
18
|
+
* So on a deployed database a session's `Expiration` has no time of day: a
|
|
19
|
+
* session created at 23:45 with a 60-minute sliding TTL, due to expire at
|
|
20
|
+
* 00:45:12, is stored as expiring at 00:00:00 and the cleanup sweep kills it
|
|
21
|
+
* ~45 minutes early. Sliding expiration, `SessionProvider.isExpired`,
|
|
22
|
+
* `cleanupExpired` and `touch` all operate on that truncated value. `DbSession`
|
|
23
|
+
* declares both fields as luxon `DateTime` and the create path has declared both
|
|
24
|
+
* as `dateTime` since 2024; `datetime` is the type the code has always assumed
|
|
25
|
+
* it had.
|
|
26
|
+
*
|
|
27
|
+
* Why this one is safe where the JSON one was not
|
|
28
|
+
* -----------------------------------------------
|
|
29
|
+
* `date -> datetime` is a WIDENING cast. Every existing value is representable
|
|
30
|
+
* in the target type (midnight of the stored day), so unlike the `text -> json`
|
|
31
|
+
* conversion it cannot fail on row content and needs no wipe. Nothing is
|
|
32
|
+
* deleted here.
|
|
33
|
+
*
|
|
34
|
+
* Guards mirror the JSON migration: skip when the table is absent, skip unless
|
|
35
|
+
* `CreatedAt` is currently exactly `date`, skip on any non-MySQL dialect
|
|
36
|
+
* (`MODIFY` is MySQL syntax; a failed migration at boot bricks every later
|
|
37
|
+
* migration on the connection through `assertNoFailed()`). `CreatedAt` alone is
|
|
38
|
+
* probed - the two columns were created by the same statement in every database
|
|
39
|
+
* that has the old shape, and a `date`/`datetime` split between them has never
|
|
40
|
+
* existed.
|
|
41
|
+
*
|
|
42
|
+
* ONE statement with two `MODIFY` clauses, via the raw escape hatch, rather than
|
|
43
|
+
* `alterTable()`: the builder compiles one statement per column and
|
|
44
|
+
* `SqlDriver.execute` runs them through `Promise.all`, i.e. two concurrent
|
|
45
|
+
* `ALTER TABLE`s on the same table from two pooled connections.
|
|
46
|
+
*
|
|
47
|
+
* Nullability is restated verbatim from the create path - `MODIFY` replaces the
|
|
48
|
+
* whole column definition, and `CreatedAt` is `notNull()` there while
|
|
49
|
+
* `Expiration` is nullable (a never-expiring session persists a NULL).
|
|
50
|
+
*/
|
|
51
|
+
export declare class UserSessionTimestamps_2026_07_31_00_00_01 extends OrmMigration {
|
|
52
|
+
up(connection: OrmDriver): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Deliberately a no-op, and this is a decision rather than an omission.
|
|
55
|
+
*
|
|
56
|
+
* `up()` widened `date -> datetime`. Reversing it would be a NARROWING cast
|
|
57
|
+
* that silently truncates the time-of-day off every stored row - it destroys
|
|
58
|
+
* data, and it re-introduces the exact expiry bug this migration exists to
|
|
59
|
+
* fix, on a schema whose model and create path have declared `dateTime` for
|
|
60
|
+
* years. A `datetime` column is also perfectly readable by every version of
|
|
61
|
+
* this package, so a rollback has nothing to gain from shrinking it.
|
|
62
|
+
*
|
|
63
|
+
* (The JSON migration's `down()` is implemented, because there the old readers
|
|
64
|
+
* genuinely cannot cope with the new column type. That is not the case here.)
|
|
65
|
+
*/
|
|
66
|
+
down(_connection: OrmDriver): Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=UserSessionTimestamps_2026_07_31_00_00_01.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UserSessionTimestamps_2026_07_31_00_00_01.d.ts","sourceRoot":"","sources":["../../../src/migrations/UserSessionTimestamps_2026_07_31_00_00_01.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAGlE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,qBACa,yCAA0C,SAAQ,YAAY;IAC5D,EAAE,CAAC,UAAU,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBrD;;;;;;;;;;;;OAYG;IAEU,IAAI,CAAC,WAAW,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;CACzD"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { Migration, OrmMigration } from '@spinajs/orm';
|
|
8
|
+
import { columnNativeType, isMySqlDialect } from '../migration-support.js';
|
|
9
|
+
/**
|
|
10
|
+
* Widens `user_sessions.CreatedAt` / `Expiration` from `date` to `datetime`.
|
|
11
|
+
*
|
|
12
|
+
* !! This is a correctness fix in its own right and has nothing to do with the
|
|
13
|
+
* JSON conversion it used to be bundled with. It is a separate migration
|
|
14
|
+
* precisely so it can be applied, skipped, reviewed and reverted on its own.
|
|
15
|
+
*
|
|
16
|
+
* The bug
|
|
17
|
+
* -------
|
|
18
|
+
* A MySQL `date` column stores only the calendar day - MySQL truncates the time
|
|
19
|
+
* component on write, silently:
|
|
20
|
+
*
|
|
21
|
+
* INSERT INTO probe VALUES ('2026-07-31 23:45:12', '2026-07-31 23:45:12');
|
|
22
|
+
* asDate asDateTime
|
|
23
|
+
* 2026-07-31 2026-07-31 23:45:12
|
|
24
|
+
*
|
|
25
|
+
* So on a deployed database a session's `Expiration` has no time of day: a
|
|
26
|
+
* session created at 23:45 with a 60-minute sliding TTL, due to expire at
|
|
27
|
+
* 00:45:12, is stored as expiring at 00:00:00 and the cleanup sweep kills it
|
|
28
|
+
* ~45 minutes early. Sliding expiration, `SessionProvider.isExpired`,
|
|
29
|
+
* `cleanupExpired` and `touch` all operate on that truncated value. `DbSession`
|
|
30
|
+
* declares both fields as luxon `DateTime` and the create path has declared both
|
|
31
|
+
* as `dateTime` since 2024; `datetime` is the type the code has always assumed
|
|
32
|
+
* it had.
|
|
33
|
+
*
|
|
34
|
+
* Why this one is safe where the JSON one was not
|
|
35
|
+
* -----------------------------------------------
|
|
36
|
+
* `date -> datetime` is a WIDENING cast. Every existing value is representable
|
|
37
|
+
* in the target type (midnight of the stored day), so unlike the `text -> json`
|
|
38
|
+
* conversion it cannot fail on row content and needs no wipe. Nothing is
|
|
39
|
+
* deleted here.
|
|
40
|
+
*
|
|
41
|
+
* Guards mirror the JSON migration: skip when the table is absent, skip unless
|
|
42
|
+
* `CreatedAt` is currently exactly `date`, skip on any non-MySQL dialect
|
|
43
|
+
* (`MODIFY` is MySQL syntax; a failed migration at boot bricks every later
|
|
44
|
+
* migration on the connection through `assertNoFailed()`). `CreatedAt` alone is
|
|
45
|
+
* probed - the two columns were created by the same statement in every database
|
|
46
|
+
* that has the old shape, and a `date`/`datetime` split between them has never
|
|
47
|
+
* existed.
|
|
48
|
+
*
|
|
49
|
+
* ONE statement with two `MODIFY` clauses, via the raw escape hatch, rather than
|
|
50
|
+
* `alterTable()`: the builder compiles one statement per column and
|
|
51
|
+
* `SqlDriver.execute` runs them through `Promise.all`, i.e. two concurrent
|
|
52
|
+
* `ALTER TABLE`s on the same table from two pooled connections.
|
|
53
|
+
*
|
|
54
|
+
* Nullability is restated verbatim from the create path - `MODIFY` replaces the
|
|
55
|
+
* whole column definition, and `CreatedAt` is `notNull()` there while
|
|
56
|
+
* `Expiration` is nullable (a never-expiring session persists a NULL).
|
|
57
|
+
*/
|
|
58
|
+
let UserSessionTimestamps_2026_07_31_00_00_01 = class UserSessionTimestamps_2026_07_31_00_00_01 extends OrmMigration {
|
|
59
|
+
async up(connection) {
|
|
60
|
+
if (!(await connection.schema().tableExists('user_sessions'))) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const createdAtType = await columnNativeType(connection, 'user_sessions', 'CreatedAt');
|
|
64
|
+
// Only the one known-bad state is converted. `datetime` is the target,
|
|
65
|
+
// `timestamp` needs no help, `null` means the probe could not read the
|
|
66
|
+
// table, and anything else is a shape this migration was not written for.
|
|
67
|
+
if (createdAtType !== 'date' || !isMySqlDialect(connection)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
await connection.schema().raw('ALTER TABLE `user_sessions` MODIFY `CreatedAt` DATETIME NOT NULL, MODIFY `Expiration` DATETIME');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Deliberately a no-op, and this is a decision rather than an omission.
|
|
74
|
+
*
|
|
75
|
+
* `up()` widened `date -> datetime`. Reversing it would be a NARROWING cast
|
|
76
|
+
* that silently truncates the time-of-day off every stored row - it destroys
|
|
77
|
+
* data, and it re-introduces the exact expiry bug this migration exists to
|
|
78
|
+
* fix, on a schema whose model and create path have declared `dateTime` for
|
|
79
|
+
* years. A `datetime` column is also perfectly readable by every version of
|
|
80
|
+
* this package, so a rollback has nothing to gain from shrinking it.
|
|
81
|
+
*
|
|
82
|
+
* (The JSON migration's `down()` is implemented, because there the old readers
|
|
83
|
+
* genuinely cannot cope with the new column type. That is not the case here.)
|
|
84
|
+
*/
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
86
|
+
async down(_connection) { }
|
|
87
|
+
};
|
|
88
|
+
UserSessionTimestamps_2026_07_31_00_00_01 = __decorate([
|
|
89
|
+
Migration('session-provider-connection')
|
|
90
|
+
], UserSessionTimestamps_2026_07_31_00_00_01);
|
|
91
|
+
export { UserSessionTimestamps_2026_07_31_00_00_01 };
|
|
92
|
+
//# sourceMappingURL=UserSessionTimestamps_2026_07_31_00_00_01.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UserSessionTimestamps_2026_07_31_00_00_01.js","sourceRoot":"","sources":["../../../src/migrations/UserSessionTimestamps_2026_07_31_00_00_01.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,SAAS,EAAE,YAAY,EAAa,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAE3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAEI,IAAM,yCAAyC,GAA/C,MAAM,yCAA0C,SAAQ,YAAY;IAClE,KAAK,CAAC,EAAE,CAAC,UAAqB;QACnC,IAAI,CAAC,CAAC,MAAM,UAAU,CAAC,MAAM,EAAE,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC;YAC9D,OAAO;QACT,CAAC;QAED,MAAM,aAAa,GAAG,MAAM,gBAAgB,CAAC,UAAU,EAAE,eAAe,EAAE,WAAW,CAAC,CAAC;QAEvF,uEAAuE;QACvE,uEAAuE;QACvE,0EAA0E;QAC1E,IAAI,aAAa,KAAK,MAAM,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,CAAC;YAC5D,OAAO;QACT,CAAC;QAED,MAAM,UAAU,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,gGAAgG,CAAC,CAAC;IAClI,CAAC;IAED;;;;;;;;;;;;OAYG;IACH,gEAAgE;IACzD,KAAK,CAAC,IAAI,CAAC,WAAsB,IAAkB,CAAC;CAC5D,CAAA;AAjCY,yCAAyC;IADrD,SAAS,CAAC,6BAA6B,CAAC;GAC5B,yCAAyC,CAiCrD"}
|
|
@@ -4,6 +4,21 @@ export declare class DbSession extends ModelBase {
|
|
|
4
4
|
SessionId: string;
|
|
5
5
|
CreatedAt: DateTime;
|
|
6
6
|
Expiration: DateTime;
|
|
7
|
+
/**
|
|
8
|
+
* Serialized session payload - see `encodeSessionData` / `decodeSessionData`.
|
|
9
|
+
*
|
|
10
|
+
* Declared as a string because that is what the WRITE path assigns:
|
|
11
|
+
* `encodeSessionData` produces JSON text and a MySQL `json` column stores it
|
|
12
|
+
* verbatim. What comes BACK depends on the driver - mysql2 parses a json
|
|
13
|
+
* column into an object, sqlite returns the text - so the read path accepts
|
|
14
|
+
* both. The declared type is deliberately not widened to `string | object`:
|
|
15
|
+
* this model is exported, the object shape never escapes the provider, and
|
|
16
|
+
* widening would force every consumer to narrow for a case it cannot observe.
|
|
17
|
+
*
|
|
18
|
+
* No `@Json()` decorator on purpose: the ORM's JsonValueConverter would
|
|
19
|
+
* `JSON.stringify` the already-encoded string on write and double-encode
|
|
20
|
+
* every session.
|
|
21
|
+
*/
|
|
7
22
|
Data: string;
|
|
8
23
|
UserId: number;
|
|
9
24
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DbSession.d.ts","sourceRoot":"","sources":["../../../src/models/DbSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAyD,MAAM,cAAc,CAAC;AAChG,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjC,qBAEa,SAAU,SAAQ,SAAS;IAE/B,SAAS,EAAE,MAAM,CAAC;IAGlB,SAAS,EAAE,QAAQ,CAAC;IAGpB,UAAU,EAAE,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"DbSession.d.ts","sourceRoot":"","sources":["../../../src/models/DbSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAyD,MAAM,cAAc,CAAC;AAChG,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjC,qBAEa,SAAU,SAAQ,SAAS;IAE/B,SAAS,EAAE,MAAM,CAAC;IAGlB,SAAS,EAAE,QAAQ,CAAC;IAGpB,UAAU,EAAE,QAAQ,CAAC;IAE5B;;;;;;;;;;;;;;OAcG;IACI,IAAI,EAAE,MAAM,CAAC;IAEb,MAAM,EAAE,MAAM,CAAC;CACvB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DbSession.js","sourceRoot":"","sources":["../../../src/models/DbSession.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,cAAc,CAAC;AAChG,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAG1B,IAAM,SAAS,GAAf,MAAM,SAAU,SAAQ,SAAS;
|
|
1
|
+
{"version":3,"file":"DbSession.js","sourceRoot":"","sources":["../../../src/models/DbSession.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,cAAc,CAAC;AAChG,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAG1B,IAAM,SAAS,GAAf,MAAM,SAAU,SAAQ,SAAS;CA4BvC,CAAA;AA1BQ;IADN,OAAO,EAAE;;4CACe;AAGlB;IADN,SAAS,EAAE;8BACM,QAAQ;4CAAC;AAGpB;IADN,EAAE,EAAE;8BACc,QAAQ;6CAAC;AARjB,SAAS;IAFrB,UAAU,CAAC,6BAA6B,CAAC;IACzC,KAAK,CAAC,eAAe,CAAC;GACV,SAAS,CA4BrB"}
|