prostgles-server 4.2.642 → 4.2.644
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/debug/caught-request-fix-build.log +4 -0
- package/debug/caught-request-fix-test-build.log +0 -0
- package/debug/caught-request-subscription-failure.log +603 -0
- package/debug/caught-validation-build.log +4 -0
- package/debug/caught-validation-fix-build.log +4 -0
- package/debug/caught-validation-fix-test-build.log +0 -0
- package/debug/caught-validation-test-build.log +0 -0
- package/debug/client-db-tx-build.log +4 -0
- package/debug/client-db-tx-test-server.log +3248 -0
- package/debug/client-db-tx-tests-build.log +0 -0
- package/debug/client-handler-shape-build.log +4 -0
- package/debug/client-handler-shape-test-server.log +544 -0
- package/debug/client-handler-shape-tests-build.log +0 -0
- package/debug/column-definitions-test-server.log +152 -164
- package/debug/fixes-review-build.log +4 -0
- package/debug/fixes-review-client-schema.backup +745 -0
- package/debug/fixes-review-generated-schema.backup +489 -0
- package/debug/fixes-review-probes-build.log +0 -0
- package/debug/fixes-review-probes.patch +43 -0
- package/debug/fixes-review-restore-build.log +0 -0
- package/debug/fixes-review-tests-build.log +0 -0
- package/debug/fixes-review-typecheck.log +8 -0
- package/debug/function-client-db-tx-build.log +4 -0
- package/debug/function-client-db-tx-tests-build.log +0 -0
- package/debug/review-current-fixes-build.log +4 -0
- package/debug/review-current-fixes-client.backup +745 -0
- package/debug/review-current-fixes-probes-build.log +0 -0
- package/debug/review-current-fixes-probes.patch +55 -0
- package/debug/review-current-fixes-restore-build.log +0 -0
- package/debug/review-current-fixes-rls.backup +478 -0
- package/debug/review-current-fixes-schema.backup +489 -0
- package/debug/review-current-fixes-test-build.log +0 -0
- package/debug/review-current-fixes-typecheck.log +8 -0
- package/debug/review-user-identity-build.log +4 -0
- package/debug/review-user-identity-probes-build.log +0 -0
- package/debug/review-user-identity-probes.log +558 -0
- package/debug/review-user-identity-probes.patch +45 -0
- package/debug/review-user-identity-restore-build.log +0 -0
- package/debug/review-user-identity-rls.backup +463 -0
- package/debug/review-user-identity-schema.backup +720 -0
- package/debug/review-user-identity-server.log +3248 -0
- package/debug/review-user-identity-test-build.log +0 -0
- package/debug/review-user-identity-typecheck.log +8 -0
- package/debug/schema-types-focused-strict-check.log +0 -0
- package/debug/schema-types-strict-check.log +38 -0
- package/debug/schema-types-tests-build.log +0 -0
- package/debug/security-review-baseline-tests.log +3250 -0
- package/debug/security-review-build.log +4 -0
- package/debug/security-review-checkfilter-repro.log +545 -0
- package/debug/security-review-clientSchemaTypes.spec.ts.backup +553 -0
- package/debug/security-review-repro-build.log +0 -0
- package/debug/security-review-repro.spec.ts +591 -0
- package/debug/security-review-restore-build.log +0 -0
- package/debug/security-review-server-tests.log +3254 -0
- package/debug/security-review-test-build.log +0 -0
- package/debug/server-user-lint.log +4 -0
- package/debug/server-user-test-server.log +538 -0
- package/debug/server-user-typecheck.log +8 -0
- package/debug/test-check-eslint.log +6 -0
- package/debug/test-check-lint-baseline.json +1 -0
- package/debug/test-check-npm-test.log +789 -0
- package/debug/test-check-types.log +8 -0
- package/debug/test-config-npm-test.log +223 -0
- package/debug/test-config-strict-null-check.log +50 -0
- package/debug/test-config-typecheck.log +8 -0
- package/debug/tryRunP-test-quick.log +78 -0
- package/debug/tryRunP-typecheck.log +42 -0
- package/debug/user-identity-direct-call-build.log +0 -0
- package/debug/user-identity-direct-call-schema.backup +489 -0
- package/debug/user-identity-regressions-build.log +4 -0
- package/debug/user-identity-regressions-generated-schema.backup +489 -0
- package/debug/user-identity-regressions-test-build.log +0 -0
- package/dist/Auth/AuthHandler.js +1 -1
- package/dist/Auth/AuthHandler.js.map +1 -1
- package/dist/Auth/AuthTypes.d.ts +11 -9
- package/dist/Auth/AuthTypes.d.ts.map +1 -1
- package/dist/Auth/AuthTypes.js.map +1 -1
- package/dist/Auth/utils/getClientRequestIPsInfo.d.ts +2 -1
- package/dist/Auth/utils/getClientRequestIPsInfo.d.ts.map +1 -1
- package/dist/Auth/utils/getClientRequestIPsInfo.js +8 -1
- package/dist/Auth/utils/getClientRequestIPsInfo.js.map +1 -1
- package/dist/Auth/utils/getSessionUser.d.ts +4 -3
- package/dist/Auth/utils/getSessionUser.d.ts.map +1 -1
- package/dist/Auth/utils/getSessionUser.js +4 -2
- package/dist/Auth/utils/getSessionUser.js.map +1 -1
- package/dist/Auth/utils/getSessionUser.spec.js +2 -4
- package/dist/Auth/utils/getSessionUser.spec.js.map +1 -1
- package/dist/Auth/utils/handleGetUser.d.ts.map +1 -1
- package/dist/Auth/utils/handleGetUser.js +16 -0
- package/dist/Auth/utils/handleGetUser.js.map +1 -1
- package/dist/Auth/utils/serverSideRequest.d.ts +17 -0
- package/dist/Auth/utils/serverSideRequest.d.ts.map +1 -0
- package/dist/Auth/utils/serverSideRequest.js +20 -0
- package/dist/Auth/utils/serverSideRequest.js.map +1 -0
- package/dist/DboBuilder/DboBuilder.d.ts.map +1 -1
- package/dist/DboBuilder/DboBuilder.js +4 -2
- package/dist/DboBuilder/DboBuilder.js.map +1 -1
- package/dist/DboBuilder/TableHandler/TableHandler.d.ts +1 -1
- package/dist/DboBuilder/ViewHandler/ViewHandler.d.ts +0 -4
- package/dist/DboBuilder/ViewHandler/ViewHandler.d.ts.map +1 -1
- package/dist/DboBuilder/ViewHandler/ViewHandler.js +5 -8
- package/dist/DboBuilder/ViewHandler/ViewHandler.js.map +1 -1
- package/dist/DboBuilder/ViewHandler/count.d.ts.map +1 -1
- package/dist/DboBuilder/ViewHandler/count.js +2 -1
- package/dist/DboBuilder/ViewHandler/count.js.map +1 -1
- package/dist/DboBuilder/ViewHandler/find.d.ts.map +1 -1
- package/dist/DboBuilder/ViewHandler/find.js +2 -1
- package/dist/DboBuilder/ViewHandler/find.js.map +1 -1
- package/dist/DboBuilder/ViewHandler/getDbHandlerWithAbort.d.ts +9 -2
- package/dist/DboBuilder/ViewHandler/getDbHandlerWithAbort.d.ts.map +1 -1
- package/dist/DboBuilder/ViewHandler/getDbHandlerWithAbort.js +57 -23
- package/dist/DboBuilder/ViewHandler/getDbHandlerWithAbort.js.map +1 -1
- package/dist/DboBuilder/ViewHandler/size.d.ts.map +1 -1
- package/dist/DboBuilder/ViewHandler/size.js +2 -1
- package/dist/DboBuilder/ViewHandler/size.js.map +1 -1
- package/dist/DboBuilder/dboBuilderUtils.d.ts +1 -0
- package/dist/DboBuilder/dboBuilderUtils.d.ts.map +1 -1
- package/dist/DboBuilder/dboBuilderUtils.js +2 -1
- package/dist/DboBuilder/dboBuilderUtils.js.map +1 -1
- package/dist/PublishParser/PublishParser.d.ts.map +1 -1
- package/dist/PublishParser/PublishParser.js +2 -1
- package/dist/PublishParser/PublishParser.js.map +1 -1
- package/dist/PublishParser/defineServerFunction.d.ts +2 -0
- package/dist/PublishParser/defineServerFunction.d.ts.map +1 -1
- package/dist/PublishParser/defineServerFunction.js.map +1 -1
- package/dist/PublishParser/getPublishedTableNames.d.ts.map +1 -1
- package/dist/PublishParser/getPublishedTableNames.js +2 -1
- package/dist/PublishParser/getPublishedTableNames.js.map +1 -1
- package/dist/PublishParser/publishTypesAndUtils.d.ts +1 -1
- package/dist/PublishParser/publishTypesAndUtils.d.ts.map +1 -1
- package/dist/StorageClient/StorageClientTypes.d.ts +5 -4
- package/dist/StorageClient/StorageClientTypes.d.ts.map +1 -1
- package/dist/WebsocketAPI/getClientHandlers.d.ts +5 -0
- package/dist/WebsocketAPI/getClientHandlers.d.ts.map +1 -1
- package/dist/WebsocketAPI/getClientHandlers.js +33 -14
- package/dist/WebsocketAPI/getClientHandlers.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/initProstgles.d.ts +12 -2
- package/dist/initProstgles.d.ts.map +1 -1
- package/dist/initProstgles.js +10 -1
- package/dist/initProstgles.js.map +1 -1
- package/dist/runClientRequest.d.ts +2 -1
- package/dist/runClientRequest.d.ts.map +1 -1
- package/dist/runClientRequest.js +7 -4
- package/dist/runClientRequest.js.map +1 -1
- package/eslint.config.mjs +21 -1
- package/lib/Auth/AuthHandler.ts +1 -1
- package/lib/Auth/AuthTypes.ts +12 -8
- package/lib/Auth/utils/getClientRequestIPsInfo.ts +11 -2
- package/lib/Auth/utils/getSessionUser.spec.ts +4 -6
- package/lib/Auth/utils/getSessionUser.ts +8 -5
- package/lib/Auth/utils/handleGetUser.ts +13 -0
- package/lib/Auth/utils/serverSideRequest.ts +27 -0
- package/lib/DboBuilder/DboBuilder.ts +7 -2
- package/lib/DboBuilder/ViewHandler/ViewHandler.ts +11 -11
- package/lib/DboBuilder/ViewHandler/count.ts +2 -1
- package/lib/DboBuilder/ViewHandler/find.ts +2 -1
- package/lib/DboBuilder/ViewHandler/getDbHandlerWithAbort.ts +72 -29
- package/lib/DboBuilder/ViewHandler/size.ts +2 -1
- package/lib/DboBuilder/dboBuilderUtils.ts +2 -0
- package/lib/PublishParser/PublishParser.ts +2 -1
- package/lib/PublishParser/defineServerFunction.ts +3 -5
- package/lib/PublishParser/getPublishedTableNames.ts +2 -1
- package/lib/PublishParser/publishTypesAndUtils.ts +1 -1
- package/lib/StorageClient/StorageClientTypes.ts +5 -4
- package/lib/WebsocketAPI/getClientHandlers.ts +63 -29
- package/lib/index.ts +3 -3
- package/lib/initProstgles.ts +31 -5
- package/lib/runClientRequest.ts +10 -3
- package/package.json +5 -3
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { once } from "node:events";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { createServer } from "node:http";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { test } from "node:test";
|
|
9
|
+
import prostgles, {
|
|
10
|
+
DB_GENERATED_NAMES,
|
|
11
|
+
defineFunction,
|
|
12
|
+
getLocalStorageClient,
|
|
13
|
+
type ProstglesInitOptions,
|
|
14
|
+
type PublishContextValue,
|
|
15
|
+
} from "prostgles-server";
|
|
16
|
+
import { getConnectionDetails } from "prostgles-server/dist/DboBuilder/runSql/getAdminClient";
|
|
17
|
+
import type { ViewHandler } from "prostgles-server/dist/DboBuilder/ViewHandler/ViewHandler";
|
|
18
|
+
import type { DB } from "prostgles-server/dist/Prostgles";
|
|
19
|
+
import { CHANNELS, type AnyObject, type ClientSchema } from "prostgles-types";
|
|
20
|
+
import { Server } from "socket.io";
|
|
21
|
+
import { io as createClient, type Socket } from "socket.io-client";
|
|
22
|
+
import ts from "typescript";
|
|
23
|
+
|
|
24
|
+
export const testClientSchemaTypes = async (db: DB) => {
|
|
25
|
+
await test(
|
|
26
|
+
"client publish profiles compile at startup and follow user types",
|
|
27
|
+
{ timeout: 30000 },
|
|
28
|
+
async (t) => {
|
|
29
|
+
const app = express();
|
|
30
|
+
app.use(express.json());
|
|
31
|
+
app.use((req, _res, next) => {
|
|
32
|
+
req.cookies = Object.fromEntries(
|
|
33
|
+
(req.headers.cookie?.split(";") ?? []).map((cookie) => cookie.trim().split("=")),
|
|
34
|
+
);
|
|
35
|
+
next();
|
|
36
|
+
});
|
|
37
|
+
const http = createServer(app);
|
|
38
|
+
const io = new Server(http);
|
|
39
|
+
const sockets: Socket[] = [];
|
|
40
|
+
let instance:
|
|
41
|
+
| Pick<
|
|
42
|
+
Awaited<ReturnType<typeof prostgles>>,
|
|
43
|
+
| "update"
|
|
44
|
+
| "db"
|
|
45
|
+
| "destroy"
|
|
46
|
+
| "getTSSchema"
|
|
47
|
+
| "reWriteDBSchema"
|
|
48
|
+
| "options"
|
|
49
|
+
| "getClientDBHandlers"
|
|
50
|
+
>
|
|
51
|
+
| undefined;
|
|
52
|
+
const suffix = randomUUID().slice(0, 8);
|
|
53
|
+
const schemaName = `client_schema_types_${suffix}`;
|
|
54
|
+
const tableName = "correspondence";
|
|
55
|
+
const privateTableName = "private_table";
|
|
56
|
+
const fileTableName = "files";
|
|
57
|
+
const generatedTypesDir = path.resolve("debug/client-schema-types", suffix);
|
|
58
|
+
try {
|
|
59
|
+
await db.none(`
|
|
60
|
+
CREATE SCHEMA ${schemaName};
|
|
61
|
+
CREATE TABLE ${schemaName}.${tableName} (
|
|
62
|
+
id SERIAL PRIMARY KEY,
|
|
63
|
+
body TEXT NOT NULL,
|
|
64
|
+
created_by TEXT NOT NULL,
|
|
65
|
+
note TEXT,
|
|
66
|
+
internal TEXT NOT NULL DEFAULT 'default',
|
|
67
|
+
synced BIGINT NOT NULL
|
|
68
|
+
);
|
|
69
|
+
CREATE TABLE ${schemaName}.${privateTableName} (id SERIAL PRIMARY KEY);
|
|
70
|
+
`);
|
|
71
|
+
http.listen(0, "127.0.0.1");
|
|
72
|
+
await once(http, "listening");
|
|
73
|
+
const address = http.address();
|
|
74
|
+
assert(address && typeof address === "object");
|
|
75
|
+
const userIdContextValue = {
|
|
76
|
+
$prostglesContext: { objectName: "user", objectPropertyName: "id" },
|
|
77
|
+
} as const satisfies PublishContextValue<{ user: { id: string } }>;
|
|
78
|
+
const serverUsers = new Map([
|
|
79
|
+
["user-guest", { id: "user-guest", type: "guest" }],
|
|
80
|
+
["user-admin", { id: "user-admin", type: "admin" }],
|
|
81
|
+
]);
|
|
82
|
+
instance = await prostgles({
|
|
83
|
+
dbConnection: {
|
|
84
|
+
...getConnectionDetails(db),
|
|
85
|
+
options: `-c search_path=${schemaName}`,
|
|
86
|
+
} as unknown as ProstglesInitOptions["dbConnection"],
|
|
87
|
+
tsGeneratedTypesDir: generatedTypesDir,
|
|
88
|
+
transactions: true,
|
|
89
|
+
schemaFilter: { [schemaName]: 1 },
|
|
90
|
+
tableConfig: {
|
|
91
|
+
[tableName]: {
|
|
92
|
+
syncConfig: { id_fields: ["id"], synced_field: "synced" },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
fileTable: {
|
|
96
|
+
tableName: fileTableName,
|
|
97
|
+
expressApp: express(),
|
|
98
|
+
storageClient: getLocalStorageClient({
|
|
99
|
+
localFolderPath: path.join(generatedTypesDir, "files"),
|
|
100
|
+
}),
|
|
101
|
+
},
|
|
102
|
+
io,
|
|
103
|
+
restApi: { expressApp: app, path: "/api" },
|
|
104
|
+
publishRawSQL: ({ user }) => user?.type === "admin",
|
|
105
|
+
auth: {
|
|
106
|
+
sidKeyName: "token",
|
|
107
|
+
getUser: (sid) => {
|
|
108
|
+
if (!sid) return undefined;
|
|
109
|
+
return {
|
|
110
|
+
user: { id: `user-${sid}`, type: sid },
|
|
111
|
+
clientUser: { id: sid, type: sid },
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
findUser: (filter) =>
|
|
115
|
+
"id" in filter && typeof filter.id === "string" ? serverUsers.get(filter.id)
|
|
116
|
+
: (
|
|
117
|
+
"$and" in filter &&
|
|
118
|
+
filter.$and?.every((condition: AnyObject) => condition.id === "user-guest")
|
|
119
|
+
) ?
|
|
120
|
+
{ id: "user-guest", type: "guest" }
|
|
121
|
+
: undefined,
|
|
122
|
+
},
|
|
123
|
+
functions: {
|
|
124
|
+
guest: {
|
|
125
|
+
userFilter: { id: "user-guest" },
|
|
126
|
+
functions: {
|
|
127
|
+
transactionTest: defineFunction({
|
|
128
|
+
input: { fail: "boolean" },
|
|
129
|
+
run: ({ fail }, { withClientDbTx }) =>
|
|
130
|
+
withClientDbTx(async (tx) => {
|
|
131
|
+
const row = await tx[tableName]!.insert!(
|
|
132
|
+
{ body: fail ? "function-rollback" : "function-commit" },
|
|
133
|
+
{ returning: "*" },
|
|
134
|
+
);
|
|
135
|
+
if (fail) {
|
|
136
|
+
await tx[tableName]!.update!({ id: row.id }, { internal: "forbidden" });
|
|
137
|
+
}
|
|
138
|
+
return row;
|
|
139
|
+
}),
|
|
140
|
+
}),
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
publish: [
|
|
145
|
+
{
|
|
146
|
+
name: "GuestDBSchema",
|
|
147
|
+
userTypes: ["guest", "member"],
|
|
148
|
+
publish: {
|
|
149
|
+
[fileTableName]: { select: "*", insert: "*", update: "*" },
|
|
150
|
+
[tableName]: {
|
|
151
|
+
select: {
|
|
152
|
+
fields: "*",
|
|
153
|
+
forcedFilter: {
|
|
154
|
+
created_by: userIdContextValue as unknown as string,
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
update: {
|
|
158
|
+
fields: ["body", "note", "created_by", "synced"],
|
|
159
|
+
disableMethods: { updateBatch: 1 },
|
|
160
|
+
postValidate: ({ row }) => {
|
|
161
|
+
if (row.body === "denied-by-post-validate") {
|
|
162
|
+
throw new Error("Update rejected by postValidate");
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
forcedData: {
|
|
166
|
+
created_by: userIdContextValue,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
insert: {
|
|
170
|
+
fields: { internal: 0 },
|
|
171
|
+
checkFilter: { body: { $ne: "denied-by-check-filter" } },
|
|
172
|
+
forcedData: {
|
|
173
|
+
created_by: userIdContextValue,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: "AdminDBSchema",
|
|
181
|
+
userTypes: ["admin"],
|
|
182
|
+
publish: {
|
|
183
|
+
[tableName]: { select: "*", insert: "*", update: "*" },
|
|
184
|
+
[privateTableName]: { select: "*" },
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "ViewerDBSchema",
|
|
189
|
+
userTypes: ["viewer"],
|
|
190
|
+
publish: { [tableName]: { select: "*" } },
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
onReady: () => {},
|
|
194
|
+
});
|
|
195
|
+
const { tsSchema } = await instance.getTSSchema();
|
|
196
|
+
assert.doesNotMatch(tsSchema, /\bimport(?:\s|\()/);
|
|
197
|
+
assert.equal(
|
|
198
|
+
readFileSync(path.join(generatedTypesDir, `${DB_GENERATED_NAMES.SCHEMA}.ts`), "utf8"),
|
|
199
|
+
tsSchema,
|
|
200
|
+
);
|
|
201
|
+
await instance.reWriteDBSchema();
|
|
202
|
+
assert.equal((await instance.getTSSchema()).tsSchema, tsSchema);
|
|
203
|
+
assert.equal(io.sockets.sockets.size, 0);
|
|
204
|
+
checkTypes(
|
|
205
|
+
tsSchema,
|
|
206
|
+
`
|
|
207
|
+
import type { TableHandler, InsertDataWithNested, ClientSchemaFor } from "prostgles-types";
|
|
208
|
+
import type { DBHandlerClient } from "prostgles-client";
|
|
209
|
+
import type { DBOFullyTypedClient, InitResult, SessionUser } from "../server/node_modules/prostgles-server";
|
|
210
|
+
import type { RestrictedFunctionContext, UnrestrictedFunctionContext } from "../server/node_modules/prostgles-server/dist/PublishParser/defineServerFunction";
|
|
211
|
+
import type { getClientHandlers } from "../server/node_modules/prostgles-server/dist/WebsocketAPI/getClientHandlers";
|
|
212
|
+
type Name = "${tableName}";
|
|
213
|
+
type FileName = "${fileTableName}";
|
|
214
|
+
declare const guest: TableHandler<GuestDBSchema, Name>;
|
|
215
|
+
declare const files: TableHandler<GuestDBSchema, FileName>;
|
|
216
|
+
declare const admin: TableHandler<AdminDBSchema, Name>;
|
|
217
|
+
declare const combined: TableHandler<ClientDBSchema, Name>;
|
|
218
|
+
declare const server: TableHandler<${DB_GENERATED_NAMES.SCHEMA}, Name>;
|
|
219
|
+
declare const viewer: TableHandler<ViewerDBSchema, Name>;
|
|
220
|
+
declare const client: DBHandlerClient<ClientDBSchema>;
|
|
221
|
+
declare const adminClient: DBHandlerClient<AdminDBSchema>;
|
|
222
|
+
declare const restricted: RestrictedFunctionContext<GuestDBSchema>;
|
|
223
|
+
declare const unrestricted: UnrestrictedFunctionContext<DBGeneratedSchema>;
|
|
224
|
+
declare const serverClient: DBOFullyTypedClient<ClientDBSchema>;
|
|
225
|
+
declare const handlers: Awaited<ReturnType<typeof getClientHandlers<GuestDBSchema>>>;
|
|
226
|
+
declare const instance: InitResult<DBGeneratedSchema, SessionUser, undefined, ClientDBSchema>;
|
|
227
|
+
declare const guestClientSchema: ClientSchemaFor<ClientSchemas, "guest">;
|
|
228
|
+
guestClientSchema satisfies GuestDBSchema;
|
|
229
|
+
async () => {
|
|
230
|
+
const combinedHandlers = await instance.getClientDBHandlers({ userId: "user-admin" }, undefined);
|
|
231
|
+
const guestHandlers = await instance.getClientDBHandlers<GuestDBSchema>({ userId: "user-guest" }, undefined);
|
|
232
|
+
await combinedHandlers.clientDb["${privateTableName}"]?.find();
|
|
233
|
+
// @ts-expect-error the guest profile does not publish private_table
|
|
234
|
+
await guestHandlers.clientDb["${privateTableName}"].find();
|
|
235
|
+
const restrictedRow = await restricted.dbo["${tableName}"].insert({ body: "hello" }, { returning: "*" });
|
|
236
|
+
restrictedRow.created_by satisfies string;
|
|
237
|
+
await handlers.clientDb["${tableName}"].update({}, { body: "changed" });
|
|
238
|
+
const transactionResult = await handlers.withClientDbTx(async (tx) => {
|
|
239
|
+
const row = await tx["${tableName}"].insert({ body: "transaction" }, { returning: "*" });
|
|
240
|
+
row.created_by satisfies string;
|
|
241
|
+
// @ts-expect-error transactions preserve profile field restrictions
|
|
242
|
+
await tx["${tableName}"].update({}, { internal: "private" });
|
|
243
|
+
// @ts-expect-error transactions do not expose unpublished tables
|
|
244
|
+
await tx["${privateTableName}"].find();
|
|
245
|
+
// @ts-expect-error transactions do not expose raw SQL
|
|
246
|
+
tx.sql("SELECT 1");
|
|
247
|
+
// @ts-expect-error transactions do not expose nested transactions
|
|
248
|
+
tx.tx(() => {});
|
|
249
|
+
return row.id;
|
|
250
|
+
});
|
|
251
|
+
transactionResult satisfies number;
|
|
252
|
+
restricted.withClientDbTx satisfies typeof handlers.withClientDbTx;
|
|
253
|
+
// @ts-expect-error callbacks do not receive the unrestricted SQL transaction
|
|
254
|
+
handlers.withClientDbTx((tx, rawTx) => rawTx.any("SELECT 1"));
|
|
255
|
+
// @ts-expect-error getClientHandlers preserves profile field restrictions
|
|
256
|
+
await handlers.clientDb["${tableName}"].update({}, { internal: "private" });
|
|
257
|
+
// @ts-expect-error restricted functions respect forced insert fields
|
|
258
|
+
await restricted.dbo["${tableName}"].insert({ body: "hello", created_by: "other" });
|
|
259
|
+
// @ts-expect-error restricted functions cannot start transactions
|
|
260
|
+
restricted.dbo.tx(() => {});
|
|
261
|
+
// @ts-expect-error client wrappers do not expose isView
|
|
262
|
+
restricted.dbo["${tableName}"].isView;
|
|
263
|
+
// @ts-expect-error table is absent for some profiles
|
|
264
|
+
await serverClient["${privateTableName}"].find();
|
|
265
|
+
await serverClient["${privateTableName}"]?.find();
|
|
266
|
+
await unrestricted.dbo.tx(async (tx) => {
|
|
267
|
+
await tx["${tableName}"].update({}, { internal: "private" });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
await client["${tableName}"].update({}, { body: "changed" });
|
|
271
|
+
await guest.update({}, { body: 123 });
|
|
272
|
+
// @ts-expect-error table is absent for guests and viewers
|
|
273
|
+
await client["${privateTableName}"].find();
|
|
274
|
+
await client["${privateTableName}"]?.find();
|
|
275
|
+
await adminClient["${privateTableName}"].find();
|
|
276
|
+
const updated = await guest.update({}, { body: "changed", note: null }, { returning: "*" });
|
|
277
|
+
updated?.[0]?.created_by satisfies string | undefined;
|
|
278
|
+
await guest.updateBatch([[{}, { body: "changed" }]]);
|
|
279
|
+
// @ts-expect-error excluded update field
|
|
280
|
+
await guest.update({}, { internal: "private" });
|
|
281
|
+
const forcedUpdate = { body: "changed", created_by: "other" };
|
|
282
|
+
// @ts-expect-error forced update field through a variable
|
|
283
|
+
await guest.update({}, forcedUpdate);
|
|
284
|
+
// @ts-expect-error casts must not bypass excluded fields
|
|
285
|
+
await guest.update({}, { created_by: { $merge: [] } });
|
|
286
|
+
// @ts-expect-error updateBatch respects allowed fields
|
|
287
|
+
await guest.updateBatch([[{}, { internal: "private" }]]);
|
|
288
|
+
// @ts-expect-error read-only profiles cannot update
|
|
289
|
+
await viewer.update({}, {});
|
|
290
|
+
await admin.update({}, { internal: "private" });
|
|
291
|
+
await combined.update({}, { internal: "private" });
|
|
292
|
+
await server.update({}, { internal: "private" });
|
|
293
|
+
const row = await guest.insert({ body: "hello", note: null }, { returning: "*" });
|
|
294
|
+
row.created_by satisfies string;
|
|
295
|
+
row.synced satisfies string;
|
|
296
|
+
await guest.insertMany([{ body: "hello" }]);
|
|
297
|
+
// @ts-expect-error excluded fields cannot be inserted
|
|
298
|
+
await guest.insert({ body: "hello", internal: "private" });
|
|
299
|
+
// @ts-expect-error a read-only profile cannot insert
|
|
300
|
+
await viewer.insert({ body: "hello" });
|
|
301
|
+
// @ts-expect-error body remains required
|
|
302
|
+
await guest.insert({});
|
|
303
|
+
// @ts-expect-error forced values are forbidden
|
|
304
|
+
await guest.insert({ body: "hello", created_by: "other" });
|
|
305
|
+
const supplied = { body: "hello", created_by: "other" };
|
|
306
|
+
// @ts-expect-error also reject forced fields through variables
|
|
307
|
+
await guest.insert(supplied);
|
|
308
|
+
await admin.insert({ body: "hello", created_by: "admin" });
|
|
309
|
+
// @ts-expect-error admin must supply created_by
|
|
310
|
+
await admin.insert({ body: "hello" });
|
|
311
|
+
await combined.insert({ body: "hello" });
|
|
312
|
+
await combined.insert({ body: "hello", created_by: "admin" });
|
|
313
|
+
// @ts-expect-error read-only profiles do not relax required insert fields
|
|
314
|
+
await combined.insert({});
|
|
315
|
+
// @ts-expect-error server insert defaults are unchanged
|
|
316
|
+
await server.insert({ body: "hello", created_by: "admin" });
|
|
317
|
+
await server.insert({ body: "hello", created_by: "admin", synced: 1 });
|
|
318
|
+
await files.insert({ original_name: "hello.txt", data: new Uint8Array() });
|
|
319
|
+
await files.insert({ id: "file-id", original_name: "hello.txt", original_last_modified: null, data: new Uint8Array() });
|
|
320
|
+
// @ts-expect-error file metadata is generated by the server
|
|
321
|
+
await files.insert({ original_name: "hello.txt", data: new Uint8Array(), content_type: "text/plain" });
|
|
322
|
+
const fileWithGeneratedMetadata = { original_name: "hello.txt", data: new Uint8Array(), content_type: "text/plain" };
|
|
323
|
+
// @ts-expect-error file metadata is also rejected through variables
|
|
324
|
+
await files.insert(fileWithGeneratedMetadata);
|
|
325
|
+
// @ts-expect-error original_name is required
|
|
326
|
+
await files.insert({ data: new Uint8Array() });
|
|
327
|
+
await files.update({}, { original_name: "updated.txt", data: new Uint8Array() });
|
|
328
|
+
// @ts-expect-error file metadata cannot be updated directly
|
|
329
|
+
await files.update({}, { content_type: "text/plain" });
|
|
330
|
+
// @ts-expect-error file metadata updates are also rejected through variables
|
|
331
|
+
await files.update({}, fileWithGeneratedMetadata);
|
|
332
|
+
// @ts-expect-error unpublished tables are absent
|
|
333
|
+
type Private = GuestDBSchema["${privateTableName}"];
|
|
334
|
+
type Nested = InsertDataWithNested<{}, GuestDBSchema, "parent">;
|
|
335
|
+
const nested: Nested = { "${tableName}": [{ body: "hello" }] };
|
|
336
|
+
// @ts-expect-error nested inputs also exclude forced fields
|
|
337
|
+
const badNested: Nested = { "${tableName}": [{ created_by: "other" }] };
|
|
338
|
+
};
|
|
339
|
+
`,
|
|
340
|
+
);
|
|
341
|
+
const originalPublish = instance.options.publish;
|
|
342
|
+
// await instance.update({ publish: originalPublish });
|
|
343
|
+
const profiles: Record<string, ClientSchema> = {};
|
|
344
|
+
for (const [role, name] of [
|
|
345
|
+
["guest", "GuestDBSchema"],
|
|
346
|
+
["admin", "AdminDBSchema"],
|
|
347
|
+
["viewer", "ViewerDBSchema"],
|
|
348
|
+
["member", "MemberDBSchema"],
|
|
349
|
+
["unknown", "UnknownDBSchema"],
|
|
350
|
+
["", "AnonymousDBSchema"],
|
|
351
|
+
] as const) {
|
|
352
|
+
const socket = createClient(`http://127.0.0.1:${address.port}`, {
|
|
353
|
+
transports: ["websocket"],
|
|
354
|
+
reconnection: false,
|
|
355
|
+
query: { ...(role ? { token: role } : {}), userId: "user-admin" },
|
|
356
|
+
auth: { userId: "user-admin" },
|
|
357
|
+
extraHeaders: { userId: "user-admin", cookie: "userId=user-admin" },
|
|
358
|
+
});
|
|
359
|
+
sockets.push(socket);
|
|
360
|
+
profiles[name] = await new Promise<ClientSchema>((resolve, reject) => {
|
|
361
|
+
socket.once("connect_error", reject);
|
|
362
|
+
socket.once(CHANNELS.SCHEMA, resolve);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
const guestSchema = profiles.GuestDBSchema!;
|
|
366
|
+
assert.equal(guestSchema.err, undefined);
|
|
367
|
+
const table = guestSchema.tableSchema.find((t) => t.name === tableName);
|
|
368
|
+
assert(table);
|
|
369
|
+
assert.equal(table.columns.find((c) => c.name === "created_by")?.insert, false);
|
|
370
|
+
assert.deepEqual(profiles.MemberDBSchema!.tableSchema, guestSchema.tableSchema);
|
|
371
|
+
assert.deepEqual(profiles.UnknownDBSchema!.tableSchema, []);
|
|
372
|
+
assert.deepEqual(profiles.AnonymousDBSchema!.tableSchema, []);
|
|
373
|
+
const serverSocket = io.sockets.sockets.get(sockets[0]!.id!);
|
|
374
|
+
assert(serverSocket);
|
|
375
|
+
const handlers = await instance.getClientDBHandlers({ socket: serverSocket }, undefined);
|
|
376
|
+
assert.equal(privateTableName in handlers.clientDb, false);
|
|
377
|
+
assert.equal("delete" in handlers.clientDb[tableName]!, false);
|
|
378
|
+
assert.equal(typeof handlers.clientDb[tableName]!.upsert, "function");
|
|
379
|
+
assert.equal(typeof handlers.clientDb[tableName]!.insertMany, "function");
|
|
380
|
+
assert.equal("updateBatch" in handlers.clientDb[tableName]!, false);
|
|
381
|
+
await t.test("server user handlers enforce permissions and reject client impersonation", async () => {
|
|
382
|
+
assert(instance);
|
|
383
|
+
const admin = await instance.getClientDBHandlers({ userId: "user-admin" }, undefined);
|
|
384
|
+
assert.equal(typeof admin.clientDb[privateTableName]!.find, "function");
|
|
385
|
+
assert.deepEqual(await admin.clientDb[privateTableName]!.find!(), []);
|
|
386
|
+
assert.equal(await admin.clientSql("SELECT 1", [], { returnType: "value" }), 1);
|
|
387
|
+
const guest = await instance.getClientDBHandlers({ userId: "user-guest" }, undefined);
|
|
388
|
+
assert.equal(privateTableName in guest.clientDb, false);
|
|
389
|
+
const row = await guest.withClientDbTx((tx) =>
|
|
390
|
+
tx[tableName]!.insert!({ body: "server-user" }, { returning: "*" }),
|
|
391
|
+
);
|
|
392
|
+
assert.equal(row.created_by, "user-guest");
|
|
393
|
+
assert.equal(await guest.clientDb[tableName]!.count!({ id: row.id }), 1);
|
|
394
|
+
await assert.rejects(guest.clientSql("SELECT 1"));
|
|
395
|
+
await assert.rejects(() => guest.clientMethods.transactionTest!.run({ fail: true }) as any);
|
|
396
|
+
assert.equal(await guest.clientDb[tableName]!.count!({ body: "function-rollback" }), 0);
|
|
397
|
+
const scoped = await instance.getClientDBHandlers(
|
|
398
|
+
{ userId: "user-admin" },
|
|
399
|
+
{ tables: { [tableName]: { select: { forcedFilter: { id: row.id } } } } },
|
|
400
|
+
);
|
|
401
|
+
assert.deepEqual(Object.keys(scoped.clientDb), [tableName]);
|
|
402
|
+
assert.equal("insert" in scoped.clientDb[tableName]!, false);
|
|
403
|
+
assert.equal(await scoped.withClientDbTx((tx) => tx[tableName]!.count!()), 1);
|
|
404
|
+
assert.throws(() => scoped.clientSql("SELECT 1"), /PermissionScope/);
|
|
405
|
+
|
|
406
|
+
for (const userId of ["missing", "", undefined, 1, { $ne: null }]) {
|
|
407
|
+
await assert.rejects(async () =>
|
|
408
|
+
instance!.getClientDBHandlers({ userId: userId as string }, undefined),
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
await assert.rejects(async () => instance!.getClientDBHandlers(
|
|
412
|
+
// @ts-expect-error Explicit identity cannot be combined with a network request.
|
|
413
|
+
{ userId: "user-admin", socket: serverSocket },
|
|
414
|
+
undefined,
|
|
415
|
+
), /userId cannot be combined/);
|
|
416
|
+
|
|
417
|
+
const socketError = await new Promise((resolve) => {
|
|
418
|
+
sockets[0]!.emit(CHANNELS.DEFAULT, {
|
|
419
|
+
tableName: privateTableName,
|
|
420
|
+
command: "find",
|
|
421
|
+
userId: "user-admin",
|
|
422
|
+
clientReq: { userId: "user-admin" },
|
|
423
|
+
}, (error: unknown) => resolve(error));
|
|
424
|
+
});
|
|
425
|
+
assert(socketError);
|
|
426
|
+
for (const token of ["guest", undefined]) {
|
|
427
|
+
const headers = {
|
|
428
|
+
"content-type": "application/json",
|
|
429
|
+
userId: "user-admin",
|
|
430
|
+
cookie: "userId=user-admin",
|
|
431
|
+
...(token ? { authorization: `Bearer ${Buffer.from(token).toString("base64")}` } : {}),
|
|
432
|
+
};
|
|
433
|
+
const response = await fetch(`http://127.0.0.1:${address.port}/api/schema?userId=user-admin`, {
|
|
434
|
+
method: "POST",
|
|
435
|
+
headers,
|
|
436
|
+
body: JSON.stringify({ userId: "user-admin", clientReq: { userId: "user-admin" } }),
|
|
437
|
+
});
|
|
438
|
+
const schema = await response.json() as ClientSchema;
|
|
439
|
+
assert.equal(response.status, 200, JSON.stringify(schema));
|
|
440
|
+
assert.equal(schema.rawSQL, false);
|
|
441
|
+
assert.equal(schema.tableSchema.some((table) => table.name === privateTableName), false);
|
|
442
|
+
const denied = await fetch(`http://127.0.0.1:${address.port}/api/db/${privateTableName}/find?userId=user-admin`, {
|
|
443
|
+
method: "POST",
|
|
444
|
+
headers,
|
|
445
|
+
body: JSON.stringify([{}, {}, { userId: "user-admin" }]),
|
|
446
|
+
});
|
|
447
|
+
assert.equal(denied.status, 400);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
serverUsers.set("user-admin", { id: "user-admin", type: "guest" });
|
|
451
|
+
await assert.rejects(admin.clientDb[privateTableName]!.find!());
|
|
452
|
+
await assert.rejects(admin.clientSql("SELECT 1"));
|
|
453
|
+
assert.equal(await admin.clientDb[tableName]!.count!({ id: row.id }), 0);
|
|
454
|
+
serverUsers.delete("user-admin");
|
|
455
|
+
await assert.rejects(admin.clientDb[tableName]!.find!(), /User not found/);
|
|
456
|
+
serverUsers.set("user-admin", { id: "wrong-id", type: "admin" });
|
|
457
|
+
await assert.rejects(
|
|
458
|
+
instance.getClientDBHandlers({ userId: "user-admin" }, undefined),
|
|
459
|
+
/User not found/,
|
|
460
|
+
);
|
|
461
|
+
serverUsers.set("user-admin", { id: "user-admin", type: "admin" });
|
|
462
|
+
await instance.db[tableName]!.delete!({ id: row.id });
|
|
463
|
+
});
|
|
464
|
+
await t.test("a rejected guest abort cannot cancel a later server-user query", async () => {
|
|
465
|
+
assert(instance);
|
|
466
|
+
const admin = await instance.getClientDBHandlers({ userId: "user-admin" }, undefined);
|
|
467
|
+
const abortSignalId = randomUUID();
|
|
468
|
+
// The third wire argument supplies the cancellation ID used by the query handler.
|
|
469
|
+
const findWithAbort = admin.clientDb[tableName]!.find! as ViewHandler["find"];
|
|
470
|
+
const find = () => findWithAbort({}, {}, { abortSignalId });
|
|
471
|
+
await find();
|
|
472
|
+
|
|
473
|
+
// No query is active. This guest must not reserve the ID for another caller.
|
|
474
|
+
const guestSocket = sockets[0]!;
|
|
475
|
+
const abortError = await new Promise((resolve) => {
|
|
476
|
+
guestSocket.emit(CHANNELS.DEFAULT, {
|
|
477
|
+
tableName,
|
|
478
|
+
command: "abort",
|
|
479
|
+
param1: { command: "find", abortSignalId },
|
|
480
|
+
}, (error: unknown) => resolve(error));
|
|
481
|
+
});
|
|
482
|
+
assert(abortError, "Aborting a nonexistent query must be rejected");
|
|
483
|
+
await assert.doesNotReject(
|
|
484
|
+
find,
|
|
485
|
+
"A rejected guest abort must not leave a pending cancellation for the admin",
|
|
486
|
+
);
|
|
487
|
+
});
|
|
488
|
+
const viewerSocket = io.sockets.sockets.get(sockets[2]!.id!);
|
|
489
|
+
assert(viewerSocket);
|
|
490
|
+
const viewerHandlers = await instance.getClientDBHandlers(
|
|
491
|
+
{ socket: viewerSocket },
|
|
492
|
+
undefined,
|
|
493
|
+
);
|
|
494
|
+
assert.deepEqual(Object.keys(viewerHandlers.clientDb).sort(), [tableName]);
|
|
495
|
+
assert.deepEqual(Object.keys(viewerHandlers.clientDb[tableName]!).sort(), [
|
|
496
|
+
"count",
|
|
497
|
+
"find",
|
|
498
|
+
"findOne",
|
|
499
|
+
"getColumns",
|
|
500
|
+
"getInfo",
|
|
501
|
+
"size",
|
|
502
|
+
"subscribe",
|
|
503
|
+
"subscribeOne",
|
|
504
|
+
]);
|
|
505
|
+
const inserted = await handlers.clientDb[tableName]!.insert!(
|
|
506
|
+
{ body: "hello" },
|
|
507
|
+
{ returning: "*" },
|
|
508
|
+
);
|
|
509
|
+
assert.equal(inserted.created_by, "user-guest");
|
|
510
|
+
assert(Number(inserted.synced) > 0);
|
|
511
|
+
const updated = await handlers.clientDb[tableName]!.update!(
|
|
512
|
+
{ id: inserted.id },
|
|
513
|
+
{ body: "updated" },
|
|
514
|
+
{ returning: "*" },
|
|
515
|
+
);
|
|
516
|
+
assert.equal(updated![0]!.body, "updated");
|
|
517
|
+
assert.equal(updated![0]!.created_by, "user-guest");
|
|
518
|
+
assert.equal(
|
|
519
|
+
(await handlers.clientDb[tableName]!.findOne!({
|
|
520
|
+
id: inserted.id,
|
|
521
|
+
}))!.created_by,
|
|
522
|
+
"user-guest",
|
|
523
|
+
);
|
|
524
|
+
await assert.rejects(
|
|
525
|
+
handlers.clientDb[tableName]!.update!({ id: inserted.id }, { internal: "forbidden" }),
|
|
526
|
+
);
|
|
527
|
+
const transactionRow = await handlers.withClientDbTx(async (...args) => {
|
|
528
|
+
assert.equal(args.length, 1);
|
|
529
|
+
const [tx] = args;
|
|
530
|
+
assert.equal("sql" in tx, false);
|
|
531
|
+
const row = await tx[tableName]!.insert!(
|
|
532
|
+
{ body: "transaction", created_by: "other" },
|
|
533
|
+
{ returning: "*" },
|
|
534
|
+
);
|
|
535
|
+
assert.equal(row.created_by, "user-guest");
|
|
536
|
+
assert.equal(await tx[tableName]!.count!({ id: row.id }), 1);
|
|
537
|
+
assert.equal(await handlers.clientDb[tableName]!.count!({ id: row.id }), 0);
|
|
538
|
+
await tx[tableName]!.update!({ id: row.id }, { body: "committed" });
|
|
539
|
+
return tx[tableName]!.findOne!({ id: row.id });
|
|
540
|
+
});
|
|
541
|
+
assert.equal(transactionRow!.body, "committed");
|
|
542
|
+
assert.equal(await handlers.clientDb[tableName]!.count!({ id: transactionRow!.id }), 1);
|
|
543
|
+
await instance.db[tableName]!.insert!({ body: "hidden", created_by: "other" });
|
|
544
|
+
await handlers.withClientDbTx(async (tx) => {
|
|
545
|
+
assert.equal(await tx[tableName]!.count!({ body: "hidden" }), 0);
|
|
546
|
+
assert.equal(privateTableName in tx, false);
|
|
547
|
+
assert.equal("delete" in tx[tableName]!, false);
|
|
548
|
+
assert.equal("db" in tx[tableName]!, false);
|
|
549
|
+
assert.equal("tx" in tx[tableName]!, false);
|
|
550
|
+
});
|
|
551
|
+
await t.test("caught subscription errors still roll back client transactions", async () => {
|
|
552
|
+
const row = { body: "before-subscription-failure" };
|
|
553
|
+
const error = { message: /subscribe/ };
|
|
554
|
+
await assert.rejects(
|
|
555
|
+
handlers.withClientDbTx(async (tx) => {
|
|
556
|
+
await tx[tableName]!.insert!(row);
|
|
557
|
+
await assert.rejects(tx[tableName]!.subscribe!({}, {}, undefined as any), error);
|
|
558
|
+
}),
|
|
559
|
+
error,
|
|
560
|
+
);
|
|
561
|
+
assert.equal(await handlers.clientDb[tableName]!.count!(row), 0);
|
|
562
|
+
});
|
|
563
|
+
for (const failure of ["throw", "permission", "database"] as const) {
|
|
564
|
+
const body = `rollback-${failure}`;
|
|
565
|
+
await assert.rejects(
|
|
566
|
+
handlers.withClientDbTx(async (tx) => {
|
|
567
|
+
await tx[tableName]!.insert!({ body });
|
|
568
|
+
if (failure === "permission") {
|
|
569
|
+
await tx[tableName]!.update!({}, { internal: "forbidden" });
|
|
570
|
+
} else if (failure === "database") {
|
|
571
|
+
await tx[tableName]!.insert!({ id: transactionRow!.id, body });
|
|
572
|
+
} else {
|
|
573
|
+
throw new Error("rollback");
|
|
574
|
+
}
|
|
575
|
+
}),
|
|
576
|
+
);
|
|
577
|
+
assert.equal(await handlers.clientDb[tableName]!.count!({ body }), 0);
|
|
578
|
+
}
|
|
579
|
+
await t.test("caught checkFilter errors still roll back client transactions", async () => {
|
|
580
|
+
const row = { body: "denied-by-check-filter" };
|
|
581
|
+
const error = { message: /failed the check condition/ };
|
|
582
|
+
await assert.rejects(handlers.clientDb[tableName]!.insert!(row), error);
|
|
583
|
+
assert.equal(await handlers.clientDb[tableName]!.count!(row), 0);
|
|
584
|
+
|
|
585
|
+
await assert.rejects(
|
|
586
|
+
handlers.withClientDbTx(async (tx) => {
|
|
587
|
+
await tx[tableName]!.insert!({ body: "before-check-filter-failure" });
|
|
588
|
+
await assert.rejects(tx[tableName]!.insert!(row), error);
|
|
589
|
+
await assert.rejects(tx[tableName]!.update!({}, { internal: "forbidden" }));
|
|
590
|
+
}),
|
|
591
|
+
error,
|
|
592
|
+
);
|
|
593
|
+
assert.equal(await handlers.clientDb[tableName]!.count!(row), 0);
|
|
594
|
+
assert.equal(
|
|
595
|
+
await handlers.clientDb[tableName]!.count!({ body: "before-check-filter-failure" }),
|
|
596
|
+
0,
|
|
597
|
+
);
|
|
598
|
+
});
|
|
599
|
+
await t.test("caught postValidate errors still roll back client transactions", async () => {
|
|
600
|
+
const error = { message: "Update rejected by postValidate" };
|
|
601
|
+
await assert.rejects(
|
|
602
|
+
handlers.withClientDbTx(async (tx) => {
|
|
603
|
+
await assert.rejects(
|
|
604
|
+
tx[tableName]!.update!(
|
|
605
|
+
{ id: transactionRow!.id },
|
|
606
|
+
{ body: "denied-by-post-validate" },
|
|
607
|
+
),
|
|
608
|
+
error,
|
|
609
|
+
);
|
|
610
|
+
}),
|
|
611
|
+
error,
|
|
612
|
+
);
|
|
613
|
+
const row = await handlers.clientDb[tableName]!.findOne!({ id: transactionRow!.id });
|
|
614
|
+
assert.equal(row!.body, "committed");
|
|
615
|
+
});
|
|
616
|
+
const scopedHandlers = await instance.getClientDBHandlers(
|
|
617
|
+
{ socket: serverSocket },
|
|
618
|
+
{ tables: { [tableName]: { select: { forcedFilter: { body: "committed" } } } } },
|
|
619
|
+
);
|
|
620
|
+
assert.deepEqual(Object.keys(scopedHandlers.clientDb).sort(), [tableName]);
|
|
621
|
+
assert.equal("insert" in scopedHandlers.clientDb[tableName]!, false);
|
|
622
|
+
assert.equal("update" in scopedHandlers.clientDb[tableName]!, false);
|
|
623
|
+
assert.equal("upsert" in scopedHandlers.clientDb[tableName]!, false);
|
|
624
|
+
await scopedHandlers.withClientDbTx(async (tx) => {
|
|
625
|
+
assert.equal(await tx[tableName]!.count!(), 1);
|
|
626
|
+
assert.deepEqual(Object.keys(tx).sort(), Object.keys(scopedHandlers.clientDb).sort());
|
|
627
|
+
assert.deepEqual(
|
|
628
|
+
Object.keys(tx[tableName]!),
|
|
629
|
+
Object.keys(scopedHandlers.clientDb[tableName]!),
|
|
630
|
+
);
|
|
631
|
+
});
|
|
632
|
+
const functionRow = await handlers.clientMethods.transactionTest!.run({ fail: false });
|
|
633
|
+
const committedRow = await handlers.clientDb[tableName]!.findOne!({
|
|
634
|
+
body: "function-commit",
|
|
635
|
+
});
|
|
636
|
+
assert.equal(committedRow!.created_by, "user-guest");
|
|
637
|
+
assert.deepEqual(functionRow, committedRow);
|
|
638
|
+
await assert.rejects(
|
|
639
|
+
() => handlers.clientMethods.transactionTest!.run({ fail: true }) as any,
|
|
640
|
+
);
|
|
641
|
+
assert.equal(await handlers.clientDb[tableName]!.count!({ body: "function-rollback" }), 0);
|
|
642
|
+
sockets.forEach((socket) => socket.disconnect());
|
|
643
|
+
io.disconnectSockets(true);
|
|
644
|
+
await instance.update({ publish: originalPublish });
|
|
645
|
+
await instance.update({
|
|
646
|
+
publish: [{ name: "EmptyDBSchema", userTypes: ["empty"], publish: null }],
|
|
647
|
+
});
|
|
648
|
+
assert(
|
|
649
|
+
(await instance.getTSSchema()).tsSchema.includes(
|
|
650
|
+
"export type EmptyDBSchema = Record<string, never>;",
|
|
651
|
+
),
|
|
652
|
+
);
|
|
653
|
+
await instance.update({ publish: originalPublish });
|
|
654
|
+
for (const [publish, error] of [
|
|
655
|
+
[[{ userTypes: [], publish: null }], /userTypes must not be empty/],
|
|
656
|
+
[
|
|
657
|
+
[
|
|
658
|
+
{ userTypes: ["guest"], publish: null },
|
|
659
|
+
{ userTypes: ["guest"], publish: null },
|
|
660
|
+
],
|
|
661
|
+
/Duplicate publish user type/,
|
|
662
|
+
],
|
|
663
|
+
[
|
|
664
|
+
[{ name: "ClientDBSchema", userTypes: ["guest"], publish: null }],
|
|
665
|
+
/Invalid client schema name/,
|
|
666
|
+
],
|
|
667
|
+
[
|
|
668
|
+
[{ name: "ClientSchemas", userTypes: ["guest"], publish: null }],
|
|
669
|
+
/Invalid client schema name/,
|
|
670
|
+
],
|
|
671
|
+
[
|
|
672
|
+
[
|
|
673
|
+
{ name: "SameSchema", userTypes: ["a"], publish: null },
|
|
674
|
+
{ name: "SameSchema", userTypes: ["b"], publish: null },
|
|
675
|
+
],
|
|
676
|
+
/Duplicate publish schema name/,
|
|
677
|
+
],
|
|
678
|
+
] as const) {
|
|
679
|
+
// await instance.update({ publish: publish });
|
|
680
|
+
// await assert.rejects((handlers.clientDb as DBHandlerServer)[tableName]!.find!(), error);
|
|
681
|
+
// await instance.update({ publish: originalPublish });
|
|
682
|
+
await assert.rejects(instance.update({ publish }), error);
|
|
683
|
+
assert.equal((await instance.getTSSchema()).tsSchema, tsSchema);
|
|
684
|
+
}
|
|
685
|
+
await instance.update({
|
|
686
|
+
publish: [{ userTypes: ["guest"], publish: { [tableName]: { select: "*" } } }],
|
|
687
|
+
});
|
|
688
|
+
const updatedSchema = (await instance.getTSSchema()).tsSchema;
|
|
689
|
+
assert(updatedSchema.includes("export type Publish1Schema"));
|
|
690
|
+
assert(!updatedSchema.includes("GuestDBSchema"));
|
|
691
|
+
assert.equal(
|
|
692
|
+
readFileSync(path.join(generatedTypesDir, `${DB_GENERATED_NAMES.SCHEMA}.ts`), "utf8"),
|
|
693
|
+
updatedSchema,
|
|
694
|
+
);
|
|
695
|
+
await instance.update({ publish: originalPublish });
|
|
696
|
+
assert.equal((await instance.getTSSchema()).tsSchema, tsSchema);
|
|
697
|
+
await t.test("server user handlers require configured authentication", async () => {
|
|
698
|
+
assert(instance);
|
|
699
|
+
const handlers = await instance.getClientDBHandlers({ userId: "user-admin" }, undefined);
|
|
700
|
+
await instance.update({ auth: undefined });
|
|
701
|
+
await assert.rejects(handlers.clientDb[privateTableName]!.find!(), /auth.findUser/);
|
|
702
|
+
await assert.rejects(
|
|
703
|
+
instance.getClientDBHandlers({ userId: "user-admin" }, undefined),
|
|
704
|
+
/auth.findUser/,
|
|
705
|
+
);
|
|
706
|
+
});
|
|
707
|
+
} finally {
|
|
708
|
+
sockets.forEach((socket) => socket.disconnect());
|
|
709
|
+
await instance?.destroy();
|
|
710
|
+
await new Promise<void>((resolve) => io.close(() => resolve()));
|
|
711
|
+
await db.none(`DROP SCHEMA IF EXISTS ${schemaName} CASCADE`);
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
);
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
const checkTypes = (schema: string, checks: string) => {
|
|
718
|
+
const filename = path.resolve(__dirname, "../../../client/client-schema-typecheck.ts");
|
|
719
|
+
const options: ts.CompilerOptions = {
|
|
720
|
+
strict: true,
|
|
721
|
+
noEmit: true,
|
|
722
|
+
skipLibCheck: true,
|
|
723
|
+
target: ts.ScriptTarget.ES2022,
|
|
724
|
+
module: ts.ModuleKind.CommonJS,
|
|
725
|
+
moduleResolution: ts.ModuleResolutionKind.Node10,
|
|
726
|
+
};
|
|
727
|
+
const host = ts.createCompilerHost(options);
|
|
728
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
729
|
+
const getSourceFile = host.getSourceFile;
|
|
730
|
+
host.getSourceFile = (name, languageVersion, onError, shouldCreateNewSourceFile) =>
|
|
731
|
+
name === filename ?
|
|
732
|
+
ts.createSourceFile(name, schema + checks, languageVersion, true)
|
|
733
|
+
: getSourceFile(name, languageVersion, onError, shouldCreateNewSourceFile);
|
|
734
|
+
const program = ts.createProgram([filename], options, host);
|
|
735
|
+
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
736
|
+
assert.equal(
|
|
737
|
+
diagnostics.length,
|
|
738
|
+
0,
|
|
739
|
+
ts.formatDiagnosticsWithColorAndContext(diagnostics, {
|
|
740
|
+
getCanonicalFileName: (name) => name,
|
|
741
|
+
getCurrentDirectory: () => process.cwd(),
|
|
742
|
+
getNewLine: () => "\n",
|
|
743
|
+
}),
|
|
744
|
+
);
|
|
745
|
+
};
|