@soulcode/bexio-oauth 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +66 -2
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +4 -0
- package/dist/bin.js.map +1 -0
- package/dist/claims.d.ts +31 -0
- package/dist/claims.d.ts.map +1 -0
- package/dist/claims.js +58 -0
- package/dist/claims.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +44 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +140 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +635 -0
- package/dist/client.js.map +1 -0
- package/dist/crypto.d.ts +27 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +101 -0
- package/dist/crypto.js.map +1 -0
- package/dist/duration.d.ts +4 -0
- package/dist/duration.d.ts.map +1 -0
- package/dist/duration.js +15 -0
- package/dist/duration.js.map +1 -0
- package/dist/errors.d.ts +38 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/permissions.d.ts +9 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +29 -0
- package/dist/permissions.js.map +1 -0
- package/dist/sql.d.ts +9 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +15 -0
- package/dist/sql.js.map +1 -0
- package/dist/store/memory.d.ts +7 -0
- package/dist/store/memory.d.ts.map +1 -0
- package/dist/store/memory.js +104 -0
- package/dist/store/memory.js.map +1 -0
- package/dist/store/postgres.d.ts +17 -0
- package/dist/store/postgres.d.ts.map +1 -0
- package/dist/store/postgres.js +168 -0
- package/dist/store/postgres.js.map +1 -0
- package/dist/store/types.d.ts +105 -0
- package/dist/store/types.d.ts.map +1 -0
- package/dist/store/types.js +2 -0
- package/dist/store/types.js.map +1 -0
- package/package.json +58 -4
- package/sql/0001_bexio_grants.sql +32 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
const COLUMNS = `id, owner_id, bexio_company_id, bexio_user_id, bexio_company_user_id,
|
|
2
|
+
company_name, user_name, user_email, role, status, access_token, refresh_token,
|
|
3
|
+
access_expires_at, refresh_expires_at, scope, refresh_locked_until,
|
|
4
|
+
last_refreshed_at, created_at, updated_at`;
|
|
5
|
+
function date(value) {
|
|
6
|
+
if (value === null)
|
|
7
|
+
return null;
|
|
8
|
+
return value instanceof Date ? value : new Date(value);
|
|
9
|
+
}
|
|
10
|
+
function toRecord(row) {
|
|
11
|
+
return {
|
|
12
|
+
id: row.id,
|
|
13
|
+
ownerId: row.owner_id,
|
|
14
|
+
companyId: row.bexio_company_id,
|
|
15
|
+
userId: row.bexio_user_id,
|
|
16
|
+
companyUserId: row.bexio_company_user_id === null
|
|
17
|
+
? null
|
|
18
|
+
: Number(row.bexio_company_user_id),
|
|
19
|
+
companyName: row.company_name,
|
|
20
|
+
userName: row.user_name,
|
|
21
|
+
userEmail: row.user_email,
|
|
22
|
+
role: row.role,
|
|
23
|
+
status: row.status,
|
|
24
|
+
accessToken: row.access_token,
|
|
25
|
+
refreshToken: row.refresh_token,
|
|
26
|
+
accessExpiresAt: date(row.access_expires_at),
|
|
27
|
+
refreshExpiresAt: date(row.refresh_expires_at),
|
|
28
|
+
scope: row.scope,
|
|
29
|
+
refreshLockedUntil: date(row.refresh_locked_until),
|
|
30
|
+
lastRefreshedAt: date(row.last_refreshed_at),
|
|
31
|
+
createdAt: date(row.created_at),
|
|
32
|
+
updatedAt: date(row.updated_at),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Grant store on the `bexio_grants` table (create it with
|
|
37
|
+
* `npx @soulcode/bexio-oauth sql`). Every operation is a single statement,
|
|
38
|
+
* so it works through transaction-mode poolers and HTTP drivers.
|
|
39
|
+
*/
|
|
40
|
+
export function postgresStore(sql) {
|
|
41
|
+
const one = async (text, params) => {
|
|
42
|
+
const { rows } = await sql.query(text, params);
|
|
43
|
+
return rows[0] ? toRecord(rows[0]) : null;
|
|
44
|
+
};
|
|
45
|
+
const many = async (text, params) => {
|
|
46
|
+
const { rows } = await sql.query(text, params);
|
|
47
|
+
return rows.map(toRecord);
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
async upsertGrant(g) {
|
|
51
|
+
const row = await one(`INSERT INTO bexio_grants (owner_id, bexio_company_id, bexio_user_id,
|
|
52
|
+
bexio_company_user_id, company_name, user_name, user_email, role, status,
|
|
53
|
+
access_token, refresh_token, access_expires_at, refresh_expires_at, scope,
|
|
54
|
+
refresh_locked_until, last_refreshed_at, created_at, updated_at)
|
|
55
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'active', $9, $10, $11, $12, $13, NULL, $14, $14, $14)
|
|
56
|
+
ON CONFLICT (owner_id, bexio_company_id, bexio_user_id) DO UPDATE SET
|
|
57
|
+
bexio_company_user_id = EXCLUDED.bexio_company_user_id,
|
|
58
|
+
company_name = EXCLUDED.company_name,
|
|
59
|
+
user_name = EXCLUDED.user_name,
|
|
60
|
+
user_email = EXCLUDED.user_email,
|
|
61
|
+
role = EXCLUDED.role,
|
|
62
|
+
status = 'active',
|
|
63
|
+
access_token = EXCLUDED.access_token,
|
|
64
|
+
refresh_token = EXCLUDED.refresh_token,
|
|
65
|
+
access_expires_at = EXCLUDED.access_expires_at,
|
|
66
|
+
refresh_expires_at = EXCLUDED.refresh_expires_at,
|
|
67
|
+
scope = EXCLUDED.scope,
|
|
68
|
+
refresh_locked_until = NULL,
|
|
69
|
+
refresh_lock_token = NULL,
|
|
70
|
+
last_refreshed_at = EXCLUDED.last_refreshed_at,
|
|
71
|
+
updated_at = EXCLUDED.updated_at
|
|
72
|
+
RETURNING ${COLUMNS}`, [
|
|
73
|
+
g.ownerId,
|
|
74
|
+
g.companyId,
|
|
75
|
+
g.userId,
|
|
76
|
+
g.companyUserId,
|
|
77
|
+
g.companyName,
|
|
78
|
+
g.userName,
|
|
79
|
+
g.userEmail,
|
|
80
|
+
g.role,
|
|
81
|
+
g.accessToken,
|
|
82
|
+
g.refreshToken,
|
|
83
|
+
g.accessExpiresAt,
|
|
84
|
+
g.refreshExpiresAt,
|
|
85
|
+
g.scope,
|
|
86
|
+
g.now,
|
|
87
|
+
]);
|
|
88
|
+
if (!row)
|
|
89
|
+
throw new Error("upsert returned no row");
|
|
90
|
+
return row;
|
|
91
|
+
},
|
|
92
|
+
async findGrants({ ownerId, companyId, grantId }) {
|
|
93
|
+
return many(`SELECT ${COLUMNS} FROM bexio_grants
|
|
94
|
+
WHERE owner_id = $1
|
|
95
|
+
AND ($2::text IS NULL OR bexio_company_id = $2)
|
|
96
|
+
AND ($3::uuid IS NULL OR id = $3::uuid)
|
|
97
|
+
ORDER BY created_at, id`, [ownerId, companyId ?? null, grantId ?? null]);
|
|
98
|
+
},
|
|
99
|
+
async getGrant(id) {
|
|
100
|
+
return one(`SELECT ${COLUMNS} FROM bexio_grants WHERE id = $1`, [id]);
|
|
101
|
+
},
|
|
102
|
+
async acquireRefreshLock({ id, now, lockUntil, lockToken, accessExpiresBefore, refreshedBefore, }) {
|
|
103
|
+
return one(`UPDATE bexio_grants SET refresh_locked_until = $3, refresh_lock_token = $6
|
|
104
|
+
WHERE id = $1
|
|
105
|
+
AND status = 'active'
|
|
106
|
+
AND (refresh_locked_until IS NULL OR refresh_locked_until <= $2)
|
|
107
|
+
AND (
|
|
108
|
+
($4::timestamptz IS NOT NULL AND (access_expires_at IS NULL OR access_expires_at <= $4))
|
|
109
|
+
OR ($5::timestamptz IS NOT NULL AND last_refreshed_at < $5)
|
|
110
|
+
)
|
|
111
|
+
RETURNING ${COLUMNS}`, [
|
|
112
|
+
id,
|
|
113
|
+
now,
|
|
114
|
+
lockUntil,
|
|
115
|
+
accessExpiresBefore ?? null,
|
|
116
|
+
refreshedBefore ?? null,
|
|
117
|
+
lockToken,
|
|
118
|
+
]);
|
|
119
|
+
},
|
|
120
|
+
// Holder writes are fenced by refresh_lock_token: a holder whose lock
|
|
121
|
+
// expired or was cleared by a reconnect matches no row and changes nothing.
|
|
122
|
+
async saveRefreshed(id, lockToken, r) {
|
|
123
|
+
return one(`UPDATE bexio_grants SET
|
|
124
|
+
access_token = $2,
|
|
125
|
+
refresh_token = COALESCE($3, refresh_token),
|
|
126
|
+
access_expires_at = $4,
|
|
127
|
+
refresh_expires_at = COALESCE($5, refresh_expires_at),
|
|
128
|
+
scope = COALESCE($6, scope),
|
|
129
|
+
role = COALESCE($7, role),
|
|
130
|
+
status = 'active',
|
|
131
|
+
refresh_locked_until = NULL,
|
|
132
|
+
refresh_lock_token = NULL,
|
|
133
|
+
last_refreshed_at = $8,
|
|
134
|
+
updated_at = $8
|
|
135
|
+
WHERE id = $1 AND refresh_lock_token = $9
|
|
136
|
+
RETURNING ${COLUMNS}`, [
|
|
137
|
+
id,
|
|
138
|
+
r.accessToken,
|
|
139
|
+
r.refreshToken,
|
|
140
|
+
r.accessExpiresAt,
|
|
141
|
+
r.refreshExpiresAt,
|
|
142
|
+
r.scope,
|
|
143
|
+
r.role,
|
|
144
|
+
r.now,
|
|
145
|
+
lockToken,
|
|
146
|
+
]);
|
|
147
|
+
},
|
|
148
|
+
async markReauthRequired(id, lockToken, now) {
|
|
149
|
+
await sql.query(`UPDATE bexio_grants SET status = 'reauth_required',
|
|
150
|
+
refresh_locked_until = NULL, refresh_lock_token = NULL, updated_at = $2
|
|
151
|
+
WHERE id = $1 AND refresh_lock_token = $3`, [id, now, lockToken]);
|
|
152
|
+
},
|
|
153
|
+
async releaseRefreshLock(id, lockToken) {
|
|
154
|
+
await sql.query(`UPDATE bexio_grants SET refresh_locked_until = NULL, refresh_lock_token = NULL
|
|
155
|
+
WHERE id = $1 AND refresh_lock_token = $2`, [id, lockToken]);
|
|
156
|
+
},
|
|
157
|
+
async deleteGrant(id) {
|
|
158
|
+
await sql.query(`DELETE FROM bexio_grants WHERE id = $1`, [id]);
|
|
159
|
+
},
|
|
160
|
+
async listIdleGrants({ refreshedBefore, limit }) {
|
|
161
|
+
return many(`SELECT ${COLUMNS} FROM bexio_grants
|
|
162
|
+
WHERE status = 'active' AND last_refreshed_at < $1
|
|
163
|
+
ORDER BY last_refreshed_at
|
|
164
|
+
LIMIT $2`, [refreshedBefore, limit]);
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=postgres.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../src/store/postgres.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Role } from "../permissions.js";
|
|
2
|
+
export type GrantStatus = "active" | "reauth_required";
|
|
3
|
+
/** A stored grant. Token fields hold JWE ciphertext, never plaintext. */
|
|
4
|
+
export interface GrantRecord {
|
|
5
|
+
id: string;
|
|
6
|
+
ownerId: string;
|
|
7
|
+
companyId: string;
|
|
8
|
+
userId: string;
|
|
9
|
+
companyUserId: number | null;
|
|
10
|
+
companyName: string | null;
|
|
11
|
+
userName: string | null;
|
|
12
|
+
userEmail: string | null;
|
|
13
|
+
role: Role;
|
|
14
|
+
status: GrantStatus;
|
|
15
|
+
accessToken: string;
|
|
16
|
+
refreshToken: string | null;
|
|
17
|
+
accessExpiresAt: Date | null;
|
|
18
|
+
refreshExpiresAt: Date | null;
|
|
19
|
+
scope: string | null;
|
|
20
|
+
refreshLockedUntil: Date | null;
|
|
21
|
+
lastRefreshedAt: Date;
|
|
22
|
+
createdAt: Date;
|
|
23
|
+
updatedAt: Date;
|
|
24
|
+
}
|
|
25
|
+
/** Insert, or overwrite the tokens of, the grant for (owner, company, user). */
|
|
26
|
+
export interface GrantUpsert {
|
|
27
|
+
ownerId: string;
|
|
28
|
+
companyId: string;
|
|
29
|
+
userId: string;
|
|
30
|
+
companyUserId: number | null;
|
|
31
|
+
companyName: string | null;
|
|
32
|
+
userName: string | null;
|
|
33
|
+
userEmail: string | null;
|
|
34
|
+
role: Role;
|
|
35
|
+
accessToken: string;
|
|
36
|
+
refreshToken: string | null;
|
|
37
|
+
accessExpiresAt: Date | null;
|
|
38
|
+
refreshExpiresAt: Date | null;
|
|
39
|
+
scope: string | null;
|
|
40
|
+
now: Date;
|
|
41
|
+
}
|
|
42
|
+
export interface GrantRefresh {
|
|
43
|
+
accessToken: string;
|
|
44
|
+
/** `null` keeps the stored refresh token (the IdP didn't rotate it). */
|
|
45
|
+
refreshToken: string | null;
|
|
46
|
+
accessExpiresAt: Date | null;
|
|
47
|
+
/** `null` keeps the stored value. */
|
|
48
|
+
refreshExpiresAt: Date | null;
|
|
49
|
+
/** `null` keeps the stored value. */
|
|
50
|
+
scope: string | null;
|
|
51
|
+
/** `null` keeps the stored role. */
|
|
52
|
+
role: Role | null;
|
|
53
|
+
now: Date;
|
|
54
|
+
}
|
|
55
|
+
export interface LockRequest {
|
|
56
|
+
id: string;
|
|
57
|
+
now: Date;
|
|
58
|
+
lockUntil: Date;
|
|
59
|
+
/**
|
|
60
|
+
* Random value identifying this lock holder. Writes that need the lock pass
|
|
61
|
+
* it back, so a holder whose lock expired (or was cleared by a reconnect)
|
|
62
|
+
* can't overwrite newer data.
|
|
63
|
+
*/
|
|
64
|
+
lockToken: string;
|
|
65
|
+
/** Lock only if the access token expires before this instant (lazy refresh)… */
|
|
66
|
+
accessExpiresBefore?: Date;
|
|
67
|
+
/** …or only if the grant was last refreshed before this instant (idle sweep). */
|
|
68
|
+
refreshedBefore?: Date;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Persistence for grants. Every method must be atomic on its own: the
|
|
72
|
+
* library never needs multi-statement transactions.
|
|
73
|
+
*/
|
|
74
|
+
export interface GrantStore {
|
|
75
|
+
upsertGrant(input: GrantUpsert): Promise<GrantRecord>;
|
|
76
|
+
findGrants(query: {
|
|
77
|
+
ownerId: string;
|
|
78
|
+
companyId?: string;
|
|
79
|
+
grantId?: string;
|
|
80
|
+
}): Promise<GrantRecord[]>;
|
|
81
|
+
getGrant(id: string): Promise<GrantRecord | null>;
|
|
82
|
+
/**
|
|
83
|
+
* Takes the refresh lock if the grant is active, not locked (or the lock
|
|
84
|
+
* expired), and still matches the `accessExpiresBefore` / `refreshedBefore`
|
|
85
|
+
* condition. Returns the locked row, or `null` if someone else holds the
|
|
86
|
+
* lock or the grant no longer needs a refresh.
|
|
87
|
+
*/
|
|
88
|
+
acquireRefreshLock(request: LockRequest): Promise<GrantRecord | null>;
|
|
89
|
+
/**
|
|
90
|
+
* Stores refreshed tokens, sets `lastRefreshedAt`, and releases the lock.
|
|
91
|
+
* Returns `null` (and changes nothing) unless `lockToken` still holds it.
|
|
92
|
+
*/
|
|
93
|
+
saveRefreshed(id: string, lockToken: string, refresh: GrantRefresh): Promise<GrantRecord | null>;
|
|
94
|
+
/** Sets `status = reauth_required` and releases the lock, if `lockToken` holds it. */
|
|
95
|
+
markReauthRequired(id: string, lockToken: string, now: Date): Promise<void>;
|
|
96
|
+
/** Releases the lock, if `lockToken` holds it. */
|
|
97
|
+
releaseRefreshLock(id: string, lockToken: string): Promise<void>;
|
|
98
|
+
deleteGrant(id: string): Promise<void>;
|
|
99
|
+
/** Active grants last refreshed before `refreshedBefore`, oldest first. */
|
|
100
|
+
listIdleGrants(query: {
|
|
101
|
+
refreshedBefore: Date;
|
|
102
|
+
limit: number;
|
|
103
|
+
}): Promise<GrantRecord[]>;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soulcode/bexio-oauth",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Bexio OAuth token custody for Node apps: connect Bexio accounts, store tokens encrypted in Postgres, get a valid access token anywhere.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bexio",
|
|
7
|
+
"oauth",
|
|
8
|
+
"oidc",
|
|
9
|
+
"openid-connect",
|
|
10
|
+
"postgres",
|
|
11
|
+
"token"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/SoulcodeAgency/bexio-oauth.git",
|
|
17
|
+
"directory": "packages/bexio-oauth"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/SoulcodeAgency/bexio-oauth/blob/main/docs/integration.md",
|
|
20
|
+
"bugs": "https://github.com/SoulcodeAgency/bexio-oauth/issues",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"bin": {
|
|
30
|
+
"bexio-oauth": "./dist/bin.js"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"sql"
|
|
35
|
+
],
|
|
36
|
+
"sideEffects": false,
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=24"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc -p tsconfig.build.json",
|
|
42
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:watch": "vitest",
|
|
45
|
+
"prepack": "npm run build"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"jose": "^6.2.13",
|
|
49
|
+
"openid-client": "^6.8.8"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/pg": "8.23.1",
|
|
53
|
+
"mock-bexio": "*",
|
|
54
|
+
"pg": "8.23.1",
|
|
55
|
+
"vitest": "5.0.5"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
-- @soulcode/bexio-oauth 0001: the grants table.
|
|
2
|
+
-- One row per (owner, Bexio company, Bexio user). Token columns hold JWE
|
|
3
|
+
-- ciphertext; only your server-side database role should be able to read them.
|
|
4
|
+
|
|
5
|
+
CREATE TABLE IF NOT EXISTS bexio_grants (
|
|
6
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
7
|
+
owner_id text NOT NULL,
|
|
8
|
+
bexio_company_id text NOT NULL,
|
|
9
|
+
bexio_user_id text NOT NULL,
|
|
10
|
+
bexio_company_user_id integer,
|
|
11
|
+
company_name text,
|
|
12
|
+
user_name text,
|
|
13
|
+
user_email text,
|
|
14
|
+
role text NOT NULL DEFAULT 'member' CHECK (role IN ('admin', 'member')),
|
|
15
|
+
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'reauth_required')),
|
|
16
|
+
access_token text NOT NULL,
|
|
17
|
+
refresh_token text,
|
|
18
|
+
access_expires_at timestamptz,
|
|
19
|
+
refresh_expires_at timestamptz,
|
|
20
|
+
scope text,
|
|
21
|
+
refresh_locked_until timestamptz,
|
|
22
|
+
refresh_lock_token text,
|
|
23
|
+
last_refreshed_at timestamptz NOT NULL DEFAULT now(),
|
|
24
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
25
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
26
|
+
CONSTRAINT bexio_grants_owner_company_user_key UNIQUE (owner_id, bexio_company_id, bexio_user_id)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
-- refreshIdle(): active grants by last refresh.
|
|
30
|
+
CREATE INDEX IF NOT EXISTS bexio_grants_idle_idx
|
|
31
|
+
ON bexio_grants (last_refreshed_at)
|
|
32
|
+
WHERE status = 'active';
|