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,553 @@
|
|
|
1
|
+
import { DB_GENERATED_NAMES } from "prostgles-server/dist/DBSchemaBuilder/constants";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { once } from "node:events";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { test } from "node:test";
|
|
9
|
+
import prostgles, {
|
|
10
|
+
type DBHandlerServer,
|
|
11
|
+
defineFunction,
|
|
12
|
+
getLocalStorageClient,
|
|
13
|
+
type ProstglesInitOptions,
|
|
14
|
+
type PublishContextValue,
|
|
15
|
+
} from "prostgles-server";
|
|
16
|
+
import type { DB } from "prostgles-server/dist/Prostgles";
|
|
17
|
+
import { getConnectionDetails } from "prostgles-server/dist/DboBuilder/runSql/getAdminClient";
|
|
18
|
+
import { CHANNELS, type ClientSchema } from "prostgles-types";
|
|
19
|
+
import { Server } from "socket.io";
|
|
20
|
+
import { io as createClient, type Socket } from "socket.io-client";
|
|
21
|
+
import ts from "typescript";
|
|
22
|
+
import express from "express";
|
|
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 () => {
|
|
29
|
+
const http = createServer();
|
|
30
|
+
const io = new Server(http);
|
|
31
|
+
const sockets: Socket[] = [];
|
|
32
|
+
let instance: Awaited<ReturnType<typeof prostgles>> | undefined;
|
|
33
|
+
const suffix = randomUUID().slice(0, 8);
|
|
34
|
+
const schemaName = `client_schema_types_${suffix}`;
|
|
35
|
+
const tableName = "correspondence";
|
|
36
|
+
const privateTableName = "private_table";
|
|
37
|
+
const fileTableName = "files";
|
|
38
|
+
const generatedTypesDir = path.resolve("debug/client-schema-types", suffix);
|
|
39
|
+
try {
|
|
40
|
+
await db.none(`
|
|
41
|
+
CREATE SCHEMA ${schemaName};
|
|
42
|
+
CREATE TABLE ${schemaName}.${tableName} (
|
|
43
|
+
id SERIAL PRIMARY KEY,
|
|
44
|
+
body TEXT NOT NULL,
|
|
45
|
+
created_by TEXT NOT NULL,
|
|
46
|
+
note TEXT,
|
|
47
|
+
internal TEXT NOT NULL DEFAULT 'default',
|
|
48
|
+
synced BIGINT NOT NULL
|
|
49
|
+
);
|
|
50
|
+
CREATE TABLE ${schemaName}.${privateTableName} (id SERIAL PRIMARY KEY);
|
|
51
|
+
`);
|
|
52
|
+
http.listen(0, "127.0.0.1");
|
|
53
|
+
await once(http, "listening");
|
|
54
|
+
const address = http.address();
|
|
55
|
+
assert(address && typeof address === "object");
|
|
56
|
+
const userIdContextValue = {
|
|
57
|
+
$prostglesContext: { objectName: "user", objectPropertyName: "id" },
|
|
58
|
+
} as const satisfies PublishContextValue<{ user: { id: string } }>;
|
|
59
|
+
instance = await prostgles({
|
|
60
|
+
dbConnection: {
|
|
61
|
+
...getConnectionDetails(db),
|
|
62
|
+
options: `-c search_path=${schemaName}`,
|
|
63
|
+
} as unknown as ProstglesInitOptions["dbConnection"],
|
|
64
|
+
tsGeneratedTypesDir: generatedTypesDir,
|
|
65
|
+
transactions: true,
|
|
66
|
+
schemaFilter: { [schemaName]: 1 },
|
|
67
|
+
tableConfig: {
|
|
68
|
+
[tableName]: {
|
|
69
|
+
syncConfig: { id_fields: ["id"], synced_field: "synced" },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
fileTable: {
|
|
73
|
+
tableName: fileTableName,
|
|
74
|
+
expressApp: express(),
|
|
75
|
+
storageClient: getLocalStorageClient({
|
|
76
|
+
localFolderPath: path.join(generatedTypesDir, "files"),
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
io,
|
|
80
|
+
auth: {
|
|
81
|
+
sidKeyName: "token",
|
|
82
|
+
getUser: async (sid) => {
|
|
83
|
+
if (!sid) return undefined;
|
|
84
|
+
return {
|
|
85
|
+
user: { id: `user-${sid}`, type: sid },
|
|
86
|
+
clientUser: { id: sid, type: sid },
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
findUser: async (filter) =>
|
|
90
|
+
"$and" in filter && filter.$and?.every((condition) => condition.id === "user-guest") ?
|
|
91
|
+
{ id: "user-guest", type: "guest" }
|
|
92
|
+
: undefined,
|
|
93
|
+
},
|
|
94
|
+
functions: {
|
|
95
|
+
guest: {
|
|
96
|
+
userFilter: { id: "user-guest" },
|
|
97
|
+
functions: {
|
|
98
|
+
transactionTest: defineFunction({
|
|
99
|
+
input: { fail: "boolean" },
|
|
100
|
+
run: ({ fail }, { getClientDbTx }) =>
|
|
101
|
+
getClientDbTx(async (tx) => {
|
|
102
|
+
const row = await tx[tableName]!.insert!(
|
|
103
|
+
{ body: fail ? "function-rollback" : "function-commit" },
|
|
104
|
+
{ returning: "*" },
|
|
105
|
+
);
|
|
106
|
+
if (fail) {
|
|
107
|
+
await tx[tableName]!.update!({ id: row.id }, { internal: "forbidden" });
|
|
108
|
+
}
|
|
109
|
+
return row;
|
|
110
|
+
}),
|
|
111
|
+
}),
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
publish: [
|
|
116
|
+
{
|
|
117
|
+
name: "GuestDBSchema",
|
|
118
|
+
userTypes: ["guest", "member"],
|
|
119
|
+
publish: {
|
|
120
|
+
[fileTableName]: { select: "*", insert: "*", update: "*" },
|
|
121
|
+
[tableName]: {
|
|
122
|
+
select: {
|
|
123
|
+
fields: "*",
|
|
124
|
+
forcedFilter: {
|
|
125
|
+
created_by: userIdContextValue as unknown as string,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
update: {
|
|
129
|
+
fields: ["body", "note", "created_by", "synced"],
|
|
130
|
+
disableMethods: { updateBatch: 1 },
|
|
131
|
+
forcedData: {
|
|
132
|
+
created_by: userIdContextValue as unknown as string,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
insert: {
|
|
136
|
+
fields: { internal: 0 },
|
|
137
|
+
forcedData: {
|
|
138
|
+
created_by: userIdContextValue as unknown as string,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "AdminDBSchema",
|
|
146
|
+
userTypes: ["admin"],
|
|
147
|
+
publish: {
|
|
148
|
+
[tableName]: { select: "*", insert: "*", update: "*" },
|
|
149
|
+
[privateTableName]: { select: "*" },
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "ViewerDBSchema",
|
|
154
|
+
userTypes: ["viewer"],
|
|
155
|
+
publish: { [tableName]: { select: "*" } },
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
onReady: () => {},
|
|
159
|
+
});
|
|
160
|
+
const { tsSchema } = await instance.getTSSchema();
|
|
161
|
+
assert.doesNotMatch(tsSchema, /\bimport(?:\s|\()/);
|
|
162
|
+
assert.equal(
|
|
163
|
+
readFileSync(
|
|
164
|
+
path.join(generatedTypesDir, `${DB_GENERATED_NAMES.SCHEMA}.ts`),
|
|
165
|
+
"utf8",
|
|
166
|
+
),
|
|
167
|
+
tsSchema,
|
|
168
|
+
);
|
|
169
|
+
await instance.reWriteDBSchema();
|
|
170
|
+
assert.equal((await instance.getTSSchema()).tsSchema, tsSchema);
|
|
171
|
+
assert.equal(io.sockets.sockets.size, 0);
|
|
172
|
+
checkTypes(
|
|
173
|
+
tsSchema,
|
|
174
|
+
`
|
|
175
|
+
import type { TableHandler, InsertDataWithNested, ClientSchemaFor } from "prostgles-types";
|
|
176
|
+
import type { DBHandlerClient } from "prostgles-client";
|
|
177
|
+
import type { DBOFullyTypedClient, InitResult, SessionUser } from "../server/node_modules/prostgles-server";
|
|
178
|
+
import type { RestrictedFunctionContext, UnrestrictedFunctionContext } from "../server/node_modules/prostgles-server/dist/PublishParser/defineServerFunction";
|
|
179
|
+
import type { getClientHandlers } from "../server/node_modules/prostgles-server/dist/WebsocketAPI/getClientHandlers";
|
|
180
|
+
type Name = "${tableName}";
|
|
181
|
+
type FileName = "${fileTableName}";
|
|
182
|
+
declare const guest: TableHandler<GuestDBSchema, Name>;
|
|
183
|
+
declare const files: TableHandler<GuestDBSchema, FileName>;
|
|
184
|
+
declare const admin: TableHandler<AdminDBSchema, Name>;
|
|
185
|
+
declare const combined: TableHandler<ClientDBSchema, Name>;
|
|
186
|
+
declare const server: TableHandler<${DB_GENERATED_NAMES.SCHEMA}, Name>;
|
|
187
|
+
declare const viewer: TableHandler<ViewerDBSchema, Name>;
|
|
188
|
+
declare const client: DBHandlerClient<ClientDBSchema>;
|
|
189
|
+
declare const adminClient: DBHandlerClient<AdminDBSchema>;
|
|
190
|
+
declare const restricted: RestrictedFunctionContext<GuestDBSchema>;
|
|
191
|
+
declare const unrestricted: UnrestrictedFunctionContext<DBGeneratedSchema>;
|
|
192
|
+
declare const serverClient: DBOFullyTypedClient<ClientDBSchema>;
|
|
193
|
+
declare const handlers: Awaited<ReturnType<typeof getClientHandlers<GuestDBSchema>>>;
|
|
194
|
+
declare const instance: InitResult<DBGeneratedSchema, SessionUser, undefined, ClientDBSchema>;
|
|
195
|
+
declare const guestClientSchema: ClientSchemaFor<ClientSchemas, "guest">;
|
|
196
|
+
guestClientSchema satisfies GuestDBSchema;
|
|
197
|
+
async () => {
|
|
198
|
+
const combinedHandlers = await instance.getClientDBHandlers(null as never, undefined);
|
|
199
|
+
const guestHandlers = await instance.getClientDBHandlers<GuestDBSchema>(null as never, undefined);
|
|
200
|
+
await combinedHandlers.clientDb["${privateTableName}"]?.find();
|
|
201
|
+
// @ts-expect-error the guest profile does not publish private_table
|
|
202
|
+
await guestHandlers.clientDb["${privateTableName}"].find();
|
|
203
|
+
const restrictedRow = await restricted.dbo["${tableName}"].insert({ body: "hello" }, { returning: "*" });
|
|
204
|
+
restrictedRow.created_by satisfies string;
|
|
205
|
+
await handlers.clientDb["${tableName}"].update({}, { body: "changed" });
|
|
206
|
+
const transactionResult = await handlers.getClientDbTx(async (tx) => {
|
|
207
|
+
const row = await tx["${tableName}"].insert({ body: "transaction" }, { returning: "*" });
|
|
208
|
+
row.created_by satisfies string;
|
|
209
|
+
// @ts-expect-error transactions preserve profile field restrictions
|
|
210
|
+
await tx["${tableName}"].update({}, { internal: "private" });
|
|
211
|
+
// @ts-expect-error transactions do not expose unpublished tables
|
|
212
|
+
await tx["${privateTableName}"].find();
|
|
213
|
+
// @ts-expect-error transactions do not expose raw SQL
|
|
214
|
+
tx.sql("SELECT 1");
|
|
215
|
+
// @ts-expect-error transactions do not expose nested transactions
|
|
216
|
+
tx.tx(() => {});
|
|
217
|
+
return row.id;
|
|
218
|
+
});
|
|
219
|
+
transactionResult satisfies number;
|
|
220
|
+
restricted.getClientDbTx satisfies typeof handlers.getClientDbTx;
|
|
221
|
+
// @ts-expect-error callbacks do not receive the unrestricted SQL transaction
|
|
222
|
+
handlers.getClientDbTx((tx, rawTx) => rawTx.any("SELECT 1"));
|
|
223
|
+
// @ts-expect-error getClientHandlers preserves profile field restrictions
|
|
224
|
+
await handlers.clientDb["${tableName}"].update({}, { internal: "private" });
|
|
225
|
+
// @ts-expect-error restricted functions respect forced insert fields
|
|
226
|
+
await restricted.dbo["${tableName}"].insert({ body: "hello", created_by: "other" });
|
|
227
|
+
// @ts-expect-error restricted functions cannot start transactions
|
|
228
|
+
restricted.dbo.tx(() => {});
|
|
229
|
+
// @ts-expect-error client wrappers do not expose isView
|
|
230
|
+
restricted.dbo["${tableName}"].isView;
|
|
231
|
+
// @ts-expect-error table is absent for some profiles
|
|
232
|
+
await serverClient["${privateTableName}"].find();
|
|
233
|
+
await serverClient["${privateTableName}"]?.find();
|
|
234
|
+
await unrestricted.dbo.tx(async (tx) => {
|
|
235
|
+
await tx["${tableName}"].update({}, { internal: "private" });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
await client["${tableName}"].update({}, { body: "changed" });
|
|
239
|
+
await guest.update({}, { body: 123 });
|
|
240
|
+
// @ts-expect-error table is absent for guests and viewers
|
|
241
|
+
await client["${privateTableName}"].find();
|
|
242
|
+
await client["${privateTableName}"]?.find();
|
|
243
|
+
await adminClient["${privateTableName}"].find();
|
|
244
|
+
const updated = await guest.update({}, { body: "changed", note: null }, { returning: "*" });
|
|
245
|
+
updated?.[0]?.created_by satisfies string | undefined;
|
|
246
|
+
await guest.updateBatch([[{}, { body: "changed" }]]);
|
|
247
|
+
// @ts-expect-error excluded update field
|
|
248
|
+
await guest.update({}, { internal: "private" });
|
|
249
|
+
const forcedUpdate = { body: "changed", created_by: "other" };
|
|
250
|
+
// @ts-expect-error forced update field through a variable
|
|
251
|
+
await guest.update({}, forcedUpdate);
|
|
252
|
+
// @ts-expect-error casts must not bypass excluded fields
|
|
253
|
+
await guest.update({}, { created_by: { $merge: [] } });
|
|
254
|
+
// @ts-expect-error updateBatch respects allowed fields
|
|
255
|
+
await guest.updateBatch([[{}, { internal: "private" }]]);
|
|
256
|
+
// @ts-expect-error read-only profiles cannot update
|
|
257
|
+
await viewer.update({}, {});
|
|
258
|
+
await admin.update({}, { internal: "private" });
|
|
259
|
+
await combined.update({}, { internal: "private" });
|
|
260
|
+
await server.update({}, { internal: "private" });
|
|
261
|
+
const row = await guest.insert({ body: "hello", note: null }, { returning: "*" });
|
|
262
|
+
row.created_by satisfies string;
|
|
263
|
+
row.synced satisfies string;
|
|
264
|
+
await guest.insertMany([{ body: "hello" }]);
|
|
265
|
+
// @ts-expect-error excluded fields cannot be inserted
|
|
266
|
+
await guest.insert({ body: "hello", internal: "private" });
|
|
267
|
+
// @ts-expect-error a read-only profile cannot insert
|
|
268
|
+
await viewer.insert({ body: "hello" });
|
|
269
|
+
// @ts-expect-error body remains required
|
|
270
|
+
await guest.insert({});
|
|
271
|
+
// @ts-expect-error forced values are forbidden
|
|
272
|
+
await guest.insert({ body: "hello", created_by: "other" });
|
|
273
|
+
const supplied = { body: "hello", created_by: "other" };
|
|
274
|
+
// @ts-expect-error also reject forced fields through variables
|
|
275
|
+
await guest.insert(supplied);
|
|
276
|
+
await admin.insert({ body: "hello", created_by: "admin" });
|
|
277
|
+
// @ts-expect-error admin must supply created_by
|
|
278
|
+
await admin.insert({ body: "hello" });
|
|
279
|
+
await combined.insert({ body: "hello" });
|
|
280
|
+
await combined.insert({ body: "hello", created_by: "admin" });
|
|
281
|
+
// @ts-expect-error read-only profiles do not relax required insert fields
|
|
282
|
+
await combined.insert({});
|
|
283
|
+
// @ts-expect-error server insert defaults are unchanged
|
|
284
|
+
await server.insert({ body: "hello", created_by: "admin" });
|
|
285
|
+
await server.insert({ body: "hello", created_by: "admin", synced: 1 });
|
|
286
|
+
await files.insert({ original_name: "hello.txt", data: new Uint8Array() });
|
|
287
|
+
await files.insert({ id: "file-id", original_name: "hello.txt", original_last_modified: null, data: new Uint8Array() });
|
|
288
|
+
// @ts-expect-error file metadata is generated by the server
|
|
289
|
+
await files.insert({ original_name: "hello.txt", data: new Uint8Array(), content_type: "text/plain" });
|
|
290
|
+
const fileWithGeneratedMetadata = { original_name: "hello.txt", data: new Uint8Array(), content_type: "text/plain" };
|
|
291
|
+
// @ts-expect-error file metadata is also rejected through variables
|
|
292
|
+
await files.insert(fileWithGeneratedMetadata);
|
|
293
|
+
// @ts-expect-error original_name is required
|
|
294
|
+
await files.insert({ data: new Uint8Array() });
|
|
295
|
+
await files.update({}, { original_name: "updated.txt", data: new Uint8Array() });
|
|
296
|
+
// @ts-expect-error file metadata cannot be updated directly
|
|
297
|
+
await files.update({}, { content_type: "text/plain" });
|
|
298
|
+
// @ts-expect-error file metadata updates are also rejected through variables
|
|
299
|
+
await files.update({}, fileWithGeneratedMetadata);
|
|
300
|
+
// @ts-expect-error unpublished tables are absent
|
|
301
|
+
type Private = GuestDBSchema["${privateTableName}"];
|
|
302
|
+
type Nested = InsertDataWithNested<{}, GuestDBSchema, "parent">;
|
|
303
|
+
const nested: Nested = { "${tableName}": [{ body: "hello" }] };
|
|
304
|
+
// @ts-expect-error nested inputs also exclude forced fields
|
|
305
|
+
const badNested: Nested = { "${tableName}": [{ created_by: "other" }] };
|
|
306
|
+
};
|
|
307
|
+
`,
|
|
308
|
+
);
|
|
309
|
+
const originalPublish = instance.options.publish;
|
|
310
|
+
// await instance.update({ publish: originalPublish });
|
|
311
|
+
const profiles: Record<string, ClientSchema> = {};
|
|
312
|
+
for (const [role, name] of [
|
|
313
|
+
["guest", "GuestDBSchema"],
|
|
314
|
+
["admin", "AdminDBSchema"],
|
|
315
|
+
["viewer", "ViewerDBSchema"],
|
|
316
|
+
["member", "MemberDBSchema"],
|
|
317
|
+
["unknown", "UnknownDBSchema"],
|
|
318
|
+
["", "AnonymousDBSchema"],
|
|
319
|
+
]) {
|
|
320
|
+
const socket = createClient(`http://127.0.0.1:${address.port}`, {
|
|
321
|
+
transports: ["websocket"],
|
|
322
|
+
reconnection: false,
|
|
323
|
+
query: role ? { token: role } : {},
|
|
324
|
+
});
|
|
325
|
+
sockets.push(socket);
|
|
326
|
+
profiles[name] = await new Promise<ClientSchema>((resolve, reject) => {
|
|
327
|
+
socket.once("connect_error", reject);
|
|
328
|
+
socket.once(CHANNELS.SCHEMA, resolve);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
const guestSchema = profiles.GuestDBSchema!;
|
|
332
|
+
assert.equal(guestSchema.err, undefined);
|
|
333
|
+
const table = guestSchema.tableSchema.find((t) => t.name === tableName);
|
|
334
|
+
assert(table);
|
|
335
|
+
assert.equal(table.columns.find((c) => c.name === "created_by")?.insert, false);
|
|
336
|
+
assert.deepEqual(profiles.MemberDBSchema!.tableSchema, guestSchema.tableSchema);
|
|
337
|
+
assert.deepEqual(profiles.UnknownDBSchema!.tableSchema, []);
|
|
338
|
+
assert.deepEqual(profiles.AnonymousDBSchema!.tableSchema, []);
|
|
339
|
+
const serverSocket = io.sockets.sockets.get(sockets[0]!.id!);
|
|
340
|
+
assert(serverSocket);
|
|
341
|
+
const handlers = await instance.getClientDBHandlers({ socket: serverSocket }, undefined);
|
|
342
|
+
assert.equal(privateTableName in handlers.clientDb, false);
|
|
343
|
+
assert.equal("delete" in handlers.clientDb[tableName]!, false);
|
|
344
|
+
assert.equal(typeof handlers.clientDb[tableName]!.upsert, "function");
|
|
345
|
+
assert.equal(typeof handlers.clientDb[tableName]!.insertMany, "function");
|
|
346
|
+
assert.equal("updateBatch" in handlers.clientDb[tableName]!, false);
|
|
347
|
+
const viewerSocket = io.sockets.sockets.get(sockets[2]!.id!);
|
|
348
|
+
assert(viewerSocket);
|
|
349
|
+
const viewerHandlers = await instance.getClientDBHandlers(
|
|
350
|
+
{ socket: viewerSocket },
|
|
351
|
+
undefined,
|
|
352
|
+
);
|
|
353
|
+
assert.deepEqual(Object.keys(viewerHandlers.clientDb).sort(), [tableName, "tx"]);
|
|
354
|
+
assert.deepEqual(Object.keys(viewerHandlers.clientDb[tableName]!).sort(), [
|
|
355
|
+
"count",
|
|
356
|
+
"find",
|
|
357
|
+
"findOne",
|
|
358
|
+
"getColumns",
|
|
359
|
+
"getInfo",
|
|
360
|
+
"size",
|
|
361
|
+
"subscribe",
|
|
362
|
+
"subscribeOne",
|
|
363
|
+
]);
|
|
364
|
+
const inserted = await (handlers.clientDb as DBHandlerServer)[tableName]!.insert!(
|
|
365
|
+
{ body: "hello" },
|
|
366
|
+
{ returning: "*" },
|
|
367
|
+
);
|
|
368
|
+
assert.equal(inserted.created_by, "user-guest");
|
|
369
|
+
assert(Number(inserted.synced) > 0);
|
|
370
|
+
const updated = await handlers.clientDb[tableName]!.update!(
|
|
371
|
+
{ id: inserted.id },
|
|
372
|
+
{ body: "updated" },
|
|
373
|
+
{ returning: "*" },
|
|
374
|
+
);
|
|
375
|
+
assert.equal(updated![0].body, "updated");
|
|
376
|
+
assert.equal(updated![0].created_by, "user-guest");
|
|
377
|
+
assert.equal(
|
|
378
|
+
(
|
|
379
|
+
await (handlers.clientDb as DBHandlerServer)[tableName]!.findOne!({
|
|
380
|
+
id: inserted.id,
|
|
381
|
+
})
|
|
382
|
+
).created_by,
|
|
383
|
+
"user-guest",
|
|
384
|
+
);
|
|
385
|
+
await assert.rejects(
|
|
386
|
+
(handlers.clientDb as DBHandlerServer)[tableName]!.update!(
|
|
387
|
+
{ id: inserted.id },
|
|
388
|
+
{ internal: "forbidden" },
|
|
389
|
+
),
|
|
390
|
+
);
|
|
391
|
+
const transactionRow = await handlers.getClientDbTx(async (...args) => {
|
|
392
|
+
assert.equal(args.length, 1);
|
|
393
|
+
const [tx] = args;
|
|
394
|
+
assert.equal("sql" in tx, false);
|
|
395
|
+
assert.throws(
|
|
396
|
+
() => (tx as DBHandlerServer).tx!(() => assert.fail("Nested transaction callback ran")),
|
|
397
|
+
/Transactions are not allowed in client handlers/,
|
|
398
|
+
);
|
|
399
|
+
const row = await tx[tableName]!.insert!(
|
|
400
|
+
{ body: "transaction", created_by: "other" },
|
|
401
|
+
{ returning: "*" },
|
|
402
|
+
);
|
|
403
|
+
assert.equal(row.created_by, "user-guest");
|
|
404
|
+
assert.equal(await tx[tableName]!.count!({ id: row.id }), 1);
|
|
405
|
+
assert.equal(await handlers.clientDb[tableName]!.count!({ id: row.id }), 0);
|
|
406
|
+
await tx[tableName]!.update!({ id: row.id }, { body: "committed" });
|
|
407
|
+
return tx[tableName]!.findOne!({ id: row.id });
|
|
408
|
+
});
|
|
409
|
+
assert.equal(transactionRow!.body, "committed");
|
|
410
|
+
assert.equal(await handlers.clientDb[tableName]!.count!({ id: transactionRow!.id }), 1);
|
|
411
|
+
await instance.db[tableName]!.insert!({ body: "hidden", created_by: "other" });
|
|
412
|
+
await handlers.getClientDbTx(async (tx) => {
|
|
413
|
+
assert.equal(await tx[tableName]!.count!({ body: "hidden" }), 0);
|
|
414
|
+
assert.equal(privateTableName in tx, false);
|
|
415
|
+
assert.equal("delete" in tx[tableName]!, false);
|
|
416
|
+
await assert.rejects(tx[tableName]!.subscribe!({}, {}, undefined));
|
|
417
|
+
assert.equal("db" in tx[tableName]!, false);
|
|
418
|
+
assert.equal("tx" in tx[tableName]!, false);
|
|
419
|
+
});
|
|
420
|
+
for (const failure of ["throw", "permission", "database"] as const) {
|
|
421
|
+
const body = `rollback-${failure}`;
|
|
422
|
+
await assert.rejects(
|
|
423
|
+
handlers.getClientDbTx(async (tx) => {
|
|
424
|
+
await tx[tableName]!.insert!({ body });
|
|
425
|
+
if (failure === "permission") {
|
|
426
|
+
await tx[tableName]!.update!({}, { internal: "forbidden" });
|
|
427
|
+
} else if (failure === "database") {
|
|
428
|
+
await tx[tableName]!.insert!({ id: transactionRow!.id, body });
|
|
429
|
+
} else {
|
|
430
|
+
throw new Error("rollback");
|
|
431
|
+
}
|
|
432
|
+
}),
|
|
433
|
+
);
|
|
434
|
+
assert.equal(await handlers.clientDb[tableName]!.count!({ body }), 0);
|
|
435
|
+
}
|
|
436
|
+
const scopedHandlers = await instance.getClientDBHandlers(
|
|
437
|
+
{ socket: serverSocket },
|
|
438
|
+
{ tables: { [tableName]: { select: { forcedFilter: { body: "committed" } } } } },
|
|
439
|
+
);
|
|
440
|
+
assert.deepEqual(Object.keys(scopedHandlers.clientDb).sort(), [tableName, "tx"]);
|
|
441
|
+
assert.equal("insert" in scopedHandlers.clientDb[tableName]!, false);
|
|
442
|
+
assert.equal("update" in scopedHandlers.clientDb[tableName]!, false);
|
|
443
|
+
assert.equal("upsert" in scopedHandlers.clientDb[tableName]!, false);
|
|
444
|
+
await scopedHandlers.getClientDbTx(async (tx) => {
|
|
445
|
+
assert.equal(await tx[tableName]!.count!(), 1);
|
|
446
|
+
assert.deepEqual(Object.keys(tx).sort(), Object.keys(scopedHandlers.clientDb).sort());
|
|
447
|
+
assert.deepEqual(
|
|
448
|
+
Object.keys(tx[tableName]!),
|
|
449
|
+
Object.keys(scopedHandlers.clientDb[tableName]!),
|
|
450
|
+
);
|
|
451
|
+
});
|
|
452
|
+
const functionRow = await handlers.clientMethods.transactionTest!.run({ fail: false });
|
|
453
|
+
const committedRow = await handlers.clientDb[tableName]!.findOne!({ body: "function-commit" });
|
|
454
|
+
assert.equal(committedRow!.created_by, "user-guest");
|
|
455
|
+
assert.deepEqual(functionRow, committedRow);
|
|
456
|
+
await assert.rejects(async () => handlers.clientMethods.transactionTest!.run({ fail: true }));
|
|
457
|
+
assert.equal(await handlers.clientDb[tableName]!.count!({ body: "function-rollback" }), 0);
|
|
458
|
+
sockets.forEach((socket) => socket.disconnect());
|
|
459
|
+
io.disconnectSockets(true);
|
|
460
|
+
await instance.update({ publish: originalPublish });
|
|
461
|
+
await instance.update({
|
|
462
|
+
publish: [{ name: "EmptyDBSchema", userTypes: ["empty"], publish: null }],
|
|
463
|
+
});
|
|
464
|
+
assert(
|
|
465
|
+
(await instance.getTSSchema()).tsSchema.includes(
|
|
466
|
+
"export type EmptyDBSchema = Record<string, never>;",
|
|
467
|
+
),
|
|
468
|
+
);
|
|
469
|
+
await instance.update({ publish: originalPublish });
|
|
470
|
+
for (const [publish, error] of [
|
|
471
|
+
[[{ userTypes: [], publish: null }], /userTypes must not be empty/],
|
|
472
|
+
[
|
|
473
|
+
[
|
|
474
|
+
{ userTypes: ["guest"], publish: null },
|
|
475
|
+
{ userTypes: ["guest"], publish: null },
|
|
476
|
+
],
|
|
477
|
+
/Duplicate publish user type/,
|
|
478
|
+
],
|
|
479
|
+
[
|
|
480
|
+
[{ name: "ClientDBSchema", userTypes: ["guest"], publish: null }],
|
|
481
|
+
/Invalid client schema name/,
|
|
482
|
+
],
|
|
483
|
+
[
|
|
484
|
+
[{ name: "ClientSchemas", userTypes: ["guest"], publish: null }],
|
|
485
|
+
/Invalid client schema name/,
|
|
486
|
+
],
|
|
487
|
+
[
|
|
488
|
+
[
|
|
489
|
+
{ name: "SameSchema", userTypes: ["a"], publish: null },
|
|
490
|
+
{ name: "SameSchema", userTypes: ["b"], publish: null },
|
|
491
|
+
],
|
|
492
|
+
/Duplicate publish schema name/,
|
|
493
|
+
],
|
|
494
|
+
] as const) {
|
|
495
|
+
// await instance.update({ publish: publish });
|
|
496
|
+
// await assert.rejects((handlers.clientDb as DBHandlerServer)[tableName]!.find!(), error);
|
|
497
|
+
// await instance.update({ publish: originalPublish });
|
|
498
|
+
await assert.rejects(instance.update({ publish }), error);
|
|
499
|
+
assert.equal((await instance.getTSSchema()).tsSchema, tsSchema);
|
|
500
|
+
}
|
|
501
|
+
await instance.update({
|
|
502
|
+
publish: [{ userTypes: ["guest"], publish: { [tableName]: { select: "*" } } }],
|
|
503
|
+
});
|
|
504
|
+
const updatedSchema = (await instance.getTSSchema()).tsSchema;
|
|
505
|
+
assert(updatedSchema.includes("export type Publish1Schema"));
|
|
506
|
+
assert(!updatedSchema.includes("GuestDBSchema"));
|
|
507
|
+
assert.equal(
|
|
508
|
+
readFileSync(
|
|
509
|
+
path.join(generatedTypesDir, `${DB_GENERATED_NAMES.SCHEMA}.ts`),
|
|
510
|
+
"utf8",
|
|
511
|
+
),
|
|
512
|
+
updatedSchema,
|
|
513
|
+
);
|
|
514
|
+
await instance.update({ publish: originalPublish });
|
|
515
|
+
assert.equal((await instance.getTSSchema()).tsSchema, tsSchema);
|
|
516
|
+
} finally {
|
|
517
|
+
sockets.forEach((socket) => socket.disconnect());
|
|
518
|
+
await instance?.destroy();
|
|
519
|
+
await new Promise<void>((resolve) => io.close(() => resolve()));
|
|
520
|
+
await db.none(`DROP SCHEMA IF EXISTS ${schemaName} CASCADE`);
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
);
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const checkTypes = (schema: string, checks: string) => {
|
|
527
|
+
const filename = path.resolve(__dirname, "../../../client/client-schema-typecheck.ts");
|
|
528
|
+
const options: ts.CompilerOptions = {
|
|
529
|
+
strict: true,
|
|
530
|
+
noEmit: true,
|
|
531
|
+
skipLibCheck: true,
|
|
532
|
+
target: ts.ScriptTarget.ES2022,
|
|
533
|
+
module: ts.ModuleKind.CommonJS,
|
|
534
|
+
moduleResolution: ts.ModuleResolutionKind.Node10,
|
|
535
|
+
};
|
|
536
|
+
const host = ts.createCompilerHost(options);
|
|
537
|
+
const getSourceFile = host.getSourceFile;
|
|
538
|
+
host.getSourceFile = (name, languageVersion, onError, shouldCreateNewSourceFile) =>
|
|
539
|
+
name === filename ?
|
|
540
|
+
ts.createSourceFile(name, schema + checks, languageVersion, true)
|
|
541
|
+
: getSourceFile(name, languageVersion, onError, shouldCreateNewSourceFile);
|
|
542
|
+
const program = ts.createProgram([filename], options, host);
|
|
543
|
+
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
544
|
+
assert.equal(
|
|
545
|
+
diagnostics.length,
|
|
546
|
+
0,
|
|
547
|
+
ts.formatDiagnosticsWithColorAndContext(diagnostics, {
|
|
548
|
+
getCanonicalFileName: (name) => name,
|
|
549
|
+
getCurrentDirectory: () => process.cwd(),
|
|
550
|
+
getNewLine: () => "\n",
|
|
551
|
+
}),
|
|
552
|
+
);
|
|
553
|
+
};
|
|
File without changes
|