create-monolayer-app 0.5.0 → 0.6.1
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/bin/index.js +91 -59
- package/package.json +19 -16
- package/template/.agents/skills/{monolayer-application-permissions → application-permissions}/SKILL.md +2 -2
- package/template/.agents/skills/application-permissions/agents/openai.yaml +4 -0
- package/template/.agents/skills/kysely/SKILL.md +48 -0
- package/template/.agents/skills/kysely/agents/openai.yaml +4 -0
- package/template/.agents/skills/kysely/references/execution-plugins-and-runtime.md +67 -0
- package/template/.agents/skills/kysely/references/expressions-and-helpers.md +102 -0
- package/template/.agents/skills/kysely/references/migrations.md +43 -0
- package/template/.agents/skills/kysely/references/official-docs-map.md +97 -0
- package/template/.agents/skills/kysely/references/query-building.md +119 -0
- package/template/.agents/skills/kysely/references/transactions.md +47 -0
- package/template/.agents/skills/kysely/references/troubleshooting-and-boundaries.md +67 -0
- package/template/.agents/skills/kysely/references/types.md +58 -0
- package/template/.agents/skills/persistent-app-data/SKILL.md +74 -0
- package/template/.agents/skills/persistent-app-data/agents/openai.yaml +4 -0
- package/template/.agents/skills/persistent-app-data/references/generated-types.md +23 -0
- package/template/.agents/skills/persistent-app-data/references/migrations.md +68 -0
- package/template/.agents/skills/vitest/GENERATION.md +25 -0
- package/template/.agents/skills/vitest/SKILL.md +59 -0
- package/template/.agents/skills/vitest/references/advanced-environments.md +26 -0
- package/template/.agents/skills/vitest/references/advanced-projects.md +31 -0
- package/template/.agents/skills/vitest/references/advanced-type-testing.md +30 -0
- package/template/.agents/skills/vitest/references/advanced-vi.md +23 -0
- package/template/.agents/skills/vitest/references/core-cli.md +23 -0
- package/template/.agents/skills/vitest/references/core-config.md +24 -0
- package/template/.agents/skills/vitest/references/core-describe.md +24 -0
- package/template/.agents/skills/vitest/references/core-expect.md +35 -0
- package/template/.agents/skills/vitest/references/core-hooks.md +26 -0
- package/template/.agents/skills/vitest/references/core-test-api.md +29 -0
- package/template/.agents/skills/vitest/references/features-benchmarking.md +25 -0
- package/template/.agents/skills/vitest/references/features-concurrency.md +20 -0
- package/template/.agents/skills/vitest/references/features-context.md +27 -0
- package/template/.agents/skills/vitest/references/features-coverage.md +17 -0
- package/template/.agents/skills/vitest/references/features-filtering.md +24 -0
- package/template/.agents/skills/vitest/references/features-mocking.md +32 -0
- package/template/.agents/skills/vitest/references/features-reporters.md +22 -0
- package/template/.agents/skills/vitest/references/features-snapshots.md +23 -0
- package/template/.agents/skills/vitest/references/features-test-tags.md +30 -0
- package/template/.agents/skills/vitest/references/version-compatibility.md +19 -0
- package/template/.claude/CLAUDE.md +2 -2
- package/template/.react-router/types/+routes.ts +41 -0
- package/template/.react-router/types/+server-build.d.ts +18 -0
- package/template/.react-router/types/app/+types/root.ts +68 -0
- package/template/.react-router/types/app/routes/+types/files.ts +71 -0
- package/template/.react-router/types/app/routes/+types/home.ts +71 -0
- package/template/AGENTS.md +6 -2
- package/template/app/routes/files.ts +3 -3
- package/template/build/client/assets/entry.client-Bc58m2-m.js +9 -0
- package/template/build/client/assets/errorBoundaries-CQxPIuVU.js +16 -0
- package/template/build/client/assets/files-BvRk9kiK.js +0 -0
- package/template/build/client/assets/home-CH2INc5e.js +1 -0
- package/template/build/client/assets/jsx-runtime-eLCqVKHJ.js +1 -0
- package/template/build/client/assets/logo-dark-pX2395Y0.svg +23 -0
- package/template/build/client/assets/logo-light-CVbx2LBR.svg +23 -0
- package/template/build/client/assets/manifest-2d65dd0f.js +1 -0
- package/template/build/client/assets/root-BeNS6njP.css +2 -0
- package/template/build/client/assets/root-C1tmjS9o.js +1 -0
- package/template/build/client/favicon.ico +0 -0
- package/template/build/server/index.js +1096 -0
- package/template/lib/db/client.ts +1 -27
- package/template/package-lock.json +8221 -0
- package/template/package.json +7 -3
- package/template/vendor/file-bucket/{client.ts → index.client.ts} +11 -9
- package/template/vendor/file-bucket/{server.ts → index.server.ts} +13 -8
- package/template/vendor/file-bucket/{index.ts → lib/file-bucket.ts} +2 -1
- package/template/vendor/file-bucket/{react-router.ts → react-router.server.ts} +15 -13
- package/template/vendor/kysely-dsql/dsql/dsql-kysely.ts +31 -3
- package/template/vendor/kysely-dsql/generator/migration-file.ts +3 -3
- package/template/vendor/kysely-dsql/plugins/index.ts +0 -2
- package/template/vendor/package-app/package-app.mjs +2 -2
- package/template/vite.config.ts +3 -1
- package/template/vitest.config.ts +1 -1
- package/template/.agents/skills/monolayer-application-permissions/agents/openai.yaml +0 -4
- package/template/vendor/kysely-dsql/plugins/soft-delete.plugin.ts +0 -217
- /package/template/.agents/skills/{monolayer-application-permissions → application-permissions}/references/development-and-verification.md +0 -0
- /package/template/.agents/skills/{monolayer-application-permissions → application-permissions}/references/interface-examples.md +0 -0
- /package/template/.agents/skills/{monolayer-application-permissions → application-permissions}/references/server-examples.md +0 -0
- /package/template/lib/{db/user-context.server.ts → user-context.server.ts} +0 -0
- /package/template/vendor/file-bucket/{errors.ts → lib/errors.server.ts} +0 -0
- /package/template/vendor/file-bucket/{multipart.ts → lib/multipart.ts} +0 -0
- /package/template/vendor/file-bucket/{protocol.ts → lib/protocol.ts} +0 -0
- /package/template/vendor/file-bucket/{transport.ts → lib/transport.client.ts} +0 -0
- /package/template/vendor/file-bucket/{types.ts → lib/types.ts} +0 -0
- /package/template/vendor/file-bucket/{transfers.ts → lib/upload-signed-url.ts} +0 -0
- /package/template/vendor/file-bucket/{xml.ts → lib/xml.ts} +0 -0
package/template/package.json
CHANGED
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
"package-app": "node vendor/package-app/package-app.mjs"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@aws-sdk/client-s3": "3.1152.0",
|
|
23
24
|
"@aws-sdk/dsql-signer": "^3.1126.0",
|
|
25
|
+
"@aws-sdk/lib-storage": "^3.1152.0",
|
|
26
|
+
"@aws-sdk/s3-request-presigner": "3.1152.0",
|
|
24
27
|
"@aws/aurora-dsql-node-postgres-connector": "^0.1.9",
|
|
25
28
|
"@kysera/infra": "^0.10.0",
|
|
26
29
|
"@react-router/node": "^8",
|
|
@@ -31,7 +34,8 @@
|
|
|
31
34
|
"pg": "^8.23.0",
|
|
32
35
|
"react": "^19.2.7",
|
|
33
36
|
"react-dom": "^19.2.7",
|
|
34
|
-
"react-router": "^8"
|
|
37
|
+
"react-router": "^8",
|
|
38
|
+
"zod": "^4.6.7"
|
|
35
39
|
},
|
|
36
40
|
"devDependencies": {
|
|
37
41
|
"@electric-sql/pglite": "^0.5.8",
|
|
@@ -42,8 +46,8 @@
|
|
|
42
46
|
"@types/react": "^19.2.14",
|
|
43
47
|
"@types/react-dom": "^19.2.3",
|
|
44
48
|
"kysely-ctl": "^0.21.0",
|
|
45
|
-
"oxfmt": "^0.
|
|
46
|
-
"oxlint": "^1.
|
|
49
|
+
"oxfmt": "^0.73.0",
|
|
50
|
+
"oxlint": "^1.88.0",
|
|
47
51
|
"oxlint-plugin-react-doctor": "^0.9.12",
|
|
48
52
|
"tailwindcss": "^4.2.2",
|
|
49
53
|
"tsx": "^4.23.13",
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import type { FileBucket } from "./
|
|
2
|
-
import { createMultipartUploadClient } from "./multipart.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
1
|
+
import type { FileBucket } from "./lib/file-bucket.js";
|
|
2
|
+
import { createMultipartUploadClient } from "./lib/multipart.js";
|
|
3
|
+
import { request } from "./lib/transport.client.js";
|
|
4
|
+
import type { GetUrlOptions, PutOptions, PutUrlOptions } from "./lib/types.js";
|
|
5
|
+
import { uploadSignedUrl } from "./lib/upload-signed-url.js";
|
|
6
6
|
|
|
7
|
-
export {
|
|
8
|
-
export
|
|
7
|
+
export { FileBucket } from "./lib/file-bucket.js";
|
|
8
|
+
export { createMultipartUploadClient } from "./lib/multipart.js";
|
|
9
|
+
export type { UploadGrant } from "./lib/protocol.js";
|
|
9
10
|
export type {
|
|
10
11
|
FileMultipartUploadDescriptor,
|
|
11
12
|
FileMultipartUploadClient,
|
|
@@ -14,7 +15,8 @@ export type {
|
|
|
14
15
|
GetUrlOptions,
|
|
15
16
|
PutOptions,
|
|
16
17
|
PutUrlOptions,
|
|
17
|
-
} from "./types.js";
|
|
18
|
+
} from "./lib/types.js";
|
|
19
|
+
|
|
18
20
|
export type BrowserUploadBody =
|
|
19
21
|
| Blob
|
|
20
22
|
| string
|
|
@@ -32,7 +34,7 @@ export type BrowserUploadBody =
|
|
|
32
34
|
* @param options - Optional settings including an `AbortSignal`.
|
|
33
35
|
* @returns A promise that resolves when the deletion is completed by the server.
|
|
34
36
|
*/
|
|
35
|
-
function deleteFile(
|
|
37
|
+
export function deleteFile(
|
|
36
38
|
bucket: FileBucket,
|
|
37
39
|
path: string,
|
|
38
40
|
options: { signal?: AbortSignal } = {}
|
|
@@ -16,9 +16,11 @@ import {
|
|
|
16
16
|
import { Upload } from "@aws-sdk/lib-storage";
|
|
17
17
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
18
18
|
|
|
19
|
-
import type { FileBucket } from "./
|
|
20
|
-
import {
|
|
21
|
-
|
|
19
|
+
import type { FileBucket } from "./lib/file-bucket.js";
|
|
20
|
+
import {
|
|
21
|
+
createMultipartUploadClient,
|
|
22
|
+
multipartPartSize,
|
|
23
|
+
} from "./lib/multipart.js";
|
|
22
24
|
import type {
|
|
23
25
|
FileUploadBody,
|
|
24
26
|
MultipartUploadOptions,
|
|
@@ -31,10 +33,11 @@ import type {
|
|
|
31
33
|
FileInfo,
|
|
32
34
|
FileDownloadClient,
|
|
33
35
|
FileUploadClient,
|
|
34
|
-
} from "./types.js";
|
|
36
|
+
} from "./lib/types.js";
|
|
37
|
+
import { uploadSignedUrl } from "./lib/upload-signed-url.js";
|
|
35
38
|
|
|
36
39
|
// Re-export public types
|
|
37
|
-
export { FileBucketErrors } from "./errors.js";
|
|
40
|
+
export { FileBucketErrors } from "./lib/errors.server.js";
|
|
38
41
|
export type {
|
|
39
42
|
FileUploadBody,
|
|
40
43
|
MultipartUploadOptions,
|
|
@@ -52,7 +55,9 @@ export type {
|
|
|
52
55
|
FileUploadClient,
|
|
53
56
|
FileDownloadDescriptor,
|
|
54
57
|
FileUploadDescriptor,
|
|
55
|
-
} from "./types.js";
|
|
58
|
+
} from "./lib/types.js";
|
|
59
|
+
|
|
60
|
+
export { FileBucket } from "./lib/file-bucket.js";
|
|
56
61
|
|
|
57
62
|
function uploadBodySize(body: FileUploadBody): number | undefined {
|
|
58
63
|
// The public body union includes strings, byte arrays, blobs, and streams.
|
|
@@ -159,7 +164,7 @@ export async function put(
|
|
|
159
164
|
});
|
|
160
165
|
options?.signal?.addEventListener("abort", abort, { once: true });
|
|
161
166
|
if (options?.onProgress) {
|
|
162
|
-
upload.on("httpUploadProgress", (progress) => {
|
|
167
|
+
upload.on("httpUploadProgress", (progress: any) => {
|
|
163
168
|
options.onProgress?.({
|
|
164
169
|
uploadedBytes: progress.loaded ?? 0,
|
|
165
170
|
totalBytes: progress.total,
|
|
@@ -270,7 +275,7 @@ export async function deleteBatch(
|
|
|
270
275
|
if (result.Errors?.length) {
|
|
271
276
|
throw new AggregateError(
|
|
272
277
|
result.Errors.map(
|
|
273
|
-
(error) =>
|
|
278
|
+
(error: any) =>
|
|
274
279
|
new Error(
|
|
275
280
|
`Failed to delete ${error.Key}: ${error.Code} ${error.Message ?? ""}`
|
|
276
281
|
)
|
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
/* oxlint-disable no-await-in-loop, anti-slop/no-unknown-returns, anti-slop/no-runtime-typeof */
|
|
2
|
-
import { FileBucket } from "./index.js";
|
|
3
|
-
import { parseFileBucketRequest } from "./protocol.js";
|
|
4
|
-
import type {
|
|
5
|
-
FileBucketRequest,
|
|
6
|
-
FileBucketResponses,
|
|
7
|
-
UploadGrant,
|
|
8
|
-
} from "./protocol.js";
|
|
9
1
|
import {
|
|
10
2
|
getUrl,
|
|
11
3
|
putUrl,
|
|
12
4
|
delete as deleteFile,
|
|
13
5
|
deleteBatch,
|
|
14
6
|
createMultipartUploadHandle,
|
|
15
|
-
} from "./server.js";
|
|
7
|
+
} from "./index.server.js";
|
|
8
|
+
/* oxlint-disable no-await-in-loop, anti-slop/no-unknown-returns, anti-slop/no-runtime-typeof */
|
|
9
|
+
import { FileBucket } from "./lib/file-bucket.js";
|
|
10
|
+
import { parseFileBucketRequest } from "./lib/protocol.js";
|
|
11
|
+
import type {
|
|
12
|
+
FileBucketRequest,
|
|
13
|
+
FileBucketResponses,
|
|
14
|
+
UploadGrant,
|
|
15
|
+
} from "./lib/protocol.js";
|
|
16
|
+
|
|
17
|
+
export { parseFileBucketRequest };
|
|
16
18
|
|
|
17
19
|
const MULTIPART_THRESHOLD = 8 * 1024 * 1024;
|
|
18
20
|
export const MAX_REQUEST_BYTES = 64 * 1024;
|
|
@@ -24,11 +26,11 @@ export type {
|
|
|
24
26
|
UploadGrant,
|
|
25
27
|
SignedUrl,
|
|
26
28
|
FileBucketResponses,
|
|
27
|
-
} from "./protocol.js";
|
|
29
|
+
} from "./lib/protocol.js";
|
|
28
30
|
|
|
29
31
|
export async function readLimitedJson(
|
|
30
32
|
request: Request,
|
|
31
|
-
maxBytes: number
|
|
33
|
+
maxBytes: number
|
|
32
34
|
): Promise<unknown> {
|
|
33
35
|
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) {
|
|
34
36
|
throw new RangeError("maxBytes must be a positive safe integer.");
|
|
@@ -72,7 +74,7 @@ export async function readLimitedJson(
|
|
|
72
74
|
|
|
73
75
|
async function uploadGrant(
|
|
74
76
|
bucket: FileBucket,
|
|
75
|
-
input: Extract<FileBucketRequest, { operation: "put" }
|
|
77
|
+
input: Extract<FileBucketRequest, { operation: "put" }>
|
|
76
78
|
): Promise<UploadGrant> {
|
|
77
79
|
const options = {
|
|
78
80
|
contentType: input.contentType,
|
|
@@ -95,7 +97,7 @@ async function uploadGrant(
|
|
|
95
97
|
|
|
96
98
|
export async function dispatch(
|
|
97
99
|
bucket: FileBucket,
|
|
98
|
-
input: FileBucketRequest
|
|
100
|
+
input: FileBucketRequest
|
|
99
101
|
): Promise<FileBucketResponses[FileBucketRequest["operation"]]> {
|
|
100
102
|
switch (input.operation) {
|
|
101
103
|
case "delete": {
|
|
@@ -8,8 +8,14 @@
|
|
|
8
8
|
* ```
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import
|
|
11
|
+
import assert from "node:assert";
|
|
12
12
|
|
|
13
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
14
|
+
import { Kysely, PGliteDialect, type KyselyConfig } from "kysely";
|
|
15
|
+
|
|
16
|
+
import plugins from "../plugins";
|
|
17
|
+
import { DSQLDialect } from "./dialect";
|
|
18
|
+
import { createDSQLPool } from "./pool";
|
|
13
19
|
import { DSQLSchemaModule } from "./schema-module";
|
|
14
20
|
|
|
15
21
|
// eslint-disable-next-line max-classes-per-file
|
|
@@ -72,8 +78,15 @@ export class DSQLKysely<DB> extends Kysely<DB> {
|
|
|
72
78
|
*/
|
|
73
79
|
// The explicit constructor documents the public configuration contract.
|
|
74
80
|
// eslint-disable-next-line no-useless-constructor
|
|
75
|
-
constructor(config
|
|
76
|
-
|
|
81
|
+
constructor(config?: KyselyConfig) {
|
|
82
|
+
if (config) {
|
|
83
|
+
super(config);
|
|
84
|
+
} else {
|
|
85
|
+
super({
|
|
86
|
+
dialect: dialect(),
|
|
87
|
+
plugins,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
77
90
|
}
|
|
78
91
|
|
|
79
92
|
/**
|
|
@@ -91,3 +104,18 @@ export class DSQLKysely<DB> extends Kysely<DB> {
|
|
|
91
104
|
return new DSQLSchemaModule(this.getExecutor());
|
|
92
105
|
}
|
|
93
106
|
}
|
|
107
|
+
|
|
108
|
+
function dialect() {
|
|
109
|
+
if (process.env.NODE_ENV === "production") {
|
|
110
|
+
const endpoint = process.env.CLUSTER_ENDPOINT;
|
|
111
|
+
assert.ok(endpoint);
|
|
112
|
+
return new DSQLDialect({
|
|
113
|
+
pool: createDSQLPool(endpoint),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return new PGliteDialect({
|
|
117
|
+
pglite: new PGlite(
|
|
118
|
+
`./.pglite_${process.env.NODE_ENV === "test" ? "test" : "development"}`
|
|
119
|
+
),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
@@ -51,11 +51,11 @@ export interface CreateMigrationFileOptions {
|
|
|
51
51
|
* ```
|
|
52
52
|
*/
|
|
53
53
|
export async function createMigrationFile(
|
|
54
|
-
options: CreateMigrationFileOptions
|
|
54
|
+
options: CreateMigrationFileOptions
|
|
55
55
|
): Promise<string> {
|
|
56
56
|
if (!MIGRATION_NAME_PATTERN.test(options.name)) {
|
|
57
57
|
throw new Error(
|
|
58
|
-
"Migration name must use lowercase letters, digits, and single hyphens."
|
|
58
|
+
"Migration name must use lowercase letters, digits, and single hyphens."
|
|
59
59
|
);
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -63,7 +63,7 @@ export async function createMigrationFile(
|
|
|
63
63
|
const migrationDirectory = path.resolve(options.migrationDirectory);
|
|
64
64
|
const filePath = path.join(
|
|
65
65
|
migrationDirectory,
|
|
66
|
-
`${timestamp}_${options.name}.ts
|
|
66
|
+
`${timestamp}_${options.name}.ts`
|
|
67
67
|
);
|
|
68
68
|
|
|
69
69
|
await mkdir(migrationDirectory, { recursive: true });
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { CamelCasePlugin } from "kysely";
|
|
2
2
|
|
|
3
3
|
import { AutoUpdatedAtPlugin } from "./auto-updated-at.plugin";
|
|
4
|
-
import { SoftDelete } from "./soft-delete.plugin";
|
|
5
4
|
|
|
6
5
|
export default [
|
|
7
6
|
new CamelCasePlugin(),
|
|
8
|
-
new SoftDelete({ ignoredTables: [] }),
|
|
9
7
|
new AutoUpdatedAtPlugin({ ignoredTables: [] }),
|
|
10
8
|
];
|
|
@@ -19,12 +19,12 @@ function getPackageName(cwd) {
|
|
|
19
19
|
if (existsSync(packageJsonPath)) {
|
|
20
20
|
try {
|
|
21
21
|
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
22
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof
|
|
22
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- Use the package name only when it is a nonempty string.
|
|
23
23
|
if (typeof pkg.name === "string" && pkg.name.trim()) {
|
|
24
24
|
return pkg.name.trim().replace(/^@/u, "").replaceAll("/", "-");
|
|
25
25
|
}
|
|
26
26
|
} catch {
|
|
27
|
-
|
|
27
|
+
return "app";
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
return "app";
|
package/template/vite.config.ts
CHANGED
|
@@ -2,8 +2,10 @@ import { reactRouter } from "@react-router/dev/vite";
|
|
|
2
2
|
import tailwindcss from "@tailwindcss/vite";
|
|
3
3
|
import { defineConfig } from "vite";
|
|
4
4
|
|
|
5
|
+
import { s3Emulator } from "./vendor/file-bucket/vite.js";
|
|
6
|
+
|
|
5
7
|
export default defineConfig({
|
|
6
|
-
plugins: [tailwindcss(), reactRouter()],
|
|
8
|
+
plugins: [tailwindcss(), reactRouter(), s3Emulator()],
|
|
7
9
|
resolve: {
|
|
8
10
|
tsconfigPaths: true,
|
|
9
11
|
},
|
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
// oxlint-disable max-classes-per-file
|
|
2
|
-
// source: https://github.com/LouisJULIEN/kysely-plugins/blob/main/src/soft-delete/soft-delete.plugin.ts
|
|
3
|
-
import type {
|
|
4
|
-
KyselyPlugin,
|
|
5
|
-
OperationNode,
|
|
6
|
-
PluginTransformQueryArgs,
|
|
7
|
-
PluginTransformResultArgs,
|
|
8
|
-
QueryResult,
|
|
9
|
-
RootOperationNode,
|
|
10
|
-
SelectQueryNode,
|
|
11
|
-
UnknownRow,
|
|
12
|
-
UpdateQueryNode,
|
|
13
|
-
} from "kysely";
|
|
14
|
-
import {
|
|
15
|
-
AliasNode,
|
|
16
|
-
BinaryOperationNode,
|
|
17
|
-
ColumnNode,
|
|
18
|
-
IdentifierNode,
|
|
19
|
-
JoinNode,
|
|
20
|
-
OperationNodeTransformer,
|
|
21
|
-
OperatorNode,
|
|
22
|
-
RawNode,
|
|
23
|
-
ReferenceNode,
|
|
24
|
-
TableNode,
|
|
25
|
-
WhereNode,
|
|
26
|
-
} from "kysely";
|
|
27
|
-
|
|
28
|
-
// credits: https://github.com/kysely-org/kysely/issues/803
|
|
29
|
-
|
|
30
|
-
export class SoftDeleteError extends Error {
|
|
31
|
-
readonly node?: OperationNode | undefined;
|
|
32
|
-
|
|
33
|
-
constructor(
|
|
34
|
-
message: string,
|
|
35
|
-
node?: OperationNode | undefined
|
|
36
|
-
// oxlint-disable-next-line unicorn/custom-error-definition
|
|
37
|
-
) {
|
|
38
|
-
super(message);
|
|
39
|
-
this.node = node;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
class SoftDeleteQueryTransformer extends OperationNodeTransformer {
|
|
43
|
-
private deletedAtColumnName: string;
|
|
44
|
-
private ignoredTables: string[];
|
|
45
|
-
|
|
46
|
-
constructor({
|
|
47
|
-
deletedAtColumnName,
|
|
48
|
-
ignoredTables,
|
|
49
|
-
}: {
|
|
50
|
-
deletedAtColumnName?: string;
|
|
51
|
-
ignoredTables?: string[];
|
|
52
|
-
} = {}) {
|
|
53
|
-
super();
|
|
54
|
-
this.deletedAtColumnName = deletedAtColumnName || "deleted_at";
|
|
55
|
-
this.ignoredTables = ignoredTables || [];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
protected override transformSelectQuery(
|
|
59
|
-
originalNode: SelectQueryNode
|
|
60
|
-
): SelectQueryNode {
|
|
61
|
-
const node = super.transformSelectQuery(originalNode);
|
|
62
|
-
return this.addDeletedAtCondition(node);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
protected override transformUpdateQuery(
|
|
66
|
-
originalNode: UpdateQueryNode
|
|
67
|
-
): UpdateQueryNode {
|
|
68
|
-
const node = super.transformUpdateQuery(originalNode);
|
|
69
|
-
return this.addDeletedAtCondition(node);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private appendFromFilters(
|
|
73
|
-
where: WhereNode | undefined,
|
|
74
|
-
node: SelectQueryNode | UpdateQueryNode
|
|
75
|
-
): WhereNode | undefined {
|
|
76
|
-
let currentWhere = where;
|
|
77
|
-
if (node.from) {
|
|
78
|
-
for (const from of node.from.froms) {
|
|
79
|
-
const deletedAtReference = this.getDeletedAtReference(from);
|
|
80
|
-
if (deletedAtReference) {
|
|
81
|
-
currentWhere = this.addIsNotDeletedFilter(
|
|
82
|
-
currentWhere,
|
|
83
|
-
deletedAtReference
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return currentWhere;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
private appendJoinFilters(
|
|
92
|
-
where: WhereNode | undefined,
|
|
93
|
-
node: SelectQueryNode | UpdateQueryNode
|
|
94
|
-
): WhereNode | undefined {
|
|
95
|
-
let currentWhere = where;
|
|
96
|
-
if (node.joins) {
|
|
97
|
-
for (const join of node.joins) {
|
|
98
|
-
const deletedAtReference = this.getDeletedAtReference(join);
|
|
99
|
-
if (deletedAtReference) {
|
|
100
|
-
currentWhere = this.addIsNotDeletedFilter(
|
|
101
|
-
currentWhere,
|
|
102
|
-
deletedAtReference
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return currentWhere;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
private addDeletedAtCondition<T extends SelectQueryNode | UpdateQueryNode>(
|
|
111
|
-
node: T
|
|
112
|
-
): T {
|
|
113
|
-
let where = this.appendFromFilters(node.where, node);
|
|
114
|
-
where = this.appendJoinFilters(where, node);
|
|
115
|
-
return {
|
|
116
|
-
...node,
|
|
117
|
-
where,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private getJoinReference(node: JoinNode): ReferenceNode | null {
|
|
122
|
-
if (node.table === null) {
|
|
123
|
-
throw new SoftDeleteError("Join without a table");
|
|
124
|
-
}
|
|
125
|
-
return this.getDeletedAtReference(node.table);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
private getAliasReference(
|
|
129
|
-
node: AliasNode,
|
|
130
|
-
deletedAtColumn: ColumnNode
|
|
131
|
-
): ReferenceNode | null {
|
|
132
|
-
if (!IdentifierNode.is(node.alias)) {
|
|
133
|
-
throw new SoftDeleteError("Alias without identifier", node.alias);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const tableAlias = node.alias.name;
|
|
137
|
-
if (!this.tableShouldHaveDeletedAt(tableAlias)) {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
return ReferenceNode.create(deletedAtColumn, TableNode.create(tableAlias));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
private getTableReference(
|
|
144
|
-
node: TableNode,
|
|
145
|
-
deletedAtColumn: ColumnNode
|
|
146
|
-
): ReferenceNode | null {
|
|
147
|
-
const tableName = node.table.identifier.name;
|
|
148
|
-
if (!this.tableShouldHaveDeletedAt(tableName)) {
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
return ReferenceNode.create(deletedAtColumn, node);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
private getDeletedAtReference(node: OperationNode): ReferenceNode | null {
|
|
155
|
-
if (JoinNode.is(node)) {
|
|
156
|
-
return this.getJoinReference(node);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const deletedAtColumn = ColumnNode.create(this.deletedAtColumnName);
|
|
160
|
-
if (TableNode.is(node)) {
|
|
161
|
-
return this.getTableReference(node, deletedAtColumn);
|
|
162
|
-
}
|
|
163
|
-
if (AliasNode.is(node)) {
|
|
164
|
-
return this.getAliasReference(node, deletedAtColumn);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
throw new SoftDeleteError(
|
|
168
|
-
`Getting a reference to ${this.deletedAtColumnName} column for ${node.kind} is not supported yet.`,
|
|
169
|
-
node
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// oxlint-disable-next-line class-methods-use-this
|
|
174
|
-
private addIsNotDeletedFilter(
|
|
175
|
-
node: WhereNode | undefined,
|
|
176
|
-
deletedAtReference: ReferenceNode
|
|
177
|
-
): WhereNode {
|
|
178
|
-
const filterOperation = BinaryOperationNode.create(
|
|
179
|
-
deletedAtReference,
|
|
180
|
-
OperatorNode.create("is"),
|
|
181
|
-
RawNode.createWithSql("null")
|
|
182
|
-
);
|
|
183
|
-
if (node === null || node === undefined) {
|
|
184
|
-
return WhereNode.create(filterOperation);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return WhereNode.cloneWithOperation(node, "And", filterOperation);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
private tableShouldHaveDeletedAt(tableName: string): boolean {
|
|
191
|
-
return (
|
|
192
|
-
!tableName.startsWith("kysely_") &&
|
|
193
|
-
!this.ignoredTables.includes(tableName)
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export class SoftDelete implements KyselyPlugin {
|
|
199
|
-
private transformer: SoftDeleteQueryTransformer;
|
|
200
|
-
|
|
201
|
-
constructor(
|
|
202
|
-
...args: ConstructorParameters<typeof SoftDeleteQueryTransformer>
|
|
203
|
-
) {
|
|
204
|
-
this.transformer = new SoftDeleteQueryTransformer(...args);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
transformQuery({ node }: PluginTransformQueryArgs): RootOperationNode {
|
|
208
|
-
return this.transformer.transformNode(node);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// oxlint-disable-next-line class-methods-use-this
|
|
212
|
-
transformResult({
|
|
213
|
-
result,
|
|
214
|
-
}: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>> {
|
|
215
|
-
return Promise.resolve(result);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|