@getstrata/core 0.7.4 → 1.0.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/CHANGELOG.md +9 -0
- package/README.md +3 -3
- package/dist/core/contracts/authUserDirectory.d.ts +4 -0
- package/dist/core/tenant/tenancyConfig.d.ts +4 -2
- package/dist/entries/audit/exportAuditLogs.js +22 -0
- package/dist/entries/auth/scimAuthMiddleware.js +13 -4
- package/dist/entries/auth/sessionCookie.js +2 -2
- package/dist/entries/auth/sessionGuard.js +2 -2
- package/dist/entries/database/model.js +4 -4
- package/dist/entries/database/query.js +2 -2
- package/dist/entries/database/repositoryQuery.js +20 -20
- package/dist/entries/database/schema.js +2 -2
- package/dist/entries/jobs/exportAuditLogsJob.js +22 -0
- package/dist/entries/openapi/generator.js +4 -4
- package/dist/entries/tenant/databaseTenantContext.js +22 -0
- package/dist/entries/tenant/tenancyConfig.js +11 -1
- package/dist/entries/tenant/tenantDatabaseScope.js +13 -4
- package/dist/framework/public-api.d.ts +1 -1
- package/dist/index.js +66 -53
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @getstrata/core changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
- First stable release of the public `@getstrata/core` API.
|
|
6
|
+
|
|
7
|
+
## 0.7.5
|
|
8
|
+
|
|
9
|
+
- `TENANCY_DRIVER=column`: tenant ALS without Postgres `SET LOCAL` / `set_config`. `isRlsTenancy()` is the RLS-only check.
|
|
10
|
+
- `AuthUserRecord` may include `name` and optional MFA columns used by generated starters.
|
|
11
|
+
|
|
3
12
|
## 0.7.4
|
|
4
13
|
|
|
5
14
|
- Default database pool and query handles live on `globalThis`, so the published `@getstrata/core` bundle and `src/core` share one pool in the same process.
|
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ import { EtaViewEngine } from "@getstrata/core/view";
|
|
|
23
23
|
|
|
24
24
|
`.eta` files are **HTML + Eta tags** (`<% %>`, `<%= %>`, `<%~ include() %>`), not another template language. Class/attribute shorthand such as `section.section` or `a href=` fails at render time with the template name.
|
|
25
25
|
|
|
26
|
-
**Dependency:** `eta` is a direct dependency of `@getstrata/core`. Apps do not need to list it separately. The database **engine** is your choice. HiroApp uses **Bun's built-in `Bun.sql`** (Postgres)
|
|
26
|
+
**Dependency:** `eta` is a direct dependency of `@getstrata/core`. Apps do not need to list it separately. The database **engine** is your choice. Generated HiroApp uses **Bun's built-in `Bun.sql`** (Postgres): `createBunSqlPool()`, then `registerDefaultDatabasePool()` and `bindDatabaseConnection()`. `bindBunSql()` is a helper that registers both. Extra engines register with `registerNamedConnection` (`database/namedConnections`, `database/sqliteConnection`, `database/mysqlConnection`).
|
|
27
27
|
|
|
28
28
|
`orderBy` accepts `{ column, direction }` objects or column shorthand such as `{ published_at: "desc" }`. `{ ilike }` uses the value as-is. Pass `%term%` yourself.
|
|
29
29
|
|
|
@@ -33,7 +33,7 @@ import { EtaViewEngine } from "@getstrata/core/view";
|
|
|
33
33
|
- `createFailedJobService`, `FailedJobService.delete()`: failed job persistence and cleanup
|
|
34
34
|
- `runQueueJob`, `jobRegistry`: dispatch retried jobs from admin UIs
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
Core ships failed-job helpers and an optional admin resource registry. Generated HiroApp does not include an admin dashboard. You do not have to use the registry.
|
|
37
37
|
|
|
38
38
|
## Build and verify (monorepo root)
|
|
39
39
|
|
|
@@ -65,7 +65,7 @@ import type { Migration } from "@getstrata/core/database/migrations/types";
|
|
|
65
65
|
Package name: **`@getstrata/core`** (npm org [`@getstrata`](https://www.npmjs.com/org/getstrata)).
|
|
66
66
|
|
|
67
67
|
1. Add `NPM_TOKEN` to GitHub repository secrets.
|
|
68
|
-
2. Tag a release: `git tag
|
|
68
|
+
2. Tag a release: `git tag v1.0.0 && git push origin v1.0.0`
|
|
69
69
|
3. [Release workflow](../../.github/workflows/release.yml) builds and runs `npm publish --access public`.
|
|
70
70
|
|
|
71
71
|
See [docs/PACKAGING.md](../../docs/PACKAGING.md).
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { AuthUser } from "../auth/authContext";
|
|
2
2
|
interface AuthUserRecord {
|
|
3
3
|
id: number;
|
|
4
|
+
name?: string | null;
|
|
4
5
|
email?: string | null;
|
|
5
6
|
role: string;
|
|
6
7
|
email_verified_at?: Date | string | null;
|
|
7
8
|
session_valid_after?: Date | string | null;
|
|
9
|
+
mfa_enabled?: boolean;
|
|
10
|
+
mfa_secret?: string | null;
|
|
11
|
+
mfa_recovery_codes?: string | null;
|
|
8
12
|
}
|
|
9
13
|
interface AuthUserDirectory {
|
|
10
14
|
resolveUserFromToken(token: string): Promise<AuthUser | null>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
type TenancyDriver = "rls" | "none";
|
|
1
|
+
type TenancyDriver = "rls" | "column" | "none";
|
|
2
2
|
declare function readTenancyDriver(env?: Record<string, string | undefined>): TenancyDriver;
|
|
3
3
|
declare function isTenancyEnabled(env?: Record<string, string | undefined>): boolean;
|
|
4
|
+
/** Postgres `SET LOCAL` / `set_config` for row-level security. SQLite and MySQL cannot do this. */
|
|
5
|
+
declare function isRlsTenancy(env?: Record<string, string | undefined>): boolean;
|
|
4
6
|
export type { TenancyDriver };
|
|
5
|
-
export { isTenancyEnabled, readTenancyDriver };
|
|
7
|
+
export { isRlsTenancy, isTenancyEnabled, readTenancyDriver };
|
|
@@ -189,7 +189,29 @@ async function safeFetch(input, init = {}, options = {}) {
|
|
|
189
189
|
|
|
190
190
|
// ../../src/core/tenant/databaseTenantContext.ts
|
|
191
191
|
import { repositoryConnection as db } from "@getstrata/core/database/repositoryConnection";
|
|
192
|
+
|
|
193
|
+
// ../../src/core/tenant/tenancyConfig.ts
|
|
194
|
+
function readTenancyDriver(env = process.env) {
|
|
195
|
+
if (env.TENANCY_DRIVER === "none") {
|
|
196
|
+
return "none";
|
|
197
|
+
}
|
|
198
|
+
if (env.TENANCY_DRIVER === "column") {
|
|
199
|
+
return "column";
|
|
200
|
+
}
|
|
201
|
+
return "rls";
|
|
202
|
+
}
|
|
203
|
+
function isTenancyEnabled(env = process.env) {
|
|
204
|
+
return readTenancyDriver(env) !== "none";
|
|
205
|
+
}
|
|
206
|
+
function isRlsTenancy(env = process.env) {
|
|
207
|
+
return readTenancyDriver(env) === "rls";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ../../src/core/tenant/databaseTenantContext.ts
|
|
192
211
|
async function runWithMigrationBypass(callback) {
|
|
212
|
+
if (!isRlsTenancy()) {
|
|
213
|
+
return await callback();
|
|
214
|
+
}
|
|
193
215
|
await db`SELECT set_config('app.bypass_rls', 'true', false)`;
|
|
194
216
|
try {
|
|
195
217
|
return await callback();
|
|
@@ -79,11 +79,20 @@ import { repositoryConnection as db } from "@getstrata/core/database/repositoryC
|
|
|
79
79
|
|
|
80
80
|
// ../../src/core/tenant/tenancyConfig.ts
|
|
81
81
|
function readTenancyDriver(env = process.env) {
|
|
82
|
-
|
|
82
|
+
if (env.TENANCY_DRIVER === "none") {
|
|
83
|
+
return "none";
|
|
84
|
+
}
|
|
85
|
+
if (env.TENANCY_DRIVER === "column") {
|
|
86
|
+
return "column";
|
|
87
|
+
}
|
|
88
|
+
return "rls";
|
|
83
89
|
}
|
|
84
90
|
function isTenancyEnabled(env = process.env) {
|
|
85
91
|
return readTenancyDriver(env) !== "none";
|
|
86
92
|
}
|
|
93
|
+
function isRlsTenancy(env = process.env) {
|
|
94
|
+
return readTenancyDriver(env) === "rls";
|
|
95
|
+
}
|
|
87
96
|
|
|
88
97
|
// ../../src/core/tenant/resolveTenant.ts
|
|
89
98
|
async function resolveTenant(tenantId) {
|
|
@@ -123,12 +132,12 @@ async function applyTenantContextToTransaction(transaction, tenantId) {
|
|
|
123
132
|
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
|
|
124
133
|
}
|
|
125
134
|
async function runWithTenantDatabase(tenant, callback) {
|
|
126
|
-
if (!isTenancyEnabled()) {
|
|
135
|
+
if (!isTenancyEnabled() || !isRlsTenancy()) {
|
|
127
136
|
return await runWithTenant(tenant, callback);
|
|
128
137
|
}
|
|
129
138
|
if (hasActiveDatabaseConnection()) {
|
|
130
|
-
const
|
|
131
|
-
await applyTenantContextToTransaction(
|
|
139
|
+
const activeConnection = getActiveDatabaseConnection(getDefaultDatabasePool());
|
|
140
|
+
await applyTenantContextToTransaction(activeConnection, tenant.id);
|
|
132
141
|
return await runWithTenant(tenant, callback);
|
|
133
142
|
}
|
|
134
143
|
return await getDefaultDatabasePool().begin(async (transaction) => {
|
|
@@ -127,8 +127,8 @@ function readSession(request) {
|
|
|
127
127
|
}
|
|
128
128
|
const parts = cookieValue.split(".");
|
|
129
129
|
if (parts.length === 4) {
|
|
130
|
-
const [
|
|
131
|
-
return readSignedSession(String(
|
|
130
|
+
const [userIdRaw, issuedAtRaw, ttlRaw, cookieSignature] = parts;
|
|
131
|
+
return readSignedSession(String(userIdRaw), String(issuedAtRaw), cookieSignature, Number.parseInt(String(ttlRaw), 10), true);
|
|
132
132
|
}
|
|
133
133
|
if (parts.length !== 3) {
|
|
134
134
|
return null;
|
|
@@ -194,8 +194,8 @@ function readSession(request) {
|
|
|
194
194
|
}
|
|
195
195
|
const parts = cookieValue.split(".");
|
|
196
196
|
if (parts.length === 4) {
|
|
197
|
-
const [
|
|
198
|
-
return readSignedSession(String(
|
|
197
|
+
const [userIdRaw, issuedAtRaw, ttlRaw, cookieSignature] = parts;
|
|
198
|
+
return readSignedSession(String(userIdRaw), String(issuedAtRaw), cookieSignature, Number.parseInt(String(ttlRaw), 10), true);
|
|
199
199
|
}
|
|
200
200
|
if (parts.length !== 3) {
|
|
201
201
|
return null;
|
|
@@ -298,8 +298,8 @@ function buildSelectList(table, select, params = []) {
|
|
|
298
298
|
}
|
|
299
299
|
return select.map((item) => {
|
|
300
300
|
if (item.kind === "column") {
|
|
301
|
-
const
|
|
302
|
-
return item.as ? `${
|
|
301
|
+
const column = qualifyColumn(item.table, item.column);
|
|
302
|
+
return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
|
|
303
303
|
}
|
|
304
304
|
if (item.kind === "literalText") {
|
|
305
305
|
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
@@ -1919,8 +1919,8 @@ class Model {
|
|
|
1919
1919
|
}
|
|
1920
1920
|
if (updating) {
|
|
1921
1921
|
const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
|
|
1922
|
-
const
|
|
1923
|
-
this.attributes = ModelClass.hydrateAttributes(
|
|
1922
|
+
const record = await this.repository.updateByIdOrThrow(this.id, changes);
|
|
1923
|
+
this.attributes = ModelClass.hydrateAttributes(record);
|
|
1924
1924
|
await runObservers(this, "updated");
|
|
1925
1925
|
await runObservers(this, "saved");
|
|
1926
1926
|
return this;
|
|
@@ -275,8 +275,8 @@ function buildSelectList(table, select, params = []) {
|
|
|
275
275
|
}
|
|
276
276
|
return select.map((item) => {
|
|
277
277
|
if (item.kind === "column") {
|
|
278
|
-
const
|
|
279
|
-
return item.as ? `${
|
|
278
|
+
const column = qualifyColumn(item.table, item.column);
|
|
279
|
+
return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
|
|
280
280
|
}
|
|
281
281
|
if (item.kind === "literalText") {
|
|
282
282
|
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
@@ -275,8 +275,8 @@ function buildSelectList(table, select, params = []) {
|
|
|
275
275
|
}
|
|
276
276
|
return select.map((item) => {
|
|
277
277
|
if (item.kind === "column") {
|
|
278
|
-
const
|
|
279
|
-
return item.as ? `${
|
|
278
|
+
const column = qualifyColumn(item.table, item.column);
|
|
279
|
+
return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
|
|
280
280
|
}
|
|
281
281
|
if (item.kind === "literalText") {
|
|
282
282
|
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
@@ -925,50 +925,50 @@ class RepositoryQuery {
|
|
|
925
925
|
}
|
|
926
926
|
async hydrateEagerLoad(rows, result, load) {
|
|
927
927
|
if (load.kind === "hasMany") {
|
|
928
|
-
const
|
|
929
|
-
const
|
|
928
|
+
const relation = load.relation;
|
|
929
|
+
const grouped = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation, load.options);
|
|
930
930
|
for (const row of result) {
|
|
931
|
-
row[load.as] = getByRelationKey(
|
|
931
|
+
row[load.as] = getByRelationKey(grouped, row[relation.localKey]) ?? [];
|
|
932
932
|
}
|
|
933
933
|
return;
|
|
934
934
|
}
|
|
935
935
|
if (load.kind === "morphMany") {
|
|
936
|
-
const
|
|
937
|
-
const
|
|
936
|
+
const relation = load.relation;
|
|
937
|
+
const grouped = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation, load.options);
|
|
938
938
|
for (const row of result) {
|
|
939
|
-
row[load.as] = getByRelationKey(
|
|
939
|
+
row[load.as] = getByRelationKey(grouped, row[relation.localKey]) ?? [];
|
|
940
940
|
}
|
|
941
941
|
return;
|
|
942
942
|
}
|
|
943
943
|
if (load.kind === "morphOne") {
|
|
944
|
-
const
|
|
945
|
-
const
|
|
944
|
+
const relation = load.relation;
|
|
945
|
+
const grouped = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation, load.options);
|
|
946
946
|
for (const row of result) {
|
|
947
|
-
row[load.as] = getByRelationKey(
|
|
947
|
+
row[load.as] = getByRelationKey(grouped, row[relation.localKey]);
|
|
948
948
|
}
|
|
949
949
|
return;
|
|
950
950
|
}
|
|
951
951
|
if (load.kind === "hasManyThrough") {
|
|
952
|
-
const
|
|
953
|
-
const
|
|
952
|
+
const relation = load.relation;
|
|
953
|
+
const grouped = await load.repository.withConnection(this.repository.getConnection()).loadHasManyThroughForParents(rows, relation, load.options);
|
|
954
954
|
for (const row of result) {
|
|
955
|
-
row[load.as] = getByRelationKey(
|
|
955
|
+
row[load.as] = getByRelationKey(grouped, row[relation.localKey]) ?? [];
|
|
956
956
|
}
|
|
957
957
|
return;
|
|
958
958
|
}
|
|
959
959
|
if (load.kind === "belongsToMany") {
|
|
960
|
-
const
|
|
961
|
-
const
|
|
960
|
+
const relation = load.relation;
|
|
961
|
+
const grouped = await this.repository.loadBelongsToManyForParents(rows, relation, load.repository, load.options);
|
|
962
962
|
for (const row of result) {
|
|
963
|
-
row[load.as] = getByRelationKey(
|
|
963
|
+
row[load.as] = getByRelationKey(grouped, row[relation.parentKey]) ?? [];
|
|
964
964
|
}
|
|
965
965
|
return;
|
|
966
966
|
}
|
|
967
967
|
if (load.kind === "morphTo") {
|
|
968
|
-
const
|
|
969
|
-
const
|
|
968
|
+
const relation = load.relation;
|
|
969
|
+
const grouped = await this.repository.loadMorphToForChildren(rows, relation, load.morphRepositories ?? new Map, load.options);
|
|
970
970
|
for (const row of result) {
|
|
971
|
-
row[load.as] = getByRelationKey(
|
|
971
|
+
row[load.as] = getByRelationKey(grouped, row[relation.morphIdKey]);
|
|
972
972
|
}
|
|
973
973
|
return;
|
|
974
974
|
}
|
|
@@ -542,8 +542,8 @@ function buildSelectList(table, select, params = []) {
|
|
|
542
542
|
}
|
|
543
543
|
return select.map((item) => {
|
|
544
544
|
if (item.kind === "column") {
|
|
545
|
-
const
|
|
546
|
-
return item.as ? `${
|
|
545
|
+
const column = qualifyColumn(item.table, item.column);
|
|
546
|
+
return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
|
|
547
547
|
}
|
|
548
548
|
if (item.kind === "literalText") {
|
|
549
549
|
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
@@ -189,7 +189,29 @@ async function safeFetch(input, init = {}, options = {}) {
|
|
|
189
189
|
|
|
190
190
|
// ../../src/core/tenant/databaseTenantContext.ts
|
|
191
191
|
import { repositoryConnection as db } from "@getstrata/core/database/repositoryConnection";
|
|
192
|
+
|
|
193
|
+
// ../../src/core/tenant/tenancyConfig.ts
|
|
194
|
+
function readTenancyDriver(env = process.env) {
|
|
195
|
+
if (env.TENANCY_DRIVER === "none") {
|
|
196
|
+
return "none";
|
|
197
|
+
}
|
|
198
|
+
if (env.TENANCY_DRIVER === "column") {
|
|
199
|
+
return "column";
|
|
200
|
+
}
|
|
201
|
+
return "rls";
|
|
202
|
+
}
|
|
203
|
+
function isTenancyEnabled(env = process.env) {
|
|
204
|
+
return readTenancyDriver(env) !== "none";
|
|
205
|
+
}
|
|
206
|
+
function isRlsTenancy(env = process.env) {
|
|
207
|
+
return readTenancyDriver(env) === "rls";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ../../src/core/tenant/databaseTenantContext.ts
|
|
192
211
|
async function runWithMigrationBypass(callback) {
|
|
212
|
+
if (!isRlsTenancy()) {
|
|
213
|
+
return await callback();
|
|
214
|
+
}
|
|
193
215
|
await db`SELECT set_config('app.bypass_rls', 'true', false)`;
|
|
194
216
|
try {
|
|
195
217
|
return await callback();
|
|
@@ -254,13 +254,13 @@ function renderOpenApiDocument(spec) {
|
|
|
254
254
|
return `${JSON.stringify(spec, null, 2)}
|
|
255
255
|
`;
|
|
256
256
|
}
|
|
257
|
-
function toMethodName(method, path,
|
|
258
|
-
const relativePath = path.startsWith(
|
|
257
|
+
function toMethodName(method, path, apiPrefix) {
|
|
258
|
+
const relativePath = path.startsWith(apiPrefix) ? path.slice(apiPrefix.length) || "/" : path;
|
|
259
259
|
const segments = relativePath.replace(/\{|\}/g, "").split("/").filter(Boolean).flatMap((segment) => segment.split("-")).map((segment) => segment.replace(/[^a-zA-Z0-9]/g, "")).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1));
|
|
260
260
|
return `${method.toLowerCase()}${segments.join("")}`;
|
|
261
261
|
}
|
|
262
|
-
function toRequestPath(path,
|
|
263
|
-
return path.startsWith(
|
|
262
|
+
function toRequestPath(path, apiPrefix) {
|
|
263
|
+
return path.startsWith(apiPrefix) ? path.slice(apiPrefix.length) || "/" : path;
|
|
264
264
|
}
|
|
265
265
|
function renderTypeScriptSdk(spec, prefix = apiPrefix()) {
|
|
266
266
|
const lines = [
|
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/core/tenant/databaseTenantContext.ts
|
|
3
3
|
import { repositoryConnection as db } from "@getstrata/core/database/repositoryConnection";
|
|
4
|
+
|
|
5
|
+
// ../../src/core/tenant/tenancyConfig.ts
|
|
6
|
+
function readTenancyDriver(env = process.env) {
|
|
7
|
+
if (env.TENANCY_DRIVER === "none") {
|
|
8
|
+
return "none";
|
|
9
|
+
}
|
|
10
|
+
if (env.TENANCY_DRIVER === "column") {
|
|
11
|
+
return "column";
|
|
12
|
+
}
|
|
13
|
+
return "rls";
|
|
14
|
+
}
|
|
15
|
+
function isTenancyEnabled(env = process.env) {
|
|
16
|
+
return readTenancyDriver(env) !== "none";
|
|
17
|
+
}
|
|
18
|
+
function isRlsTenancy(env = process.env) {
|
|
19
|
+
return readTenancyDriver(env) === "rls";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ../../src/core/tenant/databaseTenantContext.ts
|
|
4
23
|
async function runWithMigrationBypass(callback) {
|
|
24
|
+
if (!isRlsTenancy()) {
|
|
25
|
+
return await callback();
|
|
26
|
+
}
|
|
5
27
|
await db`SELECT set_config('app.bypass_rls', 'true', false)`;
|
|
6
28
|
try {
|
|
7
29
|
return await callback();
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/core/tenant/tenancyConfig.ts
|
|
3
3
|
function readTenancyDriver(env = process.env) {
|
|
4
|
-
|
|
4
|
+
if (env.TENANCY_DRIVER === "none") {
|
|
5
|
+
return "none";
|
|
6
|
+
}
|
|
7
|
+
if (env.TENANCY_DRIVER === "column") {
|
|
8
|
+
return "column";
|
|
9
|
+
}
|
|
10
|
+
return "rls";
|
|
5
11
|
}
|
|
6
12
|
function isTenancyEnabled(env = process.env) {
|
|
7
13
|
return readTenancyDriver(env) !== "none";
|
|
8
14
|
}
|
|
15
|
+
function isRlsTenancy(env = process.env) {
|
|
16
|
+
return readTenancyDriver(env) === "rls";
|
|
17
|
+
}
|
|
9
18
|
export {
|
|
19
|
+
isRlsTenancy,
|
|
10
20
|
isTenancyEnabled,
|
|
11
21
|
readTenancyDriver
|
|
12
22
|
};
|
|
@@ -30,11 +30,20 @@ function hasActiveDatabaseConnection() {
|
|
|
30
30
|
|
|
31
31
|
// ../../src/core/tenant/tenancyConfig.ts
|
|
32
32
|
function readTenancyDriver(env = process.env) {
|
|
33
|
-
|
|
33
|
+
if (env.TENANCY_DRIVER === "none") {
|
|
34
|
+
return "none";
|
|
35
|
+
}
|
|
36
|
+
if (env.TENANCY_DRIVER === "column") {
|
|
37
|
+
return "column";
|
|
38
|
+
}
|
|
39
|
+
return "rls";
|
|
34
40
|
}
|
|
35
41
|
function isTenancyEnabled(env = process.env) {
|
|
36
42
|
return readTenancyDriver(env) !== "none";
|
|
37
43
|
}
|
|
44
|
+
function isRlsTenancy(env = process.env) {
|
|
45
|
+
return readTenancyDriver(env) === "rls";
|
|
46
|
+
}
|
|
38
47
|
|
|
39
48
|
// ../../src/core/tenant/tenantContext.ts
|
|
40
49
|
var tenantContext = createAsyncContextStore("@getstrata/tenantContext");
|
|
@@ -51,12 +60,12 @@ async function applyTenantContextToTransaction(transaction, tenantId) {
|
|
|
51
60
|
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
|
|
52
61
|
}
|
|
53
62
|
async function runWithTenantDatabase(tenant, callback) {
|
|
54
|
-
if (!isTenancyEnabled()) {
|
|
63
|
+
if (!isTenancyEnabled() || !isRlsTenancy()) {
|
|
55
64
|
return await runWithTenant(tenant, callback);
|
|
56
65
|
}
|
|
57
66
|
if (hasActiveDatabaseConnection()) {
|
|
58
|
-
const
|
|
59
|
-
await applyTenantContextToTransaction(
|
|
67
|
+
const activeConnection = getActiveDatabaseConnection(getDefaultDatabasePool());
|
|
68
|
+
await applyTenantContextToTransaction(activeConnection, tenant.id);
|
|
60
69
|
return await runWithTenant(tenant, callback);
|
|
61
70
|
}
|
|
62
71
|
return await getDefaultDatabasePool().begin(async (transaction) => {
|
|
@@ -126,7 +126,7 @@ export { logSecurityEvent } from "../core/security/securityEvents.ts";
|
|
|
126
126
|
export type { StorageDriver } from "../core/storage/storage.ts";
|
|
127
127
|
export { createStorageDriver, LocalStorageDriver, resetDefaultStorage, StorageManager, } from "../core/storage/storage.ts";
|
|
128
128
|
export type { TenancyDriver } from "../core/tenant/tenancyConfig.ts";
|
|
129
|
-
export { isTenancyEnabled, readTenancyDriver } from "../core/tenant/tenancyConfig.ts";
|
|
129
|
+
export { isRlsTenancy, isTenancyEnabled, readTenancyDriver, } from "../core/tenant/tenancyConfig.ts";
|
|
130
130
|
export type { TenantContext } from "../core/tenant/tenantContext.ts";
|
|
131
131
|
export { currentTenant, currentTenantId, rateLimitMultiplierForPlan, runWithTenant, } from "../core/tenant/tenantContext.ts";
|
|
132
132
|
export { isInsideTenantDatabaseScope, runWithTenantDatabase, } from "../core/tenant/tenantDatabaseScope.ts";
|
package/dist/index.js
CHANGED
|
@@ -326,11 +326,11 @@ function abilityCatalog() {
|
|
|
326
326
|
|
|
327
327
|
// ../../src/core/auth/guard.ts
|
|
328
328
|
function devHeaderAbilities(role) {
|
|
329
|
-
const
|
|
329
|
+
const catalog = abilityCatalog();
|
|
330
330
|
if (role === "admin") {
|
|
331
|
-
return [...
|
|
331
|
+
return [...catalog.admin];
|
|
332
332
|
}
|
|
333
|
-
return [...
|
|
333
|
+
return [...catalog.member];
|
|
334
334
|
}
|
|
335
335
|
|
|
336
336
|
class GuestGuard {
|
|
@@ -1144,7 +1144,7 @@ function resolveScimTenantFromToken(token) {
|
|
|
1144
1144
|
function resolveRepositoryConnection() {
|
|
1145
1145
|
return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
|
|
1146
1146
|
}
|
|
1147
|
-
var repositoryConnection = new Proxy(function
|
|
1147
|
+
var repositoryConnection = new Proxy(function repositoryConnection() {}, {
|
|
1148
1148
|
apply(_target, _thisArg, args) {
|
|
1149
1149
|
return resolveRepositoryConnection()(...args);
|
|
1150
1150
|
},
|
|
@@ -1157,11 +1157,20 @@ var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
|
|
|
1157
1157
|
|
|
1158
1158
|
// ../../src/core/tenant/tenancyConfig.ts
|
|
1159
1159
|
function readTenancyDriver(env = process.env) {
|
|
1160
|
-
|
|
1160
|
+
if (env.TENANCY_DRIVER === "none") {
|
|
1161
|
+
return "none";
|
|
1162
|
+
}
|
|
1163
|
+
if (env.TENANCY_DRIVER === "column") {
|
|
1164
|
+
return "column";
|
|
1165
|
+
}
|
|
1166
|
+
return "rls";
|
|
1161
1167
|
}
|
|
1162
1168
|
function isTenancyEnabled(env = process.env) {
|
|
1163
1169
|
return readTenancyDriver(env) !== "none";
|
|
1164
1170
|
}
|
|
1171
|
+
function isRlsTenancy(env = process.env) {
|
|
1172
|
+
return readTenancyDriver(env) === "rls";
|
|
1173
|
+
}
|
|
1165
1174
|
|
|
1166
1175
|
// ../../src/core/tenant/resolveTenant.ts
|
|
1167
1176
|
async function resolveTenant(tenantId) {
|
|
@@ -1189,12 +1198,12 @@ async function applyTenantContextToTransaction(transaction, tenantId) {
|
|
|
1189
1198
|
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
|
|
1190
1199
|
}
|
|
1191
1200
|
async function runWithTenantDatabase(tenant, callback) {
|
|
1192
|
-
if (!isTenancyEnabled()) {
|
|
1201
|
+
if (!isTenancyEnabled() || !isRlsTenancy()) {
|
|
1193
1202
|
return await runWithTenant(tenant, callback);
|
|
1194
1203
|
}
|
|
1195
1204
|
if (hasActiveDatabaseConnection()) {
|
|
1196
|
-
const
|
|
1197
|
-
await applyTenantContextToTransaction(
|
|
1205
|
+
const activeConnection = getActiveDatabaseConnection(getDefaultDatabasePool());
|
|
1206
|
+
await applyTenantContextToTransaction(activeConnection, tenant.id);
|
|
1198
1207
|
return await runWithTenant(tenant, callback);
|
|
1199
1208
|
}
|
|
1200
1209
|
return await getDefaultDatabasePool().begin(async (transaction) => {
|
|
@@ -2307,8 +2316,8 @@ function buildSelectList(table, select, params = []) {
|
|
|
2307
2316
|
}
|
|
2308
2317
|
return select.map((item) => {
|
|
2309
2318
|
if (item.kind === "column") {
|
|
2310
|
-
const
|
|
2311
|
-
return item.as ? `${
|
|
2319
|
+
const column = qualifyColumn(item.table, item.column);
|
|
2320
|
+
return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
|
|
2312
2321
|
}
|
|
2313
2322
|
if (item.kind === "literalText") {
|
|
2314
2323
|
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
@@ -2957,50 +2966,50 @@ class RepositoryQuery {
|
|
|
2957
2966
|
}
|
|
2958
2967
|
async hydrateEagerLoad(rows, result, load) {
|
|
2959
2968
|
if (load.kind === "hasMany") {
|
|
2960
|
-
const
|
|
2961
|
-
const
|
|
2969
|
+
const relation = load.relation;
|
|
2970
|
+
const grouped = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation, load.options);
|
|
2962
2971
|
for (const row of result) {
|
|
2963
|
-
row[load.as] = getByRelationKey(
|
|
2972
|
+
row[load.as] = getByRelationKey(grouped, row[relation.localKey]) ?? [];
|
|
2964
2973
|
}
|
|
2965
2974
|
return;
|
|
2966
2975
|
}
|
|
2967
2976
|
if (load.kind === "morphMany") {
|
|
2968
|
-
const
|
|
2969
|
-
const
|
|
2977
|
+
const relation = load.relation;
|
|
2978
|
+
const grouped = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation, load.options);
|
|
2970
2979
|
for (const row of result) {
|
|
2971
|
-
row[load.as] = getByRelationKey(
|
|
2980
|
+
row[load.as] = getByRelationKey(grouped, row[relation.localKey]) ?? [];
|
|
2972
2981
|
}
|
|
2973
2982
|
return;
|
|
2974
2983
|
}
|
|
2975
2984
|
if (load.kind === "morphOne") {
|
|
2976
|
-
const
|
|
2977
|
-
const
|
|
2985
|
+
const relation = load.relation;
|
|
2986
|
+
const grouped = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation, load.options);
|
|
2978
2987
|
for (const row of result) {
|
|
2979
|
-
row[load.as] = getByRelationKey(
|
|
2988
|
+
row[load.as] = getByRelationKey(grouped, row[relation.localKey]);
|
|
2980
2989
|
}
|
|
2981
2990
|
return;
|
|
2982
2991
|
}
|
|
2983
2992
|
if (load.kind === "hasManyThrough") {
|
|
2984
|
-
const
|
|
2985
|
-
const
|
|
2993
|
+
const relation = load.relation;
|
|
2994
|
+
const grouped = await load.repository.withConnection(this.repository.getConnection()).loadHasManyThroughForParents(rows, relation, load.options);
|
|
2986
2995
|
for (const row of result) {
|
|
2987
|
-
row[load.as] = getByRelationKey(
|
|
2996
|
+
row[load.as] = getByRelationKey(grouped, row[relation.localKey]) ?? [];
|
|
2988
2997
|
}
|
|
2989
2998
|
return;
|
|
2990
2999
|
}
|
|
2991
3000
|
if (load.kind === "belongsToMany") {
|
|
2992
|
-
const
|
|
2993
|
-
const
|
|
3001
|
+
const relation = load.relation;
|
|
3002
|
+
const grouped = await this.repository.loadBelongsToManyForParents(rows, relation, load.repository, load.options);
|
|
2994
3003
|
for (const row of result) {
|
|
2995
|
-
row[load.as] = getByRelationKey(
|
|
3004
|
+
row[load.as] = getByRelationKey(grouped, row[relation.parentKey]) ?? [];
|
|
2996
3005
|
}
|
|
2997
3006
|
return;
|
|
2998
3007
|
}
|
|
2999
3008
|
if (load.kind === "morphTo") {
|
|
3000
|
-
const
|
|
3001
|
-
const
|
|
3009
|
+
const relation = load.relation;
|
|
3010
|
+
const grouped = await this.repository.loadMorphToForChildren(rows, relation, load.morphRepositories ?? new Map, load.options);
|
|
3002
3011
|
for (const row of result) {
|
|
3003
|
-
row[load.as] = getByRelationKey(
|
|
3012
|
+
row[load.as] = getByRelationKey(grouped, row[relation.morphIdKey]);
|
|
3004
3013
|
}
|
|
3005
3014
|
return;
|
|
3006
3015
|
}
|
|
@@ -4937,8 +4946,8 @@ class Model {
|
|
|
4937
4946
|
}
|
|
4938
4947
|
if (updating) {
|
|
4939
4948
|
const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
|
|
4940
|
-
const
|
|
4941
|
-
this.attributes = ModelClass.hydrateAttributes(
|
|
4949
|
+
const record = await this.repository.updateByIdOrThrow(this.id, changes);
|
|
4950
|
+
this.attributes = ModelClass.hydrateAttributes(record);
|
|
4942
4951
|
await runObservers(this, "updated");
|
|
4943
4952
|
await runObservers(this, "saved");
|
|
4944
4953
|
return this;
|
|
@@ -6911,9 +6920,9 @@ class FormRequest {
|
|
|
6911
6920
|
}
|
|
6912
6921
|
}
|
|
6913
6922
|
// ../../src/core/http/authMiddleware.ts
|
|
6914
|
-
function createAuthMiddleware(
|
|
6923
|
+
function createAuthMiddleware(auth) {
|
|
6915
6924
|
return async (request, next) => {
|
|
6916
|
-
const user = await
|
|
6925
|
+
const user = await auth.resolve(request);
|
|
6917
6926
|
return await runWithAuthUser(user, async () => {
|
|
6918
6927
|
const response = await next();
|
|
6919
6928
|
if (user) {
|
|
@@ -6930,9 +6939,9 @@ function createAuthMiddleware(auth2) {
|
|
|
6930
6939
|
};
|
|
6931
6940
|
}
|
|
6932
6941
|
// ../../src/core/http/authorizeMiddleware.ts
|
|
6933
|
-
function createAuthorizeMiddleware(gate,
|
|
6942
|
+
function createAuthorizeMiddleware(gate, auth, resource, action) {
|
|
6934
6943
|
return async (request, next) => {
|
|
6935
|
-
const user = currentAuthUser() ?? await
|
|
6944
|
+
const user = currentAuthUser() ?? await auth.resolve(request);
|
|
6936
6945
|
if (!gate.allows(resource, action, user)) {
|
|
6937
6946
|
const error = new ForbiddenError;
|
|
6938
6947
|
return Response.json({ error: error.message }, { status: error.status });
|
|
@@ -7106,9 +7115,9 @@ async function parseMultipartUpload(request, fieldName = "file") {
|
|
|
7106
7115
|
};
|
|
7107
7116
|
}
|
|
7108
7117
|
// ../../src/core/http/requireAuthMiddleware.ts
|
|
7109
|
-
function createRequireAuthMiddleware(
|
|
7118
|
+
function createRequireAuthMiddleware(auth) {
|
|
7110
7119
|
return async (request, next) => {
|
|
7111
|
-
if (!await
|
|
7120
|
+
if (!await auth.check(request)) {
|
|
7112
7121
|
const error = new UnauthorizedError;
|
|
7113
7122
|
return Response.json({ error: error.message }, { status: error.status });
|
|
7114
7123
|
}
|
|
@@ -7504,8 +7513,8 @@ function securedBindRouteModel(param, resolver, authorization, handler) {
|
|
|
7504
7513
|
const id = parsePositiveIntParam(String(request.params[param]), String(param));
|
|
7505
7514
|
const model = requireResolvedModel(await resolver(id, request));
|
|
7506
7515
|
const gate = resolveApplicationPolicyGate();
|
|
7507
|
-
const
|
|
7508
|
-
const user = currentAuthUser() ?? await
|
|
7516
|
+
const auth = resolveApplicationAuth();
|
|
7517
|
+
const user = currentAuthUser() ?? await auth.resolve(request);
|
|
7509
7518
|
gate.authorize(authorization.resource, authorization.action, user, model);
|
|
7510
7519
|
if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
|
|
7511
7520
|
assertIfMatch(request, etagFromResource(model), {
|
|
@@ -7527,8 +7536,8 @@ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
|
|
|
7527
7536
|
}
|
|
7528
7537
|
const model = requireResolvedModel(await resolver(key, request));
|
|
7529
7538
|
const gate = resolveApplicationPolicyGate();
|
|
7530
|
-
const
|
|
7531
|
-
const user = currentAuthUser() ?? await
|
|
7539
|
+
const auth = resolveApplicationAuth();
|
|
7540
|
+
const user = currentAuthUser() ?? await auth.resolve(request);
|
|
7532
7541
|
gate.authorize(authorization.resource, authorization.action, user, model);
|
|
7533
7542
|
if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
|
|
7534
7543
|
assertIfMatch(request, etagFromResource(model), {
|
|
@@ -7850,9 +7859,9 @@ function createIntendedUrlCookieFromRequest(request) {
|
|
|
7850
7859
|
}
|
|
7851
7860
|
|
|
7852
7861
|
// ../../src/core/http/requireWebAuthMiddleware.ts
|
|
7853
|
-
function createRequireWebAuthMiddleware(
|
|
7862
|
+
function createRequireWebAuthMiddleware(auth) {
|
|
7854
7863
|
return async (request, next) => {
|
|
7855
|
-
const user = await
|
|
7864
|
+
const user = await auth.resolve(request);
|
|
7856
7865
|
if (user) {
|
|
7857
7866
|
return await runWithAuthUser(user, () => next());
|
|
7858
7867
|
}
|
|
@@ -8281,15 +8290,15 @@ function buildMarkdownMailMessage(input) {
|
|
|
8281
8290
|
html: rendered.html
|
|
8282
8291
|
};
|
|
8283
8292
|
}
|
|
8284
|
-
async function sendMarkdownMail(
|
|
8285
|
-
await
|
|
8293
|
+
async function sendMarkdownMail(mailer, input) {
|
|
8294
|
+
await mailer.send(buildMarkdownMailMessage(input));
|
|
8286
8295
|
}
|
|
8287
8296
|
// ../../src/core/notifications/dispatcher.ts
|
|
8288
8297
|
class NotificationDispatcher {
|
|
8289
8298
|
mailer;
|
|
8290
8299
|
databaseStore;
|
|
8291
|
-
constructor(
|
|
8292
|
-
this.mailer =
|
|
8300
|
+
constructor(mailer, databaseStore = null) {
|
|
8301
|
+
this.mailer = mailer;
|
|
8293
8302
|
this.databaseStore = databaseStore;
|
|
8294
8303
|
}
|
|
8295
8304
|
async send(notifiable, notification) {
|
|
@@ -8341,8 +8350,8 @@ class NotificationDispatcher {
|
|
|
8341
8350
|
});
|
|
8342
8351
|
}
|
|
8343
8352
|
}
|
|
8344
|
-
function createNotificationDispatcher(
|
|
8345
|
-
return new NotificationDispatcher(
|
|
8353
|
+
function createNotificationDispatcher(mailer, databaseStore) {
|
|
8354
|
+
return new NotificationDispatcher(mailer, databaseStore ?? null);
|
|
8346
8355
|
}
|
|
8347
8356
|
// ../../src/core/notifications/notification.ts
|
|
8348
8357
|
class Notification {
|
|
@@ -8472,9 +8481,9 @@ function readSharedJobRegistry() {
|
|
|
8472
8481
|
if (globalRegistry) {
|
|
8473
8482
|
return globalRegistry;
|
|
8474
8483
|
}
|
|
8475
|
-
const
|
|
8476
|
-
globalThis[JOB_REGISTRY_KEY] =
|
|
8477
|
-
return
|
|
8484
|
+
const registry = new JobRegistry;
|
|
8485
|
+
globalThis[JOB_REGISTRY_KEY] = registry;
|
|
8486
|
+
return registry;
|
|
8478
8487
|
}
|
|
8479
8488
|
var jobRegistry = readSharedJobRegistry();
|
|
8480
8489
|
|
|
@@ -8829,6 +8838,9 @@ import { createHash } from "crypto";
|
|
|
8829
8838
|
|
|
8830
8839
|
// ../../src/core/tenant/databaseTenantContext.ts
|
|
8831
8840
|
async function runWithMigrationBypass(callback) {
|
|
8841
|
+
if (!isRlsTenancy()) {
|
|
8842
|
+
return await callback();
|
|
8843
|
+
}
|
|
8832
8844
|
await repositoryConnection`SELECT set_config('app.bypass_rls', 'true', false)`;
|
|
8833
8845
|
try {
|
|
8834
8846
|
return await callback();
|
|
@@ -9286,9 +9298,9 @@ class EtaViewEngine {
|
|
|
9286
9298
|
autoTrim: false
|
|
9287
9299
|
});
|
|
9288
9300
|
this.resolveLayoutData = resolveLayoutData;
|
|
9289
|
-
const
|
|
9301
|
+
const readFile = this.eta.readFile?.bind(this.eta);
|
|
9290
9302
|
this.eta.readFile = (path) => {
|
|
9291
|
-
const source =
|
|
9303
|
+
const source = readFile ? readFile(path) : "";
|
|
9292
9304
|
assertEtaHtmlSource(relative(viewsDirectory, path) || path, source);
|
|
9293
9305
|
return source;
|
|
9294
9306
|
};
|
|
@@ -9561,6 +9573,7 @@ export {
|
|
|
9561
9573
|
isHttpErrorLike,
|
|
9562
9574
|
isInsideTenantDatabaseScope,
|
|
9563
9575
|
isPublicReadsEnabled,
|
|
9576
|
+
isRlsTenancy,
|
|
9564
9577
|
isTenancyEnabled,
|
|
9565
9578
|
jobRegistry,
|
|
9566
9579
|
jsonResponse2 as jsonResponse,
|