create-monolayer-app 0.5.1 → 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.
Files changed (85) hide show
  1. package/bin/index.js +91 -59
  2. package/package.json +19 -16
  3. package/template/.agents/skills/{monolayer-application-permissions → application-permissions}/SKILL.md +2 -2
  4. package/template/.agents/skills/application-permissions/agents/openai.yaml +4 -0
  5. package/template/.agents/skills/kysely/SKILL.md +48 -0
  6. package/template/.agents/skills/kysely/agents/openai.yaml +4 -0
  7. package/template/.agents/skills/kysely/references/execution-plugins-and-runtime.md +67 -0
  8. package/template/.agents/skills/kysely/references/expressions-and-helpers.md +102 -0
  9. package/template/.agents/skills/kysely/references/migrations.md +43 -0
  10. package/template/.agents/skills/kysely/references/official-docs-map.md +97 -0
  11. package/template/.agents/skills/kysely/references/query-building.md +119 -0
  12. package/template/.agents/skills/kysely/references/transactions.md +47 -0
  13. package/template/.agents/skills/kysely/references/troubleshooting-and-boundaries.md +67 -0
  14. package/template/.agents/skills/kysely/references/types.md +58 -0
  15. package/template/.agents/skills/persistent-app-data/SKILL.md +74 -0
  16. package/template/.agents/skills/persistent-app-data/agents/openai.yaml +4 -0
  17. package/template/.agents/skills/persistent-app-data/references/generated-types.md +23 -0
  18. package/template/.agents/skills/persistent-app-data/references/migrations.md +68 -0
  19. package/template/.agents/skills/vitest/GENERATION.md +25 -0
  20. package/template/.agents/skills/vitest/SKILL.md +59 -0
  21. package/template/.agents/skills/vitest/references/advanced-environments.md +26 -0
  22. package/template/.agents/skills/vitest/references/advanced-projects.md +31 -0
  23. package/template/.agents/skills/vitest/references/advanced-type-testing.md +30 -0
  24. package/template/.agents/skills/vitest/references/advanced-vi.md +23 -0
  25. package/template/.agents/skills/vitest/references/core-cli.md +23 -0
  26. package/template/.agents/skills/vitest/references/core-config.md +24 -0
  27. package/template/.agents/skills/vitest/references/core-describe.md +24 -0
  28. package/template/.agents/skills/vitest/references/core-expect.md +35 -0
  29. package/template/.agents/skills/vitest/references/core-hooks.md +26 -0
  30. package/template/.agents/skills/vitest/references/core-test-api.md +29 -0
  31. package/template/.agents/skills/vitest/references/features-benchmarking.md +25 -0
  32. package/template/.agents/skills/vitest/references/features-concurrency.md +20 -0
  33. package/template/.agents/skills/vitest/references/features-context.md +27 -0
  34. package/template/.agents/skills/vitest/references/features-coverage.md +17 -0
  35. package/template/.agents/skills/vitest/references/features-filtering.md +24 -0
  36. package/template/.agents/skills/vitest/references/features-mocking.md +32 -0
  37. package/template/.agents/skills/vitest/references/features-reporters.md +22 -0
  38. package/template/.agents/skills/vitest/references/features-snapshots.md +23 -0
  39. package/template/.agents/skills/vitest/references/features-test-tags.md +30 -0
  40. package/template/.agents/skills/vitest/references/version-compatibility.md +19 -0
  41. package/template/.claude/CLAUDE.md +2 -2
  42. package/template/.react-router/types/+routes.ts +41 -0
  43. package/template/.react-router/types/+server-build.d.ts +18 -0
  44. package/template/.react-router/types/app/+types/root.ts +68 -0
  45. package/template/.react-router/types/app/routes/+types/files.ts +71 -0
  46. package/template/.react-router/types/app/routes/+types/home.ts +71 -0
  47. package/template/AGENTS.md +6 -2
  48. package/template/app/routes/files.ts +3 -3
  49. package/template/build/client/assets/entry.client-Bc58m2-m.js +9 -0
  50. package/template/build/client/assets/errorBoundaries-CQxPIuVU.js +16 -0
  51. package/template/build/client/assets/files-BvRk9kiK.js +0 -0
  52. package/template/build/client/assets/home-CH2INc5e.js +1 -0
  53. package/template/build/client/assets/jsx-runtime-eLCqVKHJ.js +1 -0
  54. package/template/build/client/assets/logo-dark-pX2395Y0.svg +23 -0
  55. package/template/build/client/assets/logo-light-CVbx2LBR.svg +23 -0
  56. package/template/build/client/assets/manifest-2d65dd0f.js +1 -0
  57. package/template/build/client/assets/root-BeNS6njP.css +2 -0
  58. package/template/build/client/assets/root-C1tmjS9o.js +1 -0
  59. package/template/build/client/favicon.ico +0 -0
  60. package/template/build/server/index.js +1096 -0
  61. package/template/lib/db/client.ts +1 -27
  62. package/template/package-lock.json +8221 -0
  63. package/template/package.json +7 -3
  64. package/template/vendor/file-bucket/{client.ts → index.client.ts} +11 -9
  65. package/template/vendor/file-bucket/{server.ts → index.server.ts} +13 -8
  66. package/template/vendor/file-bucket/{index.ts → lib/file-bucket.ts} +2 -1
  67. package/template/vendor/file-bucket/{react-router.ts → react-router.server.ts} +15 -13
  68. package/template/vendor/kysely-dsql/dsql/dsql-kysely.ts +31 -3
  69. package/template/vendor/kysely-dsql/generator/migration-file.ts +3 -3
  70. package/template/vendor/kysely-dsql/plugins/index.ts +0 -2
  71. package/template/vendor/package-app/package-app.mjs +2 -2
  72. package/template/vite.config.ts +3 -1
  73. package/template/vitest.config.ts +1 -1
  74. package/template/.agents/skills/monolayer-application-permissions/agents/openai.yaml +0 -4
  75. package/template/vendor/kysely-dsql/plugins/soft-delete.plugin.ts +0 -217
  76. /package/template/.agents/skills/{monolayer-application-permissions → application-permissions}/references/development-and-verification.md +0 -0
  77. /package/template/.agents/skills/{monolayer-application-permissions → application-permissions}/references/interface-examples.md +0 -0
  78. /package/template/.agents/skills/{monolayer-application-permissions → application-permissions}/references/server-examples.md +0 -0
  79. /package/template/vendor/file-bucket/{errors.ts → lib/errors.server.ts} +0 -0
  80. /package/template/vendor/file-bucket/{multipart.ts → lib/multipart.ts} +0 -0
  81. /package/template/vendor/file-bucket/{protocol.ts → lib/protocol.ts} +0 -0
  82. /package/template/vendor/file-bucket/{transport.ts → lib/transport.client.ts} +0 -0
  83. /package/template/vendor/file-bucket/{types.ts → lib/types.ts} +0 -0
  84. /package/template/vendor/file-bucket/{transfers.ts → lib/upload-signed-url.ts} +0 -0
  85. /package/template/vendor/file-bucket/{xml.ts → lib/xml.ts} +0 -0
