@zenstackhq/testtools 3.0.0-beta.8 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +432 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -6
- package/dist/index.d.ts +74 -6
- package/dist/index.js +429 -11
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +16 -0
- package/package.json +20 -10
package/dist/index.d.cts
CHANGED
|
@@ -1,21 +1,89 @@
|
|
|
1
|
+
import { ClientOptions, ClientContract } from '@zenstackhq/orm';
|
|
2
|
+
import { SchemaDef } from '@zenstackhq/orm/schema';
|
|
3
|
+
import { LogEvent } from 'kysely';
|
|
1
4
|
import * as _zenstackhq_language_ast from '@zenstackhq/language/ast';
|
|
2
|
-
import { SchemaDef } from '@zenstackhq/
|
|
5
|
+
import { SchemaDef as SchemaDef$1 } from '@zenstackhq/schema';
|
|
6
|
+
|
|
7
|
+
declare function getTestDbProvider(): "sqlite" | "postgresql";
|
|
8
|
+
declare const TEST_PG_CONFIG: {
|
|
9
|
+
host: string;
|
|
10
|
+
port: number;
|
|
11
|
+
user: string;
|
|
12
|
+
password: string;
|
|
13
|
+
};
|
|
14
|
+
declare const TEST_PG_URL: string;
|
|
15
|
+
type ExtraTestClientOptions = {
|
|
16
|
+
/**
|
|
17
|
+
* Database provider
|
|
18
|
+
*/
|
|
19
|
+
provider?: 'sqlite' | 'postgresql';
|
|
20
|
+
/**
|
|
21
|
+
* The main ZModel file. Only used when `usePrismaPush` is true and `schema` is an object.
|
|
22
|
+
*/
|
|
23
|
+
schemaFile?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Database name. If not provided, a name will be generated based on the test name.
|
|
26
|
+
*/
|
|
27
|
+
dbName?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Use `prisma db push` instead of ZenStack's `$pushSchema` for database initialization.
|
|
30
|
+
*/
|
|
31
|
+
usePrismaPush?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Extra source files to create and compile.
|
|
34
|
+
*/
|
|
35
|
+
extraSourceFiles?: Record<string, string>;
|
|
36
|
+
/**
|
|
37
|
+
* Working directory for the test client. If not provided, a temporary directory will be created.
|
|
38
|
+
*/
|
|
39
|
+
workDir?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Debug mode.
|
|
42
|
+
*/
|
|
43
|
+
debug?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* A sqlite database file to be used for the test. Only supported for sqlite provider.
|
|
46
|
+
*/
|
|
47
|
+
dbFile?: string;
|
|
48
|
+
/**
|
|
49
|
+
* PostgreSQL extensions to be added to the datasource. Only supported for postgresql provider.
|
|
50
|
+
*/
|
|
51
|
+
dataSourceExtensions?: string[];
|
|
52
|
+
/**
|
|
53
|
+
* Additional files to be copied to the working directory. The glob pattern is relative to the test file.
|
|
54
|
+
*/
|
|
55
|
+
copyFiles?: {
|
|
56
|
+
globPattern: string;
|
|
57
|
+
destination: string;
|
|
58
|
+
}[];
|
|
59
|
+
};
|
|
60
|
+
type CreateTestClientOptions<Schema extends SchemaDef> = Omit<ClientOptions<Schema>, 'dialect'> & ExtraTestClientOptions;
|
|
61
|
+
declare function createTestClient<Schema extends SchemaDef, Options extends ClientOptions<Schema>, CreateOptions = Omit<Options, 'dialect'>>(schema: Schema, options?: CreateOptions): Promise<ClientContract<Schema, Options>>;
|
|
62
|
+
declare function createTestClient(schema: string, options?: CreateTestClientOptions<SchemaDef>): Promise<any>;
|
|
63
|
+
declare function createPolicyTestClient<Schema extends SchemaDef>(schema: Schema, options?: CreateTestClientOptions<Schema>): Promise<ClientContract<Schema>>;
|
|
64
|
+
declare function createPolicyTestClient<Schema extends SchemaDef>(schema: string, options?: CreateTestClientOptions<Schema>): Promise<any>;
|
|
65
|
+
declare function testLogger(e: LogEvent): void;
|
|
3
66
|
|
|
4
67
|
declare function createTestProject(zmodelContent?: string): string;
|
|
5
68
|
|
|
6
|
-
declare function generateTsSchema(schemaText: string, provider?: 'sqlite' | 'postgresql', dbUrl?: string, extraSourceFiles?: Record<string, string
|
|
69
|
+
declare function generateTsSchema(schemaText: string, provider?: 'sqlite' | 'postgresql', dbUrl?: string, extraSourceFiles?: Record<string, string>, withLiteSchema?: boolean): Promise<{
|
|
7
70
|
model: _zenstackhq_language_ast.Model;
|
|
8
71
|
workDir: string;
|
|
9
|
-
schema: SchemaDef;
|
|
72
|
+
schema: SchemaDef$1;
|
|
73
|
+
schemaLite: SchemaDef$1 | undefined;
|
|
10
74
|
}>;
|
|
11
75
|
declare function generateTsSchemaFromFile(filePath: string): Promise<{
|
|
12
76
|
model: _zenstackhq_language_ast.Model;
|
|
13
77
|
workDir: string;
|
|
14
|
-
schema: SchemaDef;
|
|
78
|
+
schema: SchemaDef$1;
|
|
79
|
+
schemaLite: SchemaDef$1 | undefined;
|
|
15
80
|
}>;
|
|
16
81
|
declare function generateTsSchemaInPlace(schemaPath: string): Promise<{
|
|
17
82
|
workDir: string;
|
|
18
|
-
schema: SchemaDef;
|
|
83
|
+
schema: SchemaDef$1;
|
|
84
|
+
schemaLite: SchemaDef$1 | undefined;
|
|
19
85
|
}>;
|
|
86
|
+
declare function loadSchema(schema: string, additionalSchemas?: Record<string, string>): Promise<_zenstackhq_language_ast.Model>;
|
|
87
|
+
declare function loadSchemaWithError(schema: string, error: string | RegExp): Promise<void>;
|
|
20
88
|
|
|
21
|
-
export { createTestProject, generateTsSchema, generateTsSchemaFromFile, generateTsSchemaInPlace };
|
|
89
|
+
export { type CreateTestClientOptions, TEST_PG_CONFIG, TEST_PG_URL, createPolicyTestClient, createTestClient, createTestProject, generateTsSchema, generateTsSchemaFromFile, generateTsSchemaInPlace, getTestDbProvider, loadSchema, loadSchemaWithError, testLogger };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,21 +1,89 @@
|
|
|
1
|
+
import { ClientOptions, ClientContract } from '@zenstackhq/orm';
|
|
2
|
+
import { SchemaDef } from '@zenstackhq/orm/schema';
|
|
3
|
+
import { LogEvent } from 'kysely';
|
|
1
4
|
import * as _zenstackhq_language_ast from '@zenstackhq/language/ast';
|
|
2
|
-
import { SchemaDef } from '@zenstackhq/
|
|
5
|
+
import { SchemaDef as SchemaDef$1 } from '@zenstackhq/schema';
|
|
6
|
+
|
|
7
|
+
declare function getTestDbProvider(): "sqlite" | "postgresql";
|
|
8
|
+
declare const TEST_PG_CONFIG: {
|
|
9
|
+
host: string;
|
|
10
|
+
port: number;
|
|
11
|
+
user: string;
|
|
12
|
+
password: string;
|
|
13
|
+
};
|
|
14
|
+
declare const TEST_PG_URL: string;
|
|
15
|
+
type ExtraTestClientOptions = {
|
|
16
|
+
/**
|
|
17
|
+
* Database provider
|
|
18
|
+
*/
|
|
19
|
+
provider?: 'sqlite' | 'postgresql';
|
|
20
|
+
/**
|
|
21
|
+
* The main ZModel file. Only used when `usePrismaPush` is true and `schema` is an object.
|
|
22
|
+
*/
|
|
23
|
+
schemaFile?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Database name. If not provided, a name will be generated based on the test name.
|
|
26
|
+
*/
|
|
27
|
+
dbName?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Use `prisma db push` instead of ZenStack's `$pushSchema` for database initialization.
|
|
30
|
+
*/
|
|
31
|
+
usePrismaPush?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Extra source files to create and compile.
|
|
34
|
+
*/
|
|
35
|
+
extraSourceFiles?: Record<string, string>;
|
|
36
|
+
/**
|
|
37
|
+
* Working directory for the test client. If not provided, a temporary directory will be created.
|
|
38
|
+
*/
|
|
39
|
+
workDir?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Debug mode.
|
|
42
|
+
*/
|
|
43
|
+
debug?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* A sqlite database file to be used for the test. Only supported for sqlite provider.
|
|
46
|
+
*/
|
|
47
|
+
dbFile?: string;
|
|
48
|
+
/**
|
|
49
|
+
* PostgreSQL extensions to be added to the datasource. Only supported for postgresql provider.
|
|
50
|
+
*/
|
|
51
|
+
dataSourceExtensions?: string[];
|
|
52
|
+
/**
|
|
53
|
+
* Additional files to be copied to the working directory. The glob pattern is relative to the test file.
|
|
54
|
+
*/
|
|
55
|
+
copyFiles?: {
|
|
56
|
+
globPattern: string;
|
|
57
|
+
destination: string;
|
|
58
|
+
}[];
|
|
59
|
+
};
|
|
60
|
+
type CreateTestClientOptions<Schema extends SchemaDef> = Omit<ClientOptions<Schema>, 'dialect'> & ExtraTestClientOptions;
|
|
61
|
+
declare function createTestClient<Schema extends SchemaDef, Options extends ClientOptions<Schema>, CreateOptions = Omit<Options, 'dialect'>>(schema: Schema, options?: CreateOptions): Promise<ClientContract<Schema, Options>>;
|
|
62
|
+
declare function createTestClient(schema: string, options?: CreateTestClientOptions<SchemaDef>): Promise<any>;
|
|
63
|
+
declare function createPolicyTestClient<Schema extends SchemaDef>(schema: Schema, options?: CreateTestClientOptions<Schema>): Promise<ClientContract<Schema>>;
|
|
64
|
+
declare function createPolicyTestClient<Schema extends SchemaDef>(schema: string, options?: CreateTestClientOptions<Schema>): Promise<any>;
|
|
65
|
+
declare function testLogger(e: LogEvent): void;
|
|
3
66
|
|
|
4
67
|
declare function createTestProject(zmodelContent?: string): string;
|
|
5
68
|
|
|
6
|
-
declare function generateTsSchema(schemaText: string, provider?: 'sqlite' | 'postgresql', dbUrl?: string, extraSourceFiles?: Record<string, string
|
|
69
|
+
declare function generateTsSchema(schemaText: string, provider?: 'sqlite' | 'postgresql', dbUrl?: string, extraSourceFiles?: Record<string, string>, withLiteSchema?: boolean): Promise<{
|
|
7
70
|
model: _zenstackhq_language_ast.Model;
|
|
8
71
|
workDir: string;
|
|
9
|
-
schema: SchemaDef;
|
|
72
|
+
schema: SchemaDef$1;
|
|
73
|
+
schemaLite: SchemaDef$1 | undefined;
|
|
10
74
|
}>;
|
|
11
75
|
declare function generateTsSchemaFromFile(filePath: string): Promise<{
|
|
12
76
|
model: _zenstackhq_language_ast.Model;
|
|
13
77
|
workDir: string;
|
|
14
|
-
schema: SchemaDef;
|
|
78
|
+
schema: SchemaDef$1;
|
|
79
|
+
schemaLite: SchemaDef$1 | undefined;
|
|
15
80
|
}>;
|
|
16
81
|
declare function generateTsSchemaInPlace(schemaPath: string): Promise<{
|
|
17
82
|
workDir: string;
|
|
18
|
-
schema: SchemaDef;
|
|
83
|
+
schema: SchemaDef$1;
|
|
84
|
+
schemaLite: SchemaDef$1 | undefined;
|
|
19
85
|
}>;
|
|
86
|
+
declare function loadSchema(schema: string, additionalSchemas?: Record<string, string>): Promise<_zenstackhq_language_ast.Model>;
|
|
87
|
+
declare function loadSchemaWithError(schema: string, error: string | RegExp): Promise<void>;
|
|
20
88
|
|
|
21
|
-
export { createTestProject, generateTsSchema, generateTsSchemaFromFile, generateTsSchemaInPlace };
|
|
89
|
+
export { type CreateTestClientOptions, TEST_PG_CONFIG, TEST_PG_URL, createPolicyTestClient, createTestClient, createTestProject, generateTsSchema, generateTsSchemaFromFile, generateTsSchemaInPlace, getTestDbProvider, loadSchema, loadSchemaWithError, testLogger };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
4
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
5
|
+
}) : x)(function(x) {
|
|
6
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
7
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
// src/client.ts
|
|
11
|
+
import { invariant as invariant2 } from "@zenstackhq/common-helpers";
|
|
12
|
+
import { ZenStackClient } from "@zenstackhq/orm";
|
|
13
|
+
import { PolicyPlugin } from "@zenstackhq/plugin-policy";
|
|
14
|
+
import { PrismaSchemaGenerator } from "@zenstackhq/sdk";
|
|
15
|
+
import SQLite from "better-sqlite3";
|
|
16
|
+
import { glob } from "glob";
|
|
17
|
+
import { PostgresDialect, SqliteDialect } from "kysely";
|
|
18
|
+
import { execSync as execSync2 } from "child_process";
|
|
19
|
+
import { createHash } from "crypto";
|
|
20
|
+
import fs3 from "fs";
|
|
21
|
+
import path3 from "path";
|
|
22
|
+
import { Client as PGClient, Pool } from "pg";
|
|
23
|
+
import { expect as expect2 } from "vitest";
|
|
3
24
|
|
|
4
25
|
// src/project.ts
|
|
5
26
|
import fs from "fs";
|
|
@@ -20,7 +41,7 @@ function createTestProject(zmodelContent) {
|
|
|
20
41
|
const zenstackPackages = [
|
|
21
42
|
"language",
|
|
22
43
|
"sdk",
|
|
23
|
-
"
|
|
44
|
+
"orm",
|
|
24
45
|
"cli"
|
|
25
46
|
];
|
|
26
47
|
fs.mkdirSync(path.join(workDir, "node_modules/@zenstackhq"));
|
|
@@ -53,12 +74,27 @@ function createTestProject(zmodelContent) {
|
|
|
53
74
|
__name(createTestProject, "createTestProject");
|
|
54
75
|
|
|
55
76
|
// src/schema.ts
|
|
56
|
-
import {
|
|
77
|
+
import { invariant } from "@zenstackhq/common-helpers";
|
|
57
78
|
import { TsSchemaGenerator } from "@zenstackhq/sdk";
|
|
58
79
|
import { execSync } from "child_process";
|
|
80
|
+
import crypto from "crypto";
|
|
59
81
|
import fs2 from "fs";
|
|
82
|
+
import os from "os";
|
|
60
83
|
import path2 from "path";
|
|
61
84
|
import { match } from "ts-pattern";
|
|
85
|
+
import { expect } from "vitest";
|
|
86
|
+
|
|
87
|
+
// src/utils.ts
|
|
88
|
+
import { loadDocument } from "@zenstackhq/language";
|
|
89
|
+
function loadDocumentWithPlugins(filePath) {
|
|
90
|
+
const pluginModelFiles = [
|
|
91
|
+
__require.resolve("@zenstackhq/plugin-policy/plugin.zmodel")
|
|
92
|
+
];
|
|
93
|
+
return loadDocument(filePath, pluginModelFiles);
|
|
94
|
+
}
|
|
95
|
+
__name(loadDocumentWithPlugins, "loadDocumentWithPlugins");
|
|
96
|
+
|
|
97
|
+
// src/schema.ts
|
|
62
98
|
function makePrelude(provider, dbUrl) {
|
|
63
99
|
return match(provider).with("sqlite", () => {
|
|
64
100
|
return `
|
|
@@ -77,22 +113,30 @@ datasource db {
|
|
|
77
113
|
}).exhaustive();
|
|
78
114
|
}
|
|
79
115
|
__name(makePrelude, "makePrelude");
|
|
80
|
-
|
|
116
|
+
function replacePlaceholders(schemaText, provider, dbUrl) {
|
|
117
|
+
const url = dbUrl ?? (provider === "sqlite" ? "file:./test.db" : "postgres://postgres:postgres@localhost:5432/db");
|
|
118
|
+
return schemaText.replace(/\$DB_URL/g, url).replace(/\$PROVIDER/g, provider);
|
|
119
|
+
}
|
|
120
|
+
__name(replacePlaceholders, "replacePlaceholders");
|
|
121
|
+
async function generateTsSchema(schemaText, provider = "sqlite", dbUrl, extraSourceFiles, withLiteSchema) {
|
|
81
122
|
const workDir = createTestProject();
|
|
82
123
|
const zmodelPath = path2.join(workDir, "schema.zmodel");
|
|
83
124
|
const noPrelude = schemaText.includes("datasource ");
|
|
84
125
|
fs2.writeFileSync(zmodelPath, `${noPrelude ? "" : makePrelude(provider, dbUrl)}
|
|
85
126
|
|
|
86
|
-
${schemaText}`);
|
|
87
|
-
const result = await
|
|
127
|
+
${replacePlaceholders(schemaText, provider, dbUrl)}`);
|
|
128
|
+
const result = await loadDocumentWithPlugins(zmodelPath);
|
|
88
129
|
if (!result.success) {
|
|
89
130
|
throw new Error(`Failed to load schema from ${zmodelPath}: ${result.errors}`);
|
|
90
131
|
}
|
|
91
132
|
const generator = new TsSchemaGenerator();
|
|
92
|
-
await generator.generate(result.model,
|
|
133
|
+
await generator.generate(result.model, {
|
|
134
|
+
outDir: workDir,
|
|
135
|
+
lite: withLiteSchema
|
|
136
|
+
});
|
|
93
137
|
if (extraSourceFiles) {
|
|
94
138
|
for (const [fileName, content] of Object.entries(extraSourceFiles)) {
|
|
95
|
-
const filePath = path2.resolve(workDir, `${fileName}.ts`);
|
|
139
|
+
const filePath = path2.resolve(workDir, !fileName.endsWith(".ts") ? `${fileName}.ts` : fileName);
|
|
96
140
|
fs2.mkdirSync(path2.dirname(filePath), {
|
|
97
141
|
recursive: true
|
|
98
142
|
});
|
|
@@ -111,9 +155,15 @@ async function compileAndLoad(workDir) {
|
|
|
111
155
|
stdio: "inherit"
|
|
112
156
|
});
|
|
113
157
|
const module = await import(path2.join(workDir, "schema.js"));
|
|
158
|
+
let moduleLite;
|
|
159
|
+
try {
|
|
160
|
+
moduleLite = await import(path2.join(workDir, "schema-lite.js"));
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
114
163
|
return {
|
|
115
164
|
workDir,
|
|
116
|
-
schema: module.schema
|
|
165
|
+
schema: module.schema,
|
|
166
|
+
schemaLite: moduleLite?.schema
|
|
117
167
|
};
|
|
118
168
|
}
|
|
119
169
|
__name(compileAndLoad, "compileAndLoad");
|
|
@@ -124,19 +174,387 @@ function generateTsSchemaFromFile(filePath) {
|
|
|
124
174
|
__name(generateTsSchemaFromFile, "generateTsSchemaFromFile");
|
|
125
175
|
async function generateTsSchemaInPlace(schemaPath) {
|
|
126
176
|
const workDir = path2.dirname(schemaPath);
|
|
127
|
-
const result = await
|
|
177
|
+
const result = await loadDocumentWithPlugins(schemaPath);
|
|
128
178
|
if (!result.success) {
|
|
129
179
|
throw new Error(`Failed to load schema from ${schemaPath}: ${result.errors}`);
|
|
130
180
|
}
|
|
131
181
|
const generator = new TsSchemaGenerator();
|
|
132
|
-
await generator.generate(result.model,
|
|
182
|
+
await generator.generate(result.model, {
|
|
183
|
+
outDir: workDir
|
|
184
|
+
});
|
|
133
185
|
return compileAndLoad(workDir);
|
|
134
186
|
}
|
|
135
187
|
__name(generateTsSchemaInPlace, "generateTsSchemaInPlace");
|
|
188
|
+
async function loadSchema(schema, additionalSchemas) {
|
|
189
|
+
if (!schema.includes("datasource ")) {
|
|
190
|
+
schema = `${makePrelude("sqlite")}
|
|
191
|
+
|
|
192
|
+
${schema}`;
|
|
193
|
+
}
|
|
194
|
+
const tempDir = fs2.mkdtempSync(path2.join(os.tmpdir(), "zenstack-schema"));
|
|
195
|
+
const tempFile = path2.join(tempDir, `schema.zmodel`);
|
|
196
|
+
fs2.writeFileSync(tempFile, schema);
|
|
197
|
+
if (additionalSchemas) {
|
|
198
|
+
for (const [fileName, content] of Object.entries(additionalSchemas)) {
|
|
199
|
+
let name = fileName;
|
|
200
|
+
if (!name.endsWith(".zmodel")) {
|
|
201
|
+
name += ".zmodel";
|
|
202
|
+
}
|
|
203
|
+
const filePath = path2.join(tempDir, name);
|
|
204
|
+
fs2.writeFileSync(filePath, content);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const r = await loadDocumentWithPlugins(tempFile);
|
|
208
|
+
expect(r).toSatisfy((r2) => r2.success, `Failed to load schema: ${r.errors?.map((e) => e.toString()).join(", ")}`);
|
|
209
|
+
invariant(r.success);
|
|
210
|
+
return r.model;
|
|
211
|
+
}
|
|
212
|
+
__name(loadSchema, "loadSchema");
|
|
213
|
+
async function loadSchemaWithError(schema, error) {
|
|
214
|
+
if (!schema.includes("datasource ")) {
|
|
215
|
+
schema = `${makePrelude("sqlite")}
|
|
216
|
+
|
|
217
|
+
${schema}`;
|
|
218
|
+
}
|
|
219
|
+
const tempFile = path2.join(os.tmpdir(), `zenstack-schema-${crypto.randomUUID()}.zmodel`);
|
|
220
|
+
fs2.writeFileSync(tempFile, schema);
|
|
221
|
+
const r = await loadDocumentWithPlugins(tempFile);
|
|
222
|
+
expect(r.success).toBe(false);
|
|
223
|
+
invariant(!r.success);
|
|
224
|
+
if (typeof error === "string") {
|
|
225
|
+
expect(r).toSatisfy((r2) => r2.errors.some((e) => e.toString().toLowerCase().includes(error.toLowerCase())), `Expected error message to include "${error}" but got: ${r.errors.map((e) => e.toString()).join(", ")}`);
|
|
226
|
+
} else {
|
|
227
|
+
expect(r).toSatisfy((r2) => r2.errors.some((e) => error.test(e)), `Expected error message to match "${error}" but got: ${r.errors.map((e) => e.toString()).join(", ")}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
__name(loadSchemaWithError, "loadSchemaWithError");
|
|
231
|
+
|
|
232
|
+
// src/client.ts
|
|
233
|
+
function getTestDbProvider() {
|
|
234
|
+
const val = process.env["TEST_DB_PROVIDER"] ?? "sqlite";
|
|
235
|
+
if (![
|
|
236
|
+
"sqlite",
|
|
237
|
+
"postgresql"
|
|
238
|
+
].includes(val)) {
|
|
239
|
+
throw new Error(`Invalid TEST_DB_PROVIDER value: ${val}`);
|
|
240
|
+
}
|
|
241
|
+
return val;
|
|
242
|
+
}
|
|
243
|
+
__name(getTestDbProvider, "getTestDbProvider");
|
|
244
|
+
var TEST_PG_CONFIG = {
|
|
245
|
+
host: process.env["TEST_PG_HOST"] ?? "localhost",
|
|
246
|
+
port: process.env["TEST_PG_PORT"] ? parseInt(process.env["TEST_PG_PORT"]) : 5432,
|
|
247
|
+
user: process.env["TEST_PG_USER"] ?? "postgres",
|
|
248
|
+
password: process.env["TEST_PG_PASSWORD"] ?? "postgres"
|
|
249
|
+
};
|
|
250
|
+
var TEST_PG_URL = `postgres://${TEST_PG_CONFIG.user}:${TEST_PG_CONFIG.password}@${TEST_PG_CONFIG.host}:${TEST_PG_CONFIG.port}`;
|
|
251
|
+
async function createTestClient(schema, options) {
|
|
252
|
+
let workDir = options?.workDir;
|
|
253
|
+
let _schema;
|
|
254
|
+
const provider = options?.provider ?? getTestDbProvider() ?? "sqlite";
|
|
255
|
+
const dbName = options?.dbName ?? getTestDbName(provider);
|
|
256
|
+
const dbUrl = provider === "sqlite" ? `file:${dbName}` : `${TEST_PG_URL}/${dbName}`;
|
|
257
|
+
let model;
|
|
258
|
+
if (typeof schema === "string") {
|
|
259
|
+
const generated = await generateTsSchema(schema, provider, dbUrl, options?.extraSourceFiles, void 0);
|
|
260
|
+
workDir = generated.workDir;
|
|
261
|
+
model = generated.model;
|
|
262
|
+
_schema = {
|
|
263
|
+
...generated.schema,
|
|
264
|
+
provider: {
|
|
265
|
+
...generated.schema.provider,
|
|
266
|
+
type: provider
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
} else {
|
|
270
|
+
_schema = {
|
|
271
|
+
...schema,
|
|
272
|
+
provider: {
|
|
273
|
+
type: provider
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
workDir ??= createTestProject();
|
|
277
|
+
if (options?.schemaFile) {
|
|
278
|
+
let schemaContent = fs3.readFileSync(options.schemaFile, "utf-8");
|
|
279
|
+
if (dbUrl) {
|
|
280
|
+
schemaContent = schemaContent.replace(/datasource\s+db\s*{[^}]*}/m, `datasource db {
|
|
281
|
+
provider = '${provider}'
|
|
282
|
+
url = '${dbUrl}'
|
|
283
|
+
${options.dataSourceExtensions ? `extensions = [${options.dataSourceExtensions.join(", ")}]` : ""}
|
|
284
|
+
}`);
|
|
285
|
+
}
|
|
286
|
+
fs3.writeFileSync(path3.join(workDir, "schema.zmodel"), schemaContent);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
invariant2(workDir);
|
|
290
|
+
const { plugins, ...rest } = options ?? {};
|
|
291
|
+
const _options = {
|
|
292
|
+
...rest
|
|
293
|
+
};
|
|
294
|
+
if (options?.debug) {
|
|
295
|
+
console.log(`Work directory: ${workDir}`);
|
|
296
|
+
console.log(`Database name: ${dbName}`);
|
|
297
|
+
_options.log = testLogger;
|
|
298
|
+
}
|
|
299
|
+
if (options?.dbFile) {
|
|
300
|
+
if (provider !== "sqlite") {
|
|
301
|
+
throw new Error("dbFile option is only supported for sqlite provider");
|
|
302
|
+
}
|
|
303
|
+
fs3.copyFileSync(options.dbFile, path3.join(workDir, dbName));
|
|
304
|
+
}
|
|
305
|
+
if (options?.copyFiles) {
|
|
306
|
+
const state = expect2.getState();
|
|
307
|
+
const currentTestPath = state.testPath;
|
|
308
|
+
if (!currentTestPath) {
|
|
309
|
+
throw new Error("Unable to determine current test file path");
|
|
310
|
+
}
|
|
311
|
+
for (const { globPattern, destination } of options.copyFiles) {
|
|
312
|
+
const files = glob.sync(globPattern, {
|
|
313
|
+
cwd: path3.dirname(currentTestPath)
|
|
314
|
+
});
|
|
315
|
+
for (const file of files) {
|
|
316
|
+
const src = path3.resolve(path3.dirname(currentTestPath), file);
|
|
317
|
+
const dest = path3.resolve(workDir, destination, path3.basename(file));
|
|
318
|
+
fs3.mkdirSync(path3.dirname(dest), {
|
|
319
|
+
recursive: true
|
|
320
|
+
});
|
|
321
|
+
fs3.copyFileSync(src, dest);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (!options?.dbFile) {
|
|
326
|
+
if (options?.usePrismaPush) {
|
|
327
|
+
invariant2(typeof schema === "string" || options?.schemaFile, "a schema file must be provided when using prisma db push");
|
|
328
|
+
if (!model) {
|
|
329
|
+
const r = await loadDocumentWithPlugins(path3.join(workDir, "schema.zmodel"));
|
|
330
|
+
if (!r.success) {
|
|
331
|
+
throw new Error(r.errors.join("\n"));
|
|
332
|
+
}
|
|
333
|
+
model = r.model;
|
|
334
|
+
}
|
|
335
|
+
const prismaSchema = new PrismaSchemaGenerator(model);
|
|
336
|
+
const prismaSchemaText = await prismaSchema.generate();
|
|
337
|
+
fs3.writeFileSync(path3.resolve(workDir, "schema.prisma"), prismaSchemaText);
|
|
338
|
+
execSync2("npx prisma db push --schema ./schema.prisma --skip-generate --force-reset", {
|
|
339
|
+
cwd: workDir,
|
|
340
|
+
stdio: "ignore"
|
|
341
|
+
});
|
|
342
|
+
} else {
|
|
343
|
+
if (provider === "postgresql") {
|
|
344
|
+
invariant2(dbName, "dbName is required");
|
|
345
|
+
const pgClient = new PGClient(TEST_PG_CONFIG);
|
|
346
|
+
await pgClient.connect();
|
|
347
|
+
await pgClient.query(`DROP DATABASE IF EXISTS "${dbName}"`);
|
|
348
|
+
await pgClient.query(`CREATE DATABASE "${dbName}"`);
|
|
349
|
+
await pgClient.end();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (provider === "postgresql") {
|
|
354
|
+
_options.dialect = new PostgresDialect({
|
|
355
|
+
pool: new Pool({
|
|
356
|
+
...TEST_PG_CONFIG,
|
|
357
|
+
database: dbName
|
|
358
|
+
})
|
|
359
|
+
});
|
|
360
|
+
} else {
|
|
361
|
+
_options.dialect = new SqliteDialect({
|
|
362
|
+
database: new SQLite(path3.join(workDir, dbName))
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
let client = new ZenStackClient(_schema, _options);
|
|
366
|
+
if (!options?.usePrismaPush && !options?.dbFile) {
|
|
367
|
+
await client.$pushSchema();
|
|
368
|
+
}
|
|
369
|
+
if (plugins) {
|
|
370
|
+
for (const plugin of plugins) {
|
|
371
|
+
client = client.$use(plugin);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return client;
|
|
375
|
+
}
|
|
376
|
+
__name(createTestClient, "createTestClient");
|
|
377
|
+
async function createPolicyTestClient(schema, options) {
|
|
378
|
+
return createTestClient(schema, {
|
|
379
|
+
...options,
|
|
380
|
+
plugins: [
|
|
381
|
+
...options?.plugins ?? [],
|
|
382
|
+
new PolicyPlugin()
|
|
383
|
+
]
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
__name(createPolicyTestClient, "createPolicyTestClient");
|
|
387
|
+
function testLogger(e) {
|
|
388
|
+
console.log(e.query.sql, e.query.parameters);
|
|
389
|
+
}
|
|
390
|
+
__name(testLogger, "testLogger");
|
|
391
|
+
function getTestDbName(provider) {
|
|
392
|
+
if (provider === "sqlite") {
|
|
393
|
+
return "./test.db";
|
|
394
|
+
}
|
|
395
|
+
const testName = expect2.getState().currentTestName ?? "unnamed";
|
|
396
|
+
const testPath = expect2.getState().testPath ?? "";
|
|
397
|
+
const digest = createHash("md5").update(testName + testPath).digest("hex");
|
|
398
|
+
return "test_" + testName.toLowerCase().replace(/[^a-z0-9_]/g, "_").replace(/_+/g, "_").substring(0, 30) + digest.slice(0, 6);
|
|
399
|
+
}
|
|
400
|
+
__name(getTestDbName, "getTestDbName");
|
|
401
|
+
|
|
402
|
+
// src/vitest-ext.ts
|
|
403
|
+
import { ORMError, ORMErrorReason } from "@zenstackhq/orm";
|
|
404
|
+
import { expect as expect3 } from "vitest";
|
|
405
|
+
function isPromise(value) {
|
|
406
|
+
return typeof value.then === "function" && typeof value.catch === "function";
|
|
407
|
+
}
|
|
408
|
+
__name(isPromise, "isPromise");
|
|
409
|
+
function expectErrorReason(err, errorReason) {
|
|
410
|
+
if (err instanceof ORMError && err.reason === errorReason) {
|
|
411
|
+
return {
|
|
412
|
+
message: /* @__PURE__ */ __name(() => "", "message"),
|
|
413
|
+
pass: true
|
|
414
|
+
};
|
|
415
|
+
} else {
|
|
416
|
+
return {
|
|
417
|
+
message: /* @__PURE__ */ __name(() => `expected ORMError of reason ${errorReason}, got ${err}`, "message"),
|
|
418
|
+
pass: false
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
__name(expectErrorReason, "expectErrorReason");
|
|
423
|
+
function expectErrorMessages(expectedMessages, message) {
|
|
424
|
+
for (const m of expectedMessages) {
|
|
425
|
+
if (!message.toLowerCase().includes(m.toLowerCase())) {
|
|
426
|
+
return {
|
|
427
|
+
message: /* @__PURE__ */ __name(() => `expected message not found in error: ${m}, got message: ${message}`, "message"),
|
|
428
|
+
pass: false
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return void 0;
|
|
433
|
+
}
|
|
434
|
+
__name(expectErrorMessages, "expectErrorMessages");
|
|
435
|
+
expect3.extend({
|
|
436
|
+
async toResolveTruthy(received) {
|
|
437
|
+
if (!isPromise(received)) {
|
|
438
|
+
return {
|
|
439
|
+
message: /* @__PURE__ */ __name(() => "a promise is expected", "message"),
|
|
440
|
+
pass: false
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const r = await received;
|
|
444
|
+
return {
|
|
445
|
+
pass: !!r,
|
|
446
|
+
message: /* @__PURE__ */ __name(() => `Expected promise to resolve to a truthy value, but got ${r}`, "message")
|
|
447
|
+
};
|
|
448
|
+
},
|
|
449
|
+
async toResolveFalsy(received) {
|
|
450
|
+
if (!isPromise(received)) {
|
|
451
|
+
return {
|
|
452
|
+
message: /* @__PURE__ */ __name(() => "a promise is expected", "message"),
|
|
453
|
+
pass: false
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
const r = await received;
|
|
457
|
+
return {
|
|
458
|
+
pass: !r,
|
|
459
|
+
message: /* @__PURE__ */ __name(() => `Expected promise to resolve to a falsy value, but got ${r}`, "message")
|
|
460
|
+
};
|
|
461
|
+
},
|
|
462
|
+
async toResolveNull(received) {
|
|
463
|
+
if (!isPromise(received)) {
|
|
464
|
+
return {
|
|
465
|
+
message: /* @__PURE__ */ __name(() => "a promise is expected", "message"),
|
|
466
|
+
pass: false
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
const r = await received;
|
|
470
|
+
return {
|
|
471
|
+
pass: r === null,
|
|
472
|
+
message: /* @__PURE__ */ __name(() => `Expected promise to resolve to a null value, but got ${r}`, "message")
|
|
473
|
+
};
|
|
474
|
+
},
|
|
475
|
+
async toResolveWithLength(received, length) {
|
|
476
|
+
const r = await received;
|
|
477
|
+
return {
|
|
478
|
+
pass: Array.isArray(r) && r.length === length,
|
|
479
|
+
message: /* @__PURE__ */ __name(() => `Expected promise to resolve with an array with length ${length}, but got ${r}`, "message")
|
|
480
|
+
};
|
|
481
|
+
},
|
|
482
|
+
async toBeRejectedNotFound(received) {
|
|
483
|
+
if (!isPromise(received)) {
|
|
484
|
+
return {
|
|
485
|
+
message: /* @__PURE__ */ __name(() => "a promise is expected", "message"),
|
|
486
|
+
pass: false
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
await received;
|
|
491
|
+
} catch (err) {
|
|
492
|
+
return expectErrorReason(err, ORMErrorReason.NOT_FOUND);
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
message: /* @__PURE__ */ __name(() => `expected NotFoundError, got no error`, "message"),
|
|
496
|
+
pass: false
|
|
497
|
+
};
|
|
498
|
+
},
|
|
499
|
+
async toBeRejectedByPolicy(received, expectedMessages) {
|
|
500
|
+
if (!isPromise(received)) {
|
|
501
|
+
return {
|
|
502
|
+
message: /* @__PURE__ */ __name(() => "a promise is expected", "message"),
|
|
503
|
+
pass: false
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
try {
|
|
507
|
+
await received;
|
|
508
|
+
} catch (err) {
|
|
509
|
+
if (expectedMessages && err instanceof ORMError && err.reason === ORMErrorReason.REJECTED_BY_POLICY) {
|
|
510
|
+
const r = expectErrorMessages(expectedMessages, err.message || "");
|
|
511
|
+
if (r) {
|
|
512
|
+
return r;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return expectErrorReason(err, ORMErrorReason.REJECTED_BY_POLICY);
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
message: /* @__PURE__ */ __name(() => `expected PolicyError, got no error`, "message"),
|
|
519
|
+
pass: false
|
|
520
|
+
};
|
|
521
|
+
},
|
|
522
|
+
async toBeRejectedByValidation(received, expectedMessages) {
|
|
523
|
+
if (!isPromise(received)) {
|
|
524
|
+
return {
|
|
525
|
+
message: /* @__PURE__ */ __name(() => "a promise is expected", "message"),
|
|
526
|
+
pass: false
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
try {
|
|
530
|
+
await received;
|
|
531
|
+
} catch (err) {
|
|
532
|
+
if (expectedMessages && err instanceof ORMError && err.reason === ORMErrorReason.INVALID_INPUT) {
|
|
533
|
+
const r = expectErrorMessages(expectedMessages, err.message || "");
|
|
534
|
+
if (r) {
|
|
535
|
+
return r;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return expectErrorReason(err, ORMErrorReason.INVALID_INPUT);
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
message: /* @__PURE__ */ __name(() => `expected InputValidationError, got no error`, "message"),
|
|
542
|
+
pass: false
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
});
|
|
136
546
|
export {
|
|
547
|
+
TEST_PG_CONFIG,
|
|
548
|
+
TEST_PG_URL,
|
|
549
|
+
createPolicyTestClient,
|
|
550
|
+
createTestClient,
|
|
137
551
|
createTestProject,
|
|
138
552
|
generateTsSchema,
|
|
139
553
|
generateTsSchemaFromFile,
|
|
140
|
-
generateTsSchemaInPlace
|
|
554
|
+
generateTsSchemaInPlace,
|
|
555
|
+
getTestDbProvider,
|
|
556
|
+
loadSchema,
|
|
557
|
+
loadSchemaWithError,
|
|
558
|
+
testLogger
|
|
141
559
|
};
|
|
142
560
|
//# sourceMappingURL=index.js.map
|