@@ -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.65.0",
46
- "oxlint": "^1.80.0",
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 "./index.js";
2
- import { createMultipartUploadClient } from "./multipart.js";
3
- import { uploadSignedUrl } from "./transfers.js";
4
- import { request } from "./transport.js";
5
- import type { GetUrlOptions, PutOptions, PutUrlOptions } from "./types.js";
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 { createMultipartUploadClient } from "./multipart.js";
8
- export type { UploadGrant } from "./protocol.js";
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 "./index.js";
20
- import { createMultipartUploadClient, multipartPartSize } from "./multipart.js";
21
- import { uploadSignedUrl } from "./transfers.js";
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,6 +1,7 @@
1
- /** Shared file bucket definition. Safe to import on the server and in the browser. */
1
+ /** Shared file bucket definition for server and browser code. */
2
2
  export class FileBucket {
3
3
  readonly name: string;
4
+
4
5
  constructor(name: string) {
5
6
  this.name = name;
6
7
  }
@@ -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 { Kysely, type KyselyConfig } from "kysely";
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: KyselyConfig) {
76
- super(config);
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
- // Ignore parse errors and use fallback
27
+ return "app";
28
28
  }
29
29
  }
30
30
  return "app";
@@ -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
  },
@@ -6,7 +6,7 @@ export default defineConfig({
6
6
  resolve: {
7
7
  alias: {
8
8
  "virtual:db-migrations": fileURLToPath(
9
- new URL("test/fixtures/virtual-db-migrations.ts", import.meta.url),
9
+ new URL("test/fixtures/virtual-db-migrations.ts", import.meta.url)
10
10
  ),
11
11
  },
12
12
  },
@@ -1,4 +0,0 @@
1
- interface:
2
- display_name: "Monolayer Application Permissions"
3
- short_description: "Use shared application roles in React Router"
4
- default_prompt: "Use $monolayer-application-permissions to apply shared read and write permissions with the existing user middleware."
@@ -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
- }