├── .gitignore
├── LICENSE
├── README.md
├── audit-log-context
├── .env.example
├── README.md
├── docker-compose.yml
├── package.json
├── prisma
│ ├── migrations
│ │ ├── 20221208204953_init
│ │ │ └── migration.sql
│ │ ├── 20221208205006_audit_triggers
│ │ │ └── migration.sql
│ │ └── migration_lock.toml
│ ├── schema.prisma
│ └── seed.ts
├── script.ts
└── tsconfig.json
├── callback-free-itx
├── .env.example
├── README.md
├── docker-compose.yml
├── package.json
├── prisma
│ ├── migrations
│ │ ├── 20221212035639_init
│ │ │ └── migration.sql
│ │ └── migration_lock.toml
│ ├── schema.prisma
│ └── seed.ts
├── script.ts
└── tsconfig.json
├── computed-fields
├── README.md
├── package.json
├── prisma
│ ├── migrations
│ │ ├── 20221209170543_init
│ │ │ └── migration.sql
│ │ └── migration_lock.toml
│ ├── schema.prisma
│ └── seed.ts
├── script.ts
└── tsconfig.json
├── exists-fn
├── README.md
├── package.json
├── prisma
│ ├── schema.prisma
│ └── seed.ts
├── script.ts
└── tsconfig.json
├── input-transformation
├── README.md
├── package.json
├── prisma
│ ├── migrations
│ │ ├── 20221209213844_init
│ │ │ └── migration.sql
│ │ └── migration_lock.toml
│ ├── schema.prisma
│ └── seed.ts
├── script.ts
└── tsconfig.json
├── input-validation
├── README.md
├── package.json
├── prisma
│ ├── migrations
│ │ ├── 20221209220230_init
│ │ │ └── migration.sql
│ │ └── migration_lock.toml
│ └── schema.prisma
├── src
│ ├── index.ts
│ └── models
│ │ ├── product.ts
│ │ └── review.ts
└── tsconfig.json
├── instance-methods
├── README.md
├── package.json
├── prisma
│ ├── migrations
│ │ ├── 20221209221951_init
│ │ │ └── migration.sql
│ │ └── migration_lock.toml
│ └── schema.prisma
├── script.ts
└── tsconfig.json
├── json-field-types
├── .env.example
├── README.md
├── docker-compose.yml
├── package.json
├── prisma
│ ├── migrations
│ │ ├── 20221212001724_init
│ │ │ └── migration.sql
│ │ └── migration_lock.toml
│ ├── schema.prisma
│ └── seed.ts
├── src
│ ├── index.ts
│ └── schemas.ts
└── tsconfig.json
├── model-filters
├── README.md
├── package.json
├── prisma
│ ├── migrations
│ │ ├── 20221212025553_init
│ │ │ └── migration.sql
│ │ └── migration_lock.toml
│ ├── schema.prisma
│ └── seed.ts
├── script.ts
└── tsconfig.json
├── obfuscated-fields
├── README.md
├── package.json
├── prisma
│ ├── migrations
│ │ ├── 20221209223744_init
│ │ │ └── migration.sql
│ │ └── migration_lock.toml
│ ├── schema.prisma
│ └── seed.ts
├── script.ts
└── tsconfig.json
├── query-logging
├── README.md
├── package.json
├── prisma
│ ├── migrations
│ │ ├── 20221211185834_init
│ │ │ └── migration.sql
│ │ └── migration_lock.toml
│ ├── schema.prisma
│ └── seed.ts
├── script.ts
└── tsconfig.json
├── readonly-client
├── README.md
├── package.json
├── prisma
│ ├── migrations
│ │ ├── 20221209213844_init
│ │ │ └── migration.sql
│ │ └── migration_lock.toml
│ ├── schema.prisma
│ └── seed.ts
├── script.ts
└── tsconfig.json
├── renovate.json
├── retry-transactions
├── .env.example
├── README.md
├── docker-compose.yml
├── package.json
├── prisma
│ ├── migrations
│ │ ├── 20221211193956_init
│ │ │ └── migration.sql
│ │ └── migration_lock.toml
│ ├── schema.prisma
│ └── seed.ts
├── script.ts
└── tsconfig.json
├── row-level-security
├── .env.example
├── README.md
├── docker-compose.yml
├── docker
│ ├── Dockerfile
│ └── init-app-db.sh
├── package.json
├── prisma
│ ├── migrations
│ │ ├── 20221211203130_init
│ │ │ └── migration.sql
│ │ ├── 20221211203153_row_level_security
│ │ │ └── migration.sql
│ │ └── migration_lock.toml
│ ├── schema.prisma
│ └── seed.ts
├── script.ts
└── tsconfig.json
├── static-methods
├── README.md
├── package.json
├── prisma
│ ├── migrations
│ │ ├── 20221209172935_init
│ │ │ └── migration.sql
│ │ └── migration_lock.toml
│ └── schema.prisma
├── script.ts
└── tsconfig.json
├── transformed-fields
├── README.md
├── package.json
├── prisma
│ ├── migrations
│ │ ├── 20221211201016_init
│ │ │ └── migration.sql
│ │ └── migration_lock.toml
│ ├── schema.prisma
│ └── seed.ts
├── script.ts
└── tsconfig.json
└── update-delete-ignore-not-found
├── README.md
├── package.json
├── prisma
├── migrations
│ ├── 20221211185834_init
│ │ └── migration.sql
│ └── migration_lock.toml
├── schema.prisma
└── seed.ts
├── script.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .DS_Store
3 | .idea
4 | package-lock.json
5 | .vscode/
6 | dev.db
7 | dev.db-journal
8 | .env
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2022 Prisma Data, LLC
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
Prisma Client Extension Examples
3 |
4 |
5 | This repository contains a number of examples of [Prisma Client extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions).
6 |
7 | > The extensions in this repository are provided as examples only, and without warranty. They are not intended to be used in production environments.
8 |
9 | ## Examples
10 |
11 | | Example | Description |
12 | |---|---|
13 | | [`audit-log-context`](audit-log-context) | Provides the current user's ID as context to Postgres audit log triggers |
14 | | [`callback-free-itx`](callback-free-itx) | Adds a method to start interactive transactions without callbacks |
15 | | [`computed-fields`](computed-fields) | Adds virtual / computed fields to result objects |
16 | | [`input-transformation`](input-transformation) | Transforms the input arguments passed to Prisma Client queries to filter the result set |
17 | | [`input-validation`](input-validation) | Runs custom validation logic on input arguments passed to mutation methods |
18 | | [`instance-methods`](instance-methods) | Adds Active Record-like methods like `save()` and `delete()` to result objects |
19 | | [`json-field-types`](json-field-types) | Uses strongly-typed runtime parsing for data stored in JSON columns |
20 | | [`model-filters`](model-filters) | Adds reusable filters that can composed into complex `where` conditions for a model |
21 | | [`obfuscated-fields`](obfuscated-fields) | Prevents sensitive data (e.g. `password` fields) from being included in results |
22 | | [`query-logging`](query-logging) | Wraps Prisma Client queries with simple query timing and logging |
23 | | [`readonly-client`](readonly-client) | Creates a client that only allows read operations |
24 | | [`retry-transactions`](retry-transactions) | Adds a retry mechanism to transactions with exponential backoff and jitter |
25 | | [`row-level-security`](row-level-security) | Uses Postgres row-level security policies to isolate data in a multi-tenant application |
26 | | [`static-methods`](static-methods) | Adds custom query methods to Prisma Client models |
27 | | [`transformed-fields`](transformed-fields) | Demonstrates how to use result extensions to transform query results and add i18n to an app |
28 | | [`exists-fn`](exists-fn) | Adds an `exists` function to all your models |
29 | | [`update-delete-ignore-not-found`](./update-delete-ignore-not-found/) | Adds `updateIgnoreOnNotFound` and `deleteIgnoreOnNotFound` function to all your models |
30 |
31 |
32 | ## Authoring extensions
33 |
34 | If you're interested in building an extension you want to share as a package, we recommend using the following [starter repository](https://github.com/prisma/prisma-client-extension-starter).
35 |
36 | Refer to our [documentation](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/shared-extensions) to learn more about how to create and publish extensions.
37 |
--------------------------------------------------------------------------------
/audit-log-context/.env.example:
--------------------------------------------------------------------------------
1 | DATABASE_URL="postgresql://postgres:postgres@localhost:6004/postgres"
2 |
--------------------------------------------------------------------------------
/audit-log-context/README.md:
--------------------------------------------------------------------------------
1 | # Prisma Client Extension - Audit Log Context
2 |
3 | This example shows how to use a Prisma Client extension to provide the current application user's ID as context to an audit log trigger in Postgres. User IDs are included in an audit trail tracking every change to rows on certain tables.
4 |
5 | ## Caveats
6 |
7 | > **NOTE**: Because this example extension wraps every query in a new batch transaction, explicitly running transactions with `prisma.$transaction()` may not work as intended. In a future version of Prisma Client, `query` extensions will have access to information about whether they are run inside a transaction, similar to [the `runInTransaction` parameter provided to Prisma middleware](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#params). When this is available, this example will be updated to work for queries run inside explicit transactions.
8 |
9 | This extension is provided as an example only. It is not intended to be used in production environments.
10 |
11 | Please read [the documentation on `query` extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/query) for more information.
12 |
13 | ## Background
14 |
15 | Certain applications need to track changes to data over time and generate an audit trail to record which user made each change. One approach is to store this audit log in one or more tables in the same database and populate the audit log automatically with database triggers.
16 |
17 | In order to provide information about the current user and/or session, it is possible to set a runtime parameter in Postgres and reference it from a trigger.
18 |
19 | ## How it works
20 |
21 | There are a few steps required to set up an audit table in Postgres and then provide session context to it from Prisma:
22 |
23 | ### 1. Create one or more tables to store audit logs
24 |
25 | There is more than one way to structure audit log tables. For example, you might create a single audit log table which tracks changes to various other tables. In this case, you could include `text` columns that track the base table's schema and name, as well as a `json` column which contains the actual data for each modification.
26 |
27 | In this example, we create a separate "`Version`" table for each base table that needs an audit log. These tables contain columns that mirror all of the columns on the base table, as well as a few extra columns that contain metadata about each version:
28 |
29 | - `versionId`: The primary key of the version table — a unique value for each version of each row in the base table
30 | - `versionOperation`: Tracks whether the modification was a `CREATE`, `UPDATE`, or `DELETE` command
31 | - `versionId`: The ID of the row in the base table. This is a nullable foreign key to the base table for rows that still exist, but can be `NULL` for deleted rows. The deleted row IDs are tracked in the mirrored `id` column.
32 | - `versionUserId`: The ID of the user who made the modification, provided as context by the application
33 | - `versionTimestamp`: When the modification was made
34 |
35 | There are benefits and drawbacks to each way to model audit log tables, but the general approach for including session context is the same.
36 |
37 | > **NOTE**: This example also uses the [`multiSchema` preview feature](https://www.prisma.io/docs/guides/database/multi-schema) for Postgres in order to store audit tables in a separate `audit` schema.
38 |
39 | ### 2. Create audit log triggers
40 |
41 | Create [a custom migration file](https://www.prisma.io/docs/guides/database/developing-with-prisma-migrate/customizing-migrations) that includes SQL commands to create audit triggers for operations on the base tables. See [this migration file](prisma/migrations/20221208205006_audit_triggers/migration.sql), which includes a trigger for each table that needs to be audited:
42 |
43 | ```sql
44 | CREATE OR REPLACE FUNCTION "audit"."Product_audit"() RETURNS TRIGGER AS $$
45 | BEGIN
46 | IF (TG_OP = 'DELETE') THEN
47 | INSERT INTO "audit"."ProductVersion"
48 | VALUES (DEFAULT, 'DELETE', NULL, current_setting('app.current_user_id', TRUE)::int, now(), OLD.*);
49 | ELSIF (TG_OP = 'UPDATE') THEN
50 | INSERT INTO "audit"."ProductVersion"
51 | VALUES (DEFAULT, 'UPDATE', NEW."id", current_setting('app.current_user_id', TRUE)::int, now(), NEW.*);
52 | ELSIF (TG_OP = 'INSERT') THEN
53 | INSERT INTO "audit"."ProductVersion"
54 | VALUES (DEFAULT, 'INSERT', NEW."id", current_setting('app.current_user_id', TRUE)::int, now(), NEW.*);
55 | END IF;
56 | RETURN NULL;
57 | END;
58 | $$ LANGUAGE plpgsql;
59 |
60 | CREATE TRIGGER audit
61 | AFTER INSERT OR UPDATE OR DELETE ON "public"."Product"
62 | FOR EACH ROW EXECUTE FUNCTION "audit"."Product_audit"();
63 | ```
64 |
65 | These triggers create a row in a `Version` table for every row affected by an `INSERT`, `UPDATE`, or `DELETE` command on the base table. They set the `versionUserId` column to the result of calling `current_setting('app.current_user_id', TRUE)::int`, which gets a runtime parameter named `app.current_user_id` that we'll set via a Prisma Client extension.
66 |
67 | ### 3. Create a Prisma Client extension that wraps queries in a transaction and sets the audit log session context
68 |
69 | In order to set a runtime parameter in Postgres, you can execute a raw query with Prisma:
70 |
71 | ```typescript
72 | await prisma.$executeRaw`SELECT set_config('app.current_user_id', ${user}, TRUE)`;
73 | ```
74 |
75 | However, each time you run a query, Prisma may use a different connection from the connection pool. In order to associate the parameter with all of the queries in the context of a given request in your application, you should:
76 |
77 | 1. Start a transaction.
78 | 2. Set the runtime parameter as a `LOCAL` setting, which lasts only until the end of the current transaction. This may be done by passing `TRUE` to the third argument (`is_local`) of the `set_config()` function.
79 | 3. Run all queries for the duration of the request inside this transaction.
80 |
81 | All queries for a given transaction will use the same database connection, and because the setting is local, it won't affect any other transactions.
82 |
83 | Prisma Client extensions provide a way to easily ensure all queries run inside a transaction with a setting enabled for RLS. See [the `script.ts` file](script.ts) for an example extension, which allows you to create a client instance for the current user:
84 |
85 | ```typescript
86 | const userPrisma = prisma.forUser(user.id);
87 |
88 | await userPrisma.product.update({
89 | where: { id: product.id },
90 | data: { name: "Updated Name" },
91 | });
92 | ```
93 |
94 | ## How to use
95 |
96 | ### Prerequisites
97 |
98 | - Install [Node.js](https://nodejs.org/en/download/)
99 | - Install [Docker](https://docs.docker.com/get-docker/)
100 |
101 | ### 1. Download example & install dependencies
102 |
103 | Clone this repository:
104 |
105 | ```sh
106 | git clone git@github.com:sbking/prisma-client-extensions.git
107 | ```
108 |
109 | Create a `.env` file and install dependencies:
110 |
111 | ```sh
112 | cd audit-log-context
113 | cp .env.example .env
114 | npm install
115 | ```
116 |
117 | ### 2. Start the database
118 |
119 | Run the following command to start a new Postgres database in a Docker container:
120 |
121 | ```sh
122 | docker compose up -d
123 | ```
124 |
125 | ### 3. Run migrations
126 |
127 | Run this command to apply migrations to the database:
128 |
129 | ```sh
130 | npx prisma migrate deploy
131 | ```
132 |
133 | ### 4. Seed the database
134 |
135 | Run the following command to add seed data to the database:
136 |
137 | ```sh
138 | npx prisma db seed
139 | ```
140 |
141 | ### 5. Run the `dev` script
142 |
143 | To run the `script.ts` file, run the following command:
144 |
145 | ```sh
146 | npm run dev
147 | ```
148 |
--------------------------------------------------------------------------------
/audit-log-context/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.7"
2 | services:
3 | postgres:
4 | image: postgres:latest
5 | restart: always
6 | environment:
7 | - POSTGRES_USER=postgres
8 | - POSTGRES_PASSWORD=postgres
9 | - POSTGRES_DB=postgres
10 | ports:
11 | - "6004:5432"
12 | volumes:
13 | - postgres-data:/var/lib/postgresql/data
14 |
15 | volumes:
16 | postgres-data:
17 |
--------------------------------------------------------------------------------
/audit-log-context/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "audit-log-context",
3 | "private": true,
4 | "scripts": {
5 | "dev": "ts-node script.ts"
6 | },
7 | "dependencies": {
8 | "@prisma/client": "6.0.1"
9 | },
10 | "devDependencies": {
11 | "@faker-js/faker": "9.0.2",
12 | "@types/node": "22.8.2",
13 | "prisma": "6.0.1",
14 | "ts-node": "10.9.2",
15 | "typescript": "5.6.2"
16 | },
17 | "prisma": {
18 | "seed": "ts-node prisma/seed.ts"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/audit-log-context/prisma/migrations/20221208204953_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateSchema
2 | CREATE SCHEMA IF NOT EXISTS "audit";
3 |
4 | -- CreateEnum
5 | CREATE TYPE "audit"."AuditOperation" AS ENUM ('INSERT', 'UPDATE', 'DELETE');
6 |
7 | -- CreateTable
8 | CREATE TABLE "public"."User" (
9 | "id" SERIAL NOT NULL,
10 | "email" TEXT NOT NULL,
11 |
12 | CONSTRAINT "User_pkey" PRIMARY KEY ("id")
13 | );
14 |
15 | -- CreateTable
16 | CREATE TABLE "public"."Product" (
17 | "id" SERIAL NOT NULL,
18 | "name" TEXT NOT NULL,
19 |
20 | CONSTRAINT "Product_pkey" PRIMARY KEY ("id")
21 | );
22 |
23 | -- CreateTable
24 | CREATE TABLE "public"."Order" (
25 | "id" SERIAL NOT NULL,
26 | "userId" INTEGER NOT NULL,
27 | "productId" INTEGER NOT NULL,
28 | "quantity" INTEGER NOT NULL,
29 |
30 | CONSTRAINT "Order_pkey" PRIMARY KEY ("id")
31 | );
32 |
33 | -- CreateTable
34 | CREATE TABLE "audit"."ProductVersion" (
35 | "versionId" SERIAL NOT NULL,
36 | "versionOperation" "audit"."AuditOperation" NOT NULL,
37 | "versionProductId" INTEGER,
38 | "versionUserId" INTEGER,
39 | "versionTimestamp" TIMESTAMP(3) NOT NULL,
40 | "id" INTEGER NOT NULL,
41 | "name" TEXT NOT NULL,
42 |
43 | CONSTRAINT "ProductVersion_pkey" PRIMARY KEY ("versionId")
44 | );
45 |
46 | -- CreateTable
47 | CREATE TABLE "audit"."OrderVersion" (
48 | "versionId" SERIAL NOT NULL,
49 | "versionOperation" "audit"."AuditOperation" NOT NULL,
50 | "versionOrderId" INTEGER,
51 | "versionUserId" INTEGER,
52 | "versionTimestamp" TIMESTAMP(3) NOT NULL,
53 | "id" INTEGER NOT NULL,
54 | "userId" INTEGER NOT NULL,
55 | "productId" INTEGER NOT NULL,
56 | "quantity" INTEGER NOT NULL,
57 |
58 | CONSTRAINT "OrderVersion_pkey" PRIMARY KEY ("versionId")
59 | );
60 |
61 | -- AddForeignKey
62 | ALTER TABLE "public"."Order" ADD CONSTRAINT "Order_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
63 |
64 | -- AddForeignKey
65 | ALTER TABLE "public"."Order" ADD CONSTRAINT "Order_productId_fkey" FOREIGN KEY ("productId") REFERENCES "public"."Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
66 |
67 | -- AddForeignKey
68 | ALTER TABLE "audit"."ProductVersion" ADD CONSTRAINT "ProductVersion_versionProductId_fkey" FOREIGN KEY ("versionProductId") REFERENCES "public"."Product"("id") ON DELETE SET NULL ON UPDATE CASCADE;
69 |
70 | -- AddForeignKey
71 | ALTER TABLE "audit"."ProductVersion" ADD CONSTRAINT "ProductVersion_versionUserId_fkey" FOREIGN KEY ("versionUserId") REFERENCES "public"."User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
72 |
73 | -- AddForeignKey
74 | ALTER TABLE "audit"."OrderVersion" ADD CONSTRAINT "OrderVersion_versionOrderId_fkey" FOREIGN KEY ("versionOrderId") REFERENCES "public"."Order"("id") ON DELETE SET NULL ON UPDATE CASCADE;
75 |
76 | -- AddForeignKey
77 | ALTER TABLE "audit"."OrderVersion" ADD CONSTRAINT "OrderVersion_versionUserId_fkey" FOREIGN KEY ("versionUserId") REFERENCES "public"."User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
78 |
--------------------------------------------------------------------------------
/audit-log-context/prisma/migrations/20221208205006_audit_triggers/migration.sql:
--------------------------------------------------------------------------------
1 | -- Product audit trigger function
2 | CREATE OR REPLACE FUNCTION "audit"."Product_audit"() RETURNS TRIGGER AS $$
3 | BEGIN
4 | IF (TG_OP = 'DELETE') THEN
5 | INSERT INTO "audit"."ProductVersion"
6 | VALUES (DEFAULT, 'DELETE', NULL, current_setting('app.current_user_id', TRUE)::int, now(), OLD.*);
7 | ELSIF (TG_OP = 'UPDATE') THEN
8 | INSERT INTO "audit"."ProductVersion"
9 | VALUES (DEFAULT, 'UPDATE', NEW."id", current_setting('app.current_user_id', TRUE)::int, now(), NEW.*);
10 | ELSIF (TG_OP = 'INSERT') THEN
11 | INSERT INTO "audit"."ProductVersion"
12 | VALUES (DEFAULT, 'INSERT', NEW."id", current_setting('app.current_user_id', TRUE)::int, now(), NEW.*);
13 | END IF;
14 | RETURN NULL;
15 | END;
16 | $$ LANGUAGE plpgsql;
17 |
18 | -- Order audit trigger function
19 | CREATE OR REPLACE FUNCTION "audit"."Order_audit"() RETURNS TRIGGER AS $$
20 | BEGIN
21 | IF (TG_OP = 'DELETE') THEN
22 | INSERT INTO "audit"."OrderVersion"
23 | VALUES (DEFAULT, 'DELETE', NULL, current_setting('app.current_user_id', TRUE)::int, now(), OLD.*);
24 | ELSIF (TG_OP = 'UPDATE') THEN
25 | INSERT INTO "audit"."OrderVersion"
26 | VALUES (DEFAULT, 'UPDATE', NEW."id", current_setting('app.current_user_id', TRUE)::int, now(), NEW.*);
27 | ELSIF (TG_OP = 'INSERT') THEN
28 | INSERT INTO "audit"."OrderVersion"
29 | VALUES (DEFAULT, 'INSERT', NEW."id", current_setting('app.current_user_id', TRUE)::int, now(), NEW.*);
30 | END IF;
31 | RETURN NULL;
32 | END;
33 | $$ LANGUAGE plpgsql;
34 |
35 | -- Product trigger
36 | CREATE TRIGGER audit
37 | AFTER INSERT OR UPDATE OR DELETE ON "public"."Product"
38 | FOR EACH ROW EXECUTE FUNCTION "audit"."Product_audit"();
39 |
40 | -- Order trigger
41 | CREATE TRIGGER audit
42 | AFTER INSERT OR UPDATE OR DELETE ON "public"."Order"
43 | FOR EACH ROW EXECUTE FUNCTION "audit"."Order_audit"();
44 |
--------------------------------------------------------------------------------
/audit-log-context/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/audit-log-context/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | previewFeatures = ["multiSchema"]
4 | }
5 |
6 | datasource db {
7 | provider = "postgresql"
8 | url = env("DATABASE_URL")
9 | schemas = ["public", "audit"]
10 | }
11 |
12 | model User {
13 | id Int @id @default(autoincrement())
14 | email String
15 |
16 | orders Order[]
17 | productVersions ProductVersion[]
18 | orderVersions OrderVersion[]
19 |
20 | @@schema("public")
21 | }
22 |
23 | model Product {
24 | id Int @id @default(autoincrement())
25 | name String
26 |
27 | orders Order[]
28 | versions ProductVersion[]
29 |
30 | @@schema("public")
31 | }
32 |
33 | model Order {
34 | id Int @id @default(autoincrement())
35 | userId Int
36 | productId Int
37 | quantity Int
38 |
39 | user User @relation(fields: [userId], references: [id])
40 | product Product @relation(fields: [productId], references: [id])
41 | versions OrderVersion[]
42 |
43 | @@schema("public")
44 | }
45 |
46 | enum AuditOperation {
47 | INSERT
48 | UPDATE
49 | DELETE
50 |
51 | @@schema("audit")
52 | }
53 |
54 | model ProductVersion {
55 | // Version metadata fields
56 | versionId Int @id @default(autoincrement())
57 | versionOperation AuditOperation
58 | versionProductId Int?
59 | versionUserId Int?
60 | versionTimestamp DateTime
61 |
62 | // Mirrored fields from the Product table
63 | id Int
64 | name String
65 |
66 | product Product? @relation(fields: [versionProductId], references: [id])
67 | user User? @relation(fields: [versionUserId], references: [id])
68 |
69 | @@schema("audit")
70 | }
71 |
72 | model OrderVersion {
73 | // Version metadata fields
74 | versionId Int @id @default(autoincrement())
75 | versionOperation AuditOperation
76 | versionOrderId Int?
77 | versionUserId Int?
78 | versionTimestamp DateTime
79 |
80 | // Mirrored fields from the Order table
81 | id Int
82 | userId Int
83 | productId Int
84 | quantity Int
85 |
86 | order Order? @relation(fields: [versionOrderId], references: [id])
87 | user User? @relation(fields: [versionUserId], references: [id])
88 |
89 | @@schema("audit")
90 | }
91 |
--------------------------------------------------------------------------------
/audit-log-context/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | import { faker } from "@faker-js/faker";
2 | import { Prisma, PrismaClient } from "@prisma/client";
3 |
4 | const prisma = new PrismaClient();
5 |
6 | async function main() {
7 | // Create 100 random users
8 | const users = Array.from({ length: 100 }, (_, i) => ({
9 | id: i + 1,
10 | email: faker.helpers.unique(faker.internet.email),
11 | })) satisfies Prisma.UserCreateManyInput[];
12 |
13 | // Create 20 random products
14 | const products = Array.from({ length: 20 }, (_, i) => ({
15 | id: i + 1,
16 | name: faker.helpers.unique(faker.commerce.productName),
17 | })) satisfies Prisma.ProductCreateManyInput[];
18 |
19 | // Create 300 random orders
20 | const orders = Array.from({ length: 300 }, (_, i) => ({
21 | userId: faker.helpers.arrayElement(users).id,
22 | productId: faker.helpers.arrayElement(products).id,
23 | quantity: faker.datatype.number({ min: 1, max: 100 }),
24 | })) satisfies Prisma.OrderCreateManyInput[];
25 |
26 | await prisma.$transaction([
27 | // Cleanup existing data
28 | prisma.$executeRaw`TRUNCATE TABLE "audit"."OrderVersion" RESTART IDENTITY`,
29 | prisma.$executeRaw`TRUNCATE TABLE "audit"."ProductVersion" RESTART IDENTITY`,
30 | prisma.$executeRaw`TRUNCATE TABLE "public"."Order" RESTART IDENTITY CASCADE`,
31 | prisma.$executeRaw`TRUNCATE TABLE "public"."Product" RESTART IDENTITY CASCADE`,
32 | prisma.$executeRaw`TRUNCATE TABLE "public"."User" RESTART IDENTITY CASCADE`,
33 |
34 | // Add new data
35 | prisma.user.createMany({ data: users }),
36 | prisma.product.createMany({ data: products }),
37 | prisma.order.createMany({ data: orders }),
38 | ]);
39 |
40 | console.log(`Database has been seeded. 🌱`);
41 | }
42 |
43 | main()
44 | .then(async () => {
45 | await prisma.$disconnect();
46 | })
47 | .catch(async (e) => {
48 | console.error(e);
49 | await prisma.$disconnect();
50 | process.exit(1);
51 | });
52 |
--------------------------------------------------------------------------------
/audit-log-context/script.ts:
--------------------------------------------------------------------------------
1 | import { Prisma, PrismaClient } from "@prisma/client";
2 |
3 | function forUser(userId: number) {
4 | return Prisma.defineExtension((prisma) =>
5 | prisma.$extends({
6 | query: {
7 | $allModels: {
8 | async $allOperations({ args, query }) {
9 | const [, result] = await prisma.$transaction([
10 | prisma.$executeRaw`SELECT set_config('app.current_user_id', ${userId.toString()}, TRUE)`,
11 | query(args),
12 | ]);
13 | return result;
14 | },
15 | },
16 | },
17 | })
18 | );
19 | }
20 |
21 | const prisma = new PrismaClient();
22 |
23 | async function main() {
24 | const user = await prisma.user.findFirstOrThrow();
25 | const product = await prisma.product.findFirstOrThrow();
26 |
27 | const userPrisma = prisma.$extends(forUser(user.id));
28 |
29 | await userPrisma.product.update({
30 | where: { id: product.id },
31 | data: { name: "New Slurm" },
32 | });
33 |
34 | await userPrisma.product.update({
35 | where: { id: product.id },
36 | data: { name: "Slurm Classic" },
37 | });
38 |
39 | const order = await userPrisma.order.create({
40 | data: { userId: user.id, productId: product.id, quantity: 10 },
41 | });
42 |
43 | await userPrisma.order.update({
44 | where: { id: order.id },
45 | data: { quantity: 20 },
46 | });
47 |
48 | await userPrisma.order.delete({
49 | where: { id: order.id },
50 | });
51 |
52 | const productVersions = await userPrisma.productVersion.findMany({
53 | where: { id: product.id },
54 | });
55 |
56 | const orderVersions = await userPrisma.orderVersion.findMany({
57 | where: { id: order.id },
58 | });
59 |
60 | console.log({ productVersions, orderVersions });
61 | }
62 |
63 | main()
64 | .then(async () => {
65 | await prisma.$disconnect();
66 | })
67 | .catch(async (e) => {
68 | console.error(e);
69 | await prisma.$disconnect();
70 | process.exit(1);
71 | });
72 |
--------------------------------------------------------------------------------
/audit-log-context/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "exclude": ["node_modules"],
4 | "compilerOptions": {
5 | "noEmit": true,
6 | "lib": ["ES2019"],
7 | "isolatedModules": true,
8 | "esModuleInterop": true,
9 | "module": "CommonJS",
10 | "moduleResolution": "node",
11 | "resolveJsonModule": true,
12 | "target": "ES2019",
13 | "strict": true,
14 | "allowJs": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "skipLibCheck": true
17 | },
18 | "ts-node": {
19 | "compilerOptions": {
20 | "module": "commonjs"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/callback-free-itx/.env.example:
--------------------------------------------------------------------------------
1 | DATABASE_URL="postgresql://postgres:postgres@localhost:6005/postgres"
2 |
--------------------------------------------------------------------------------
/callback-free-itx/README.md:
--------------------------------------------------------------------------------
1 | # Prisma Client Extension - Callback-free Interactive Transactions
2 |
3 | This example shows a Prisma Client extension which adds a new API for starting [interactive transactions](https://www.prisma.io/docs/concepts/components/prisma-client/transactions#interactive-transactions) without callbacks.
4 |
5 | This gives you the full power of [interactive transactions](https://www.prisma.io/docs/concepts/components/prisma-client/transactions#interactive-transactions) (such as read–modify–write cycles), but in a more imperative API. This may be more convenient than the normal callback-style API for interactive transactions in some scenarios:
6 |
7 | ```typescript
8 | const tx = await prisma.$begin();
9 | const user = await tx.user.findFirstOrThrow();
10 | await tx.user.update(/* ... */);
11 | await tx.$commit(); // Or: await tx.$rollback();
12 | ```
13 |
14 | ## Caveats
15 |
16 | This extension is provided as an example only. It is not intended to be used in production environments.
17 |
18 | Please read [the documentation on `client` extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/client) for more information.
19 |
20 | ## How to use
21 |
22 | ### Prerequisites
23 |
24 | - Install [Node.js](https://nodejs.org/en/download/)
25 | - Install [Docker](https://docs.docker.com/get-docker/)
26 |
27 | ### 1. Download example & install dependencies
28 |
29 | Clone this repository:
30 |
31 | ```sh
32 | git clone git@github.com:sbking/prisma-client-extensions.git
33 | ```
34 |
35 | Create a `.env` file and install dependencies:
36 |
37 | ```sh
38 | cd callback-free-itx
39 | cp .env.example .env
40 | npm install
41 | ```
42 |
43 | ### 2. Start the database
44 |
45 | Run the following command to start a new Postgres database in a Docker container:
46 |
47 | ```sh
48 | docker compose up -d
49 | ```
50 |
51 | ### 3. Run migrations
52 |
53 | Run this command to apply migrations to the database:
54 |
55 | ```sh
56 | npx prisma migrate deploy
57 | ```
58 |
59 | ### 4. Seed the database
60 |
61 | Run the following command to add seed data to the database:
62 |
63 | ```sh
64 | npx prisma db seed
65 | ```
66 |
67 | ### 5. Run the `dev` script
68 |
69 | To run the `script.ts` file, run the following command:
70 |
71 | ```sh
72 | npm run dev
73 | ```
74 |
--------------------------------------------------------------------------------
/callback-free-itx/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.7"
2 | services:
3 | postgres:
4 | image: postgres:latest
5 | restart: always
6 | environment:
7 | - POSTGRES_USER=postgres
8 | - POSTGRES_PASSWORD=postgres
9 | - POSTGRES_DB=postgres
10 | ports:
11 | - "6005:5432"
12 | volumes:
13 | - postgres-data:/var/lib/postgresql/data
14 |
15 | volumes:
16 | postgres-data:
17 |
--------------------------------------------------------------------------------
/callback-free-itx/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "callback-free-itx",
3 | "private": true,
4 | "scripts": {
5 | "dev": "ts-node script.ts"
6 | },
7 | "dependencies": {
8 | "@prisma/client": "6.0.1"
9 | },
10 | "devDependencies": {
11 | "@faker-js/faker": "9.0.2",
12 | "@types/node": "22.8.2",
13 | "prisma": "6.0.1",
14 | "ts-node": "10.9.2",
15 | "typescript": "5.6.2"
16 | },
17 | "prisma": {
18 | "seed": "ts-node prisma/seed.ts"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/callback-free-itx/prisma/migrations/20221212035639_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" TEXT NOT NULL,
4 | "firstName" TEXT NOT NULL,
5 | "lastName" TEXT NOT NULL,
6 | "email" TEXT NOT NULL,
7 |
8 | CONSTRAINT "User_pkey" PRIMARY KEY ("id")
9 | );
10 |
--------------------------------------------------------------------------------
/callback-free-itx/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/callback-free-itx/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "postgresql"
7 | url = env("DATABASE_URL")
8 | }
9 |
10 | model User {
11 | id String @id @default(cuid())
12 | firstName String
13 | lastName String
14 | email String
15 | }
16 |
--------------------------------------------------------------------------------
/callback-free-itx/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | import { faker } from "@faker-js/faker";
2 | import { Prisma, PrismaClient } from "@prisma/client";
3 |
4 | const prisma = new PrismaClient();
5 |
6 | async function main() {
7 | // Cleanup existing users
8 | await prisma.user.deleteMany({});
9 |
10 | // Create 100 random users
11 | const users = Array.from({ length: 100 }, () => ({
12 | firstName: faker.name.firstName(),
13 | lastName: faker.name.lastName(),
14 | email: faker.internet.email(),
15 | })) satisfies Prisma.UserCreateInput[];
16 |
17 | // Seed the database
18 | for (const user of users) {
19 | await prisma.user.create({ data: user });
20 | }
21 |
22 | console.log(`Database has been seeded. 🌱`);
23 | }
24 |
25 | main()
26 | .then(async () => {
27 | await prisma.$disconnect();
28 | })
29 | .catch(async (e) => {
30 | console.error(e);
31 | await prisma.$disconnect();
32 | process.exit(1);
33 | });
34 |
--------------------------------------------------------------------------------
/callback-free-itx/script.ts:
--------------------------------------------------------------------------------
1 | import { Prisma, PrismaClient } from "@prisma/client";
2 |
3 | type FlatTransactionClient = Prisma.TransactionClient & {
4 | $commit: () => Promise;
5 | $rollback: () => Promise;
6 | };
7 |
8 | const ROLLBACK = { [Symbol.for("prisma.client.extension.rollback")]: true };
9 |
10 | const prisma = new PrismaClient({ log: ["query"] }).$extends({
11 | client: {
12 | async $begin() {
13 | const prisma = Prisma.getExtensionContext(this);
14 | let setTxClient: (txClient: Prisma.TransactionClient) => void;
15 | let commit: () => void;
16 | let rollback: () => void;
17 |
18 | // a promise for getting the tx inner client
19 | const txClient = new Promise((res) => {
20 | setTxClient = (txClient) => res(txClient);
21 | });
22 |
23 | // a promise for controlling the transaction
24 | const txPromise = new Promise((_res, _rej) => {
25 | commit = () => _res(undefined);
26 | rollback = () => _rej(ROLLBACK);
27 | });
28 |
29 | // opening a transaction to control externally
30 | if (
31 | "$transaction" in prisma &&
32 | typeof prisma.$transaction === "function"
33 | ) {
34 | const tx = prisma.$transaction((txClient) => {
35 | setTxClient(txClient as unknown as Prisma.TransactionClient);
36 |
37 | return txPromise.catch((e) => {
38 | if (e === ROLLBACK) return;
39 | throw e;
40 | });
41 | });
42 |
43 | // return a proxy TransactionClient with `$commit` and `$rollback` methods
44 | return new Proxy(await txClient, {
45 | get(target, prop) {
46 | if (prop === "$commit") {
47 | return () => {
48 | commit();
49 | return tx;
50 | };
51 | }
52 | if (prop === "$rollback") {
53 | return () => {
54 | rollback();
55 | return tx;
56 | };
57 | }
58 | return target[prop as keyof typeof target];
59 | },
60 | }) as FlatTransactionClient;
61 | }
62 |
63 | throw new Error("Transactions are not supported by this client");
64 | },
65 | },
66 | });
67 |
68 | async function main() {
69 | const tx = await prisma.$begin();
70 | const user = await tx.user.findFirstOrThrow();
71 |
72 | const tx2 = await prisma.$begin();
73 | await tx2.user.findMany();
74 |
75 | await tx.user.update({
76 | where: { id: user.id },
77 | data: { firstName: `${user.firstName} II` },
78 | });
79 | await tx.$commit();
80 |
81 | await tx2.user.count();
82 | await tx2.$commit();
83 | }
84 |
85 | main()
86 | .then(async () => {
87 | await prisma.$disconnect();
88 | })
89 | .catch(async (e) => {
90 | console.error(e);
91 | await prisma.$disconnect();
92 | process.exit(1);
93 | });
94 |
--------------------------------------------------------------------------------
/callback-free-itx/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "exclude": ["node_modules"],
4 | "compilerOptions": {
5 | "noEmit": true,
6 | "lib": ["ES2019"],
7 | "isolatedModules": true,
8 | "esModuleInterop": true,
9 | "module": "CommonJS",
10 | "moduleResolution": "node",
11 | "resolveJsonModule": true,
12 | "target": "ES2019",
13 | "strict": true,
14 | "allowJs": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "skipLibCheck": true
17 | },
18 | "ts-node": {
19 | "compilerOptions": {
20 | "module": "commonjs"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/computed-fields/README.md:
--------------------------------------------------------------------------------
1 | # Prisma Client Extension - Computed Fields
2 |
3 | This example demonstrates how to create a Prisma Client extension that adds virtual / computed fields to a Prisma model. These fields are not included in the database, but rather are computed at runtime.
4 |
5 | Computed fields are type-safe and may return anything from simple values to complex objects, or even functions that can act as methods for your models.
6 |
7 | Computed fields must specify which other fields they depend on, and they may be composed / reused by other computed fields.
8 |
9 | ## Caveats
10 |
11 | This extension is provided as an example only. It is not intended to be used in production environments.
12 |
13 | Please read [the documentation on `result` extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/result) for more information.
14 |
15 | ## How to use
16 |
17 | ### Prerequisites
18 |
19 | - Install [Node.js](https://nodejs.org/en/download/)
20 |
21 | ### 1. Download example & install dependencies
22 |
23 | Clone this repository:
24 |
25 | ```sh
26 | git clone git@github.com:sbking/prisma-client-extensions.git
27 | ```
28 |
29 | Install dependencies:
30 |
31 | ```sh
32 | cd computed-fields
33 | npm install
34 | ```
35 |
36 | ### 2. Create an SQLite database and run migrations
37 |
38 | Run the following command. An SQLite database will be created automatically:
39 |
40 | ```sh
41 | npx prisma migrate deploy
42 | ```
43 |
44 | ### 3. Seed the database
45 |
46 | Run the following command to add seed data to the database:
47 |
48 | ```sh
49 | npx prisma db seed
50 | ```
51 |
52 | ### 4. Run the `dev` script
53 |
54 | To run the `script.ts` file, run the following command:
55 |
56 | ```sh
57 | npm run dev
58 | ```
59 |
--------------------------------------------------------------------------------
/computed-fields/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "computed-fields",
3 | "private": true,
4 | "scripts": {
5 | "dev": "ts-node script.ts"
6 | },
7 | "dependencies": {
8 | "@prisma/client": "6.0.1"
9 | },
10 | "devDependencies": {
11 | "@faker-js/faker": "9.0.2",
12 | "@types/node": "22.8.2",
13 | "prisma": "6.0.1",
14 | "ts-node": "10.9.2",
15 | "typescript": "5.6.2"
16 | },
17 | "prisma": {
18 | "seed": "ts-node prisma/seed.ts"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/computed-fields/prisma/migrations/20221209170543_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" TEXT NOT NULL PRIMARY KEY,
4 | "firstName" TEXT NOT NULL,
5 | "lastName" TEXT NOT NULL,
6 | "email" TEXT NOT NULL
7 | );
8 |
--------------------------------------------------------------------------------
/computed-fields/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "sqlite"
--------------------------------------------------------------------------------
/computed-fields/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "sqlite"
7 | url = "file:./dev.db"
8 | }
9 |
10 | model User {
11 | id String @id @default(cuid())
12 | firstName String
13 | lastName String
14 | email String
15 | }
16 |
--------------------------------------------------------------------------------
/computed-fields/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | import { faker } from "@faker-js/faker";
2 | import { Prisma, PrismaClient } from "@prisma/client";
3 |
4 | const prisma = new PrismaClient();
5 |
6 | async function main() {
7 | // Cleanup existing users
8 | await prisma.user.deleteMany({});
9 |
10 | // Create 100 random users
11 | const users = Array.from({ length: 100 }, () => ({
12 | firstName: faker.name.firstName(),
13 | lastName: faker.name.lastName(),
14 | email: faker.internet.email(),
15 | })) satisfies Prisma.UserCreateInput[];
16 |
17 | // Seed the database
18 | for (const user of users) {
19 | await prisma.user.create({ data: user });
20 | }
21 |
22 | console.log(`Database has been seeded. 🌱`);
23 | }
24 |
25 | main()
26 | .then(async () => {
27 | await prisma.$disconnect();
28 | })
29 | .catch(async (e) => {
30 | console.error(e);
31 | await prisma.$disconnect();
32 | process.exit(1);
33 | });
34 |
--------------------------------------------------------------------------------
/computed-fields/script.ts:
--------------------------------------------------------------------------------
1 | import { Prisma, PrismaClient } from "@prisma/client";
2 |
3 | const prisma = new PrismaClient()
4 | .$extends({
5 | result: {
6 | user: {
7 | fullName: {
8 | needs: { firstName: true, lastName: true },
9 | compute(user) {
10 | return `${user.firstName} ${user.lastName}`;
11 | },
12 | },
13 | },
14 | },
15 | })
16 | .$extends({
17 | result: {
18 | user: {
19 | displayName: {
20 | needs: { fullName: true, email: true },
21 | compute(user) {
22 | return `${user.fullName} <${user.email}>`;
23 | },
24 | },
25 | },
26 | },
27 | });
28 |
29 | async function main() {
30 | const users = await prisma.user.findMany({ take: 5 });
31 |
32 | for (const user of users) {
33 | console.info(`- ${user.displayName}`);
34 | }
35 | }
36 |
37 | main()
38 | .then(async () => {
39 | await prisma.$disconnect();
40 | })
41 | .catch(async (e) => {
42 | console.error(e);
43 | await prisma.$disconnect();
44 | process.exit(1);
45 | });
46 |
--------------------------------------------------------------------------------
/computed-fields/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "exclude": ["node_modules"],
4 | "compilerOptions": {
5 | "noEmit": true,
6 | "lib": ["ES2019"],
7 | "isolatedModules": true,
8 | "esModuleInterop": true,
9 | "module": "CommonJS",
10 | "moduleResolution": "node",
11 | "resolveJsonModule": true,
12 | "target": "ES2019",
13 | "strict": true,
14 | "allowJs": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "skipLibCheck": true
17 | },
18 | "ts-node": {
19 | "compilerOptions": {
20 | "module": "commonjs"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/exists-fn/README.md:
--------------------------------------------------------------------------------
1 | # Prisma Client Extension - `exists` function
2 |
3 | This example demonstrates how to create a Prisma Client extension that adds an `exists` method to all your models.
4 |
5 |
6 | ## Caveats
7 |
8 | This extension is provided as an example only. It is not intended to be used in production environments.
9 |
10 | Please read [the documentation on `model` extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/model) for more information.
11 |
12 | ## How to use
13 |
14 | ### Prerequisites
15 |
16 | - Install [Node.js](https://nodejs.org/en/download/)
17 |
18 | ### 1. Download example & install dependencies
19 |
20 | Clone this repository:
21 |
22 | ```sh
23 | git clone git@github.com:sbking/prisma-client-extensions.git
24 | ```
25 |
26 | Install dependencies:
27 |
28 | ```sh
29 | cd exists-fn
30 | npm install
31 | ```
32 |
33 | ### 2. Create an SQLite database and run migrations
34 |
35 | Run the following command. An SQLite database will be created automatically and seeded with data contained in [`seed.ts`](./prisma/seed.ts):
36 |
37 | ```sh
38 | npx prisma migrate dev --name init
39 | ```
40 |
41 | ### 3. Run the `dev` script
42 |
43 | To run the `script.ts` file, run the following command:
44 |
45 | ```sh
46 | npm run dev
47 | ```
48 |
--------------------------------------------------------------------------------
/exists-fn/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "exists-fn",
3 | "private": true,
4 | "scripts": {
5 | "dev": "ts-node script.ts"
6 | },
7 | "dependencies": {
8 | "@prisma/client": "6.0.1"
9 | },
10 | "devDependencies": {
11 | "@types/node": "22.8.2",
12 | "prisma": "6.0.1",
13 | "ts-node": "10.9.2",
14 | "typescript": "5.6.2"
15 | },
16 | "prisma": {
17 | "seed": "ts-node --transpile-only prisma/seed.ts"
18 | }
19 | }
--------------------------------------------------------------------------------
/exists-fn/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "sqlite"
7 | url = "file:./dev.db"
8 | }
9 |
10 | model User {
11 | id Int @id @default(autoincrement())
12 | email String @unique
13 | name String?
14 | posts Post[]
15 | }
16 |
17 | model Post {
18 | id Int @id @default(autoincrement())
19 | createdAt DateTime @default(now())
20 | updatedAt DateTime @updatedAt
21 | title String
22 | content String?
23 | published Boolean @default(false)
24 | viewCount Int @default(0)
25 | author User? @relation(fields: [authorId], references: [id])
26 | authorId Int?
27 | }
--------------------------------------------------------------------------------
/exists-fn/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient, Prisma } from '@prisma/client'
2 |
3 | const prisma = new PrismaClient()
4 |
5 | const userData: Prisma.UserCreateInput[] = [
6 | {
7 | name: 'Alice',
8 | email: 'alice@prisma.io',
9 | posts: {
10 | create: [
11 | {
12 | title: 'Join the Prisma Discord',
13 | content: 'https://pris.ly/discord',
14 | published: true,
15 | },
16 | ],
17 | },
18 | },
19 | {
20 | name: 'Nilu',
21 | email: 'nilu@prisma.io',
22 | posts: {
23 | create: [
24 | {
25 | title: 'Follow Prisma on Twitter',
26 | content: 'https://www.twitter.com/prisma',
27 | published: true,
28 | viewCount: 42,
29 | },
30 | ],
31 | },
32 | },
33 | {
34 | name: 'Mahmoud',
35 | email: 'mahmoud@prisma.io',
36 | posts: {
37 | create: [
38 | {
39 | title: 'Ask a question about Prisma on GitHub',
40 | content: 'https://www.github.com/prisma/prisma/discussions',
41 | published: true,
42 | viewCount: 128,
43 | },
44 | {
45 | title: 'Prisma on YouTube',
46 | content: 'https://pris.ly/youtube',
47 | },
48 | ],
49 | },
50 | },
51 | ]
52 |
53 | async function main() {
54 | console.log(`Start seeding ...`)
55 | for (const u of userData) {
56 | const user = await prisma.user.create({
57 | data: u,
58 | })
59 | console.log(`Created user with id: ${user.id}`)
60 | }
61 | console.log(`Seeding finished.`)
62 | }
63 |
64 | main()
65 | .then(async () => {
66 | await prisma.$disconnect()
67 | })
68 | .catch(async (e) => {
69 | console.error(e)
70 | await prisma.$disconnect()
71 | process.exit(1)
72 | })
73 |
--------------------------------------------------------------------------------
/exists-fn/script.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient, Prisma } from "@prisma/client";
2 |
3 | const prisma = new PrismaClient().$extends({
4 | model: {
5 | $allModels: {
6 | async exists(
7 | this: T,
8 | where: Prisma.Args['where']
9 | ): Promise {
10 | const context = Prisma.getExtensionContext(this)
11 | const result = await (context as any).findFirst({ where })
12 | return result !== null
13 | },
14 | },
15 | },
16 | })
17 |
18 | async function main() {
19 | const user = await prisma.user.exists({
20 | name: 'Alice'
21 | })
22 |
23 | console.log({ user })
24 |
25 | const post = await prisma.post.exists({
26 | OR: [
27 | { title: { contains: 'Prisma' } },
28 | { content: { contains: 'Prisma' } }
29 | ]
30 | })
31 |
32 | console.log({ post })
33 | }
34 |
35 | main()
36 | .then(async () => {
37 | await prisma.$disconnect();
38 | })
39 | .catch(async (e) => {
40 | console.error(e);
41 | await prisma.$disconnect();
42 | process.exit(1);
43 | });
44 |
--------------------------------------------------------------------------------
/exists-fn/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "exclude": ["node_modules"],
4 | "compilerOptions": {
5 | "noEmit": true,
6 | "lib": ["ES2019"],
7 | "isolatedModules": true,
8 | "esModuleInterop": true,
9 | "module": "CommonJS",
10 | "moduleResolution": "node",
11 | "resolveJsonModule": true,
12 | "target": "ES2019",
13 | "strict": true,
14 | "allowJs": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "skipLibCheck": true
17 | },
18 | "ts-node": {
19 | "compilerOptions": {
20 | "module": "commonjs"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/input-transformation/README.md:
--------------------------------------------------------------------------------
1 | # Prisma Client Extension - Input Transformation
2 |
3 | This example creates an `adminPrisma` client instance, as well as an extended `publicPrisma` client instance. The `publicPrisma` client automatically restricts queries to only include `published` posts.
4 |
5 | This extension also uses the [`extendedWhereUnique` preview flag](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#enable-the-ability-to-filter-on-non-unique-fields-with-userwhereuniqueinput) to allow filtering on non-unique fields like `published` in methods like `findUnique`.
6 |
7 | Currently, `query` extensions allow you to modify the arguments passed to Prisma Client methods like `findMany` which do not affect the return type, such as `where`, `take`, and `skip`. This allows you to customize how queries are run and filter down the result set.
8 |
9 | ## Caveats
10 |
11 | > **NOTE**: Query extensions do not currently work for nested operations. In this example, the `publicPrisma` client will only filter out unpublished posts when calling a top level `post` method such as `publicPrisma.post.findMany`. It won't filter out posts that are included as relations of another model with [nested reads using `include` or `select`](https://www.prisma.io/docs/concepts/components/prisma-client/relation-queries#nested-reads).
12 |
13 | This extension is provided as an example only. It is not intended to be used in production environments.
14 |
15 | Please read [the documentation on `query` extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/query) for more information.
16 |
17 | ## How to use
18 |
19 | ### Prerequisites
20 |
21 | - Install [Node.js](https://nodejs.org/en/download/)
22 |
23 | ### 1. Download example & install dependencies
24 |
25 | Clone this repository:
26 |
27 | ```sh
28 | git clone git@github.com:sbking/prisma-client-extensions.git
29 | ```
30 |
31 | Install dependencies:
32 |
33 | ```sh
34 | cd input-transformation
35 | npm install
36 | ```
37 |
38 | ### 2. Create an SQLite database and run migrations
39 |
40 | Run the following command. An SQLite database will be created automatically:
41 |
42 | ```sh
43 | npx prisma migrate deploy
44 | ```
45 |
46 | ### 3. Seed the database
47 |
48 | Run the following command to add seed data to the database:
49 |
50 | ```sh
51 | npx prisma db seed
52 | ```
53 |
54 | ### 4. Run the `dev` script
55 |
56 | To run the `script.ts` file, run the following command:
57 |
58 | ```sh
59 | npm run dev
60 | ```
61 |
--------------------------------------------------------------------------------
/input-transformation/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "input-transformation",
3 | "private": true,
4 | "scripts": {
5 | "dev": "ts-node script.ts"
6 | },
7 | "dependencies": {
8 | "@prisma/client": "6.0.1"
9 | },
10 | "devDependencies": {
11 | "@faker-js/faker": "9.0.2",
12 | "@types/node": "22.8.2",
13 | "prisma": "6.0.1",
14 | "ts-node": "10.9.2",
15 | "typescript": "5.6.2"
16 | },
17 | "prisma": {
18 | "seed": "ts-node prisma/seed.ts"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/input-transformation/prisma/migrations/20221209213844_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "Post" (
3 | "id" TEXT NOT NULL PRIMARY KEY,
4 | "title" TEXT NOT NULL,
5 | "published" BOOLEAN NOT NULL
6 | );
7 |
--------------------------------------------------------------------------------
/input-transformation/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "sqlite"
--------------------------------------------------------------------------------
/input-transformation/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "sqlite"
7 | url = "file:./dev.db"
8 | }
9 |
10 | model Post {
11 | id String @id @default(cuid())
12 | title String
13 | published Boolean
14 | }
15 |
--------------------------------------------------------------------------------
/input-transformation/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | import { faker } from "@faker-js/faker";
2 | import { Prisma, PrismaClient } from "@prisma/client";
3 |
4 | const prisma = new PrismaClient();
5 |
6 | async function main() {
7 | // Cleanup existing users
8 | await prisma.post.deleteMany({});
9 |
10 | // Create 100 random posts
11 | const posts = Array.from({ length: 100 }, () => ({
12 | title: faker.hacker.phrase(),
13 | published: faker.datatype.boolean(),
14 | })) satisfies Prisma.PostCreateInput[];
15 |
16 | // Seed the database
17 | for (const post of posts) {
18 | await prisma.post.create({ data: post });
19 | }
20 |
21 | console.log(`Database has been seeded. 🌱`);
22 | }
23 |
24 | main()
25 | .then(async () => {
26 | await prisma.$disconnect();
27 | })
28 | .catch(async (e) => {
29 | console.error(e);
30 | await prisma.$disconnect();
31 | process.exit(1);
32 | });
33 |
--------------------------------------------------------------------------------
/input-transformation/script.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | const adminPrisma = new PrismaClient();
4 |
5 | const publicPrisma = adminPrisma.$extends({
6 | query: {
7 | post: {
8 | $allOperations({ args, query, operation }) {
9 | // Do nothing for `create`
10 | if (operation === "create") {
11 | return query(args);
12 | }
13 |
14 | // Refine the type - methods other than `create` accept a `where` clause
15 | args = args as Extract;
16 |
17 | // Augment the `where` clause with `published: true`
18 | return query({
19 | ...args,
20 | where: {
21 | ...args.where,
22 | published: true,
23 | },
24 | });
25 | },
26 | },
27 | },
28 | });
29 |
30 | async function main() {
31 | const allPosts = await adminPrisma.post.count();
32 | console.log(`- All posts: ${allPosts}`);
33 |
34 | const publishedPosts = await publicPrisma.post.count();
35 | console.log(`- Published posts: ${publishedPosts}`);
36 | }
37 |
38 | main()
39 | .then(async () => {
40 | await adminPrisma.$disconnect();
41 | })
42 | .catch(async (e) => {
43 | console.error(e);
44 | await adminPrisma.$disconnect();
45 | process.exit(1);
46 | });
47 |
--------------------------------------------------------------------------------
/input-transformation/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "exclude": ["node_modules"],
4 | "compilerOptions": {
5 | "noEmit": true,
6 | "lib": ["ES2019"],
7 | "isolatedModules": true,
8 | "esModuleInterop": true,
9 | "module": "CommonJS",
10 | "moduleResolution": "node",
11 | "resolveJsonModule": true,
12 | "target": "ES2019",
13 | "strict": true,
14 | "allowJs": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "skipLibCheck": true
17 | },
18 | "ts-node": {
19 | "compilerOptions": {
20 | "module": "commonjs"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/input-validation/README.md:
--------------------------------------------------------------------------------
1 | # Prisma Client Extension - Input Validation
2 |
3 | This example uses Prisma Client extensions to perform custom runtime validations when creating and updating database objects.
4 |
5 | This technique could be used to sanitize user input or otherwise deny mutations that do not meet some criteria.
6 |
7 | ## Caveats
8 |
9 | > **NOTE**: Query extensions do not currently work for nested operations. In this example, validations are only run on the top level `data` object passed to methods such as `prisma.product.create()`. Validations implemented this way do not automatically run for [nested writes](https://www.prisma.io/docs/concepts/components/prisma-client/relation-queries#nested-writes).
10 |
11 | This extension is provided as an example only. It is not intended to be used in production environments.
12 |
13 | Please read [the documentation on `query` extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/query) for more information.
14 |
15 | ## How to use
16 |
17 | ### Prerequisites
18 |
19 | - Install [Node.js](https://nodejs.org/en/download/)
20 |
21 | ### 1. Download example & install dependencies
22 |
23 | Clone this repository:
24 |
25 | ```sh
26 | git clone git@github.com:sbking/prisma-client-extensions.git
27 | ```
28 |
29 | Install dependencies:
30 |
31 | ```sh
32 | cd input-validation
33 | npm install
34 | ```
35 |
36 | ### 2. Create an SQLite database and run migrations
37 |
38 | Run the following command. An SQLite database will be created automatically:
39 |
40 | ```sh
41 | npx prisma migrate deploy
42 | ```
43 |
44 | ### 3. Run the `dev` script
45 |
46 | To run the `script.ts` file, run the following command:
47 |
48 | ```sh
49 | npm run dev
50 | ```
51 |
--------------------------------------------------------------------------------
/input-validation/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "input-validation",
3 | "private": true,
4 | "scripts": {
5 | "dev": "ts-node src/index.ts"
6 | },
7 | "dependencies": {
8 | "@prisma/client": "6.0.1",
9 | "zod": "3.23.8"
10 | },
11 | "devDependencies": {
12 | "@faker-js/faker": "9.0.2",
13 | "@types/node": "22.8.2",
14 | "prisma": "6.0.1",
15 | "ts-node": "10.9.2",
16 | "typescript": "5.6.2"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/input-validation/prisma/migrations/20221209220230_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "Product" (
3 | "id" TEXT NOT NULL PRIMARY KEY,
4 | "slug" TEXT NOT NULL,
5 | "name" TEXT NOT NULL,
6 | "description" TEXT NOT NULL,
7 | "price" DECIMAL NOT NULL
8 | );
9 |
10 | -- CreateTable
11 | CREATE TABLE "Review" (
12 | "id" TEXT NOT NULL PRIMARY KEY,
13 | "body" TEXT NOT NULL,
14 | "stars" INTEGER NOT NULL,
15 | "productId" TEXT NOT NULL,
16 | CONSTRAINT "Review_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product" ("id") ON DELETE CASCADE ON UPDATE CASCADE
17 | );
18 |
--------------------------------------------------------------------------------
/input-validation/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "sqlite"
--------------------------------------------------------------------------------
/input-validation/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "sqlite"
7 | url = "file:./dev.db"
8 | }
9 |
10 | model Product {
11 | id String @id @default(cuid())
12 | slug String
13 | name String
14 | description String
15 | price Decimal
16 | reviews Review[]
17 | }
18 |
19 | model Review {
20 | id String @id @default(cuid())
21 | body String
22 | stars Int
23 | product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
24 | productId String
25 | }
26 |
--------------------------------------------------------------------------------
/input-validation/src/index.ts:
--------------------------------------------------------------------------------
1 | import { Prisma, PrismaClient } from "@prisma/client";
2 | import { ProductValidation } from "./models/product";
3 | import { ReviewValidation } from "./models/review";
4 |
5 | const prisma = new PrismaClient()
6 | .$extends(ProductValidation)
7 | .$extends(ReviewValidation);
8 |
9 | async function main() {
10 | await prisma.product.deleteMany();
11 |
12 | // Valid product
13 | const product = await prisma.product.create({
14 | data: {
15 | slug: "example-product",
16 | name: "Example Product",
17 | description: "Lorem ipsum dolor sit amet",
18 | price: new Prisma.Decimal("10.95"),
19 | },
20 | });
21 |
22 | // Valid review
23 | const review = await prisma.review.create({
24 | data: {
25 | stars: 4,
26 | body: "Lorem ipsum dolor sit amet",
27 | productId: product.id,
28 | },
29 | });
30 |
31 | console.log({ product, review });
32 |
33 | // Invalid product
34 | try {
35 | await prisma.product.create({
36 | data: {
37 | slug: "invalid-product",
38 | name: "Invalid Product",
39 | description: "Lorem ipsum dolor sit amet",
40 | price: new Prisma.Decimal("-1.00"),
41 | },
42 | });
43 | } catch (err: any) {
44 | console.log(err?.cause?.issues);
45 | }
46 |
47 | // Invalid review
48 | try {
49 | await prisma.review.create({
50 | data: {
51 | stars: 6,
52 | body: "Wow!",
53 | productId: product.id,
54 | },
55 | });
56 | } catch (err: any) {
57 | console.log(err?.cause?.issues);
58 | }
59 | }
60 |
61 | main()
62 | .then(async () => {
63 | await prisma.$disconnect();
64 | })
65 | .catch(async (e) => {
66 | console.error(e);
67 | await prisma.$disconnect();
68 | process.exit(1);
69 | });
70 |
--------------------------------------------------------------------------------
/input-validation/src/models/product.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { Prisma } from "@prisma/client";
3 |
4 | const schema = z.object({
5 | slug: z
6 | .string()
7 | .max(100)
8 | .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
9 | name: z.string().max(100),
10 | description: z.string().max(1000),
11 | price: z
12 | .instanceof(Prisma.Decimal)
13 | .refine((price) => price.gte("0.01") && price.lt("1000000.00")),
14 | }) satisfies z.Schema;
15 |
16 | export const ProductValidation = Prisma.defineExtension({
17 | query: {
18 | product: {
19 | create({ args, query }) {
20 | args.data = schema.parse(args.data);
21 | return query(args);
22 | },
23 | update({ args, query }) {
24 | args.data = schema.partial().parse(args.data);
25 | return query(args);
26 | },
27 | updateMany({ args, query }) {
28 | args.data = schema.partial().parse(args.data);
29 | return query(args);
30 | },
31 | upsert({ args, query }) {
32 | args.create = schema.parse(args.create);
33 | args.update = schema.partial().parse(args.update);
34 | return query(args);
35 | },
36 | },
37 | },
38 | });
39 |
--------------------------------------------------------------------------------
/input-validation/src/models/review.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { Prisma } from "@prisma/client";
3 |
4 | const schema = z.object({
5 | body: z.string().max(250),
6 | stars: z.number().int().min(1).max(5),
7 | productId: z.string(),
8 | }) satisfies z.Schema;
9 |
10 | export const ReviewValidation = Prisma.defineExtension({
11 | query: {
12 | review: {
13 | create({ args, query }) {
14 | args.data = schema.parse(args.data);
15 | return query(args);
16 | },
17 | update({ args, query }) {
18 | args.data = schema.partial().parse(args.data);
19 | return query(args);
20 | },
21 | updateMany({ args, query }) {
22 | args.data = schema.partial().parse(args.data);
23 | return query(args);
24 | },
25 | upsert({ args, query }) {
26 | args.create = schema.parse(args.create);
27 | args.update = schema.partial().parse(args.update);
28 | return query(args);
29 | },
30 | },
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/input-validation/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "exclude": ["node_modules"],
4 | "compilerOptions": {
5 | "noEmit": true,
6 | "lib": ["ES2019"],
7 | "isolatedModules": true,
8 | "esModuleInterop": true,
9 | "module": "CommonJS",
10 | "moduleResolution": "node",
11 | "resolveJsonModule": true,
12 | "target": "ES2019",
13 | "strict": true,
14 | "allowJs": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "skipLibCheck": true
17 | },
18 | "ts-node": {
19 | "compilerOptions": {
20 | "module": "commonjs"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/instance-methods/README.md:
--------------------------------------------------------------------------------
1 | # Prisma Client Extension - Instance Methods
2 |
3 | This example shows how to add an [Active Record](https://www.martinfowler.com/eaaCatalog/activeRecord.html)-like interface to Prisma result objects. It uses a `result` extension to add `save` and `delete` methods directly to `User` model objects returned by Prisma Client methods.
4 |
5 | This technique can be used to customize Prisma result objects with behavior, analogous to adding instance methods to model classes.
6 |
7 | ## Caveats
8 |
9 | This extension is provided as an example only. It is not intended to be used in production environments.
10 |
11 | Please read [the documentation on `result` extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/result) for more information.
12 |
13 | ## How to use
14 |
15 | ### Prerequisites
16 |
17 | - Install [Node.js](https://nodejs.org/en/download/)
18 |
19 | ### 1. Download example & install dependencies
20 |
21 | Clone this repository:
22 |
23 | ```sh
24 | git clone git@github.com:sbking/prisma-client-extensions.git
25 | ```
26 |
27 | Install dependencies:
28 |
29 | ```sh
30 | cd instance-methods
31 | npm install
32 | ```
33 |
34 | ### 2. Create an SQLite database and run migrations
35 |
36 | Run the following command. An SQLite database will be created automatically:
37 |
38 | ```sh
39 | npx prisma migrate deploy
40 | ```
41 |
42 | ### 3. Run the `dev` script
43 |
44 | To run the `script.ts` file, run the following command:
45 |
46 | ```sh
47 | npm run dev
48 | ```
49 |
--------------------------------------------------------------------------------
/instance-methods/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "instance-methods",
3 | "private": true,
4 | "scripts": {
5 | "dev": "ts-node script.ts"
6 | },
7 | "dependencies": {
8 | "@prisma/client": "6.0.1"
9 | },
10 | "devDependencies": {
11 | "@faker-js/faker": "9.0.2",
12 | "@types/node": "22.8.2",
13 | "prisma": "6.0.1",
14 | "ts-node": "10.9.2",
15 | "typescript": "5.6.2"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/instance-methods/prisma/migrations/20221209221951_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" TEXT NOT NULL PRIMARY KEY,
4 | "email" TEXT NOT NULL
5 | );
6 |
--------------------------------------------------------------------------------
/instance-methods/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "sqlite"
--------------------------------------------------------------------------------
/instance-methods/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "sqlite"
7 | url = "file:./dev.db"
8 | }
9 |
10 | model User {
11 | id String @id @default(cuid())
12 | email String
13 | }
14 |
--------------------------------------------------------------------------------
/instance-methods/script.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | const prisma = new PrismaClient().$extends({
4 | result: {
5 | user: {
6 | save: {
7 | needs: { id: true, email: true },
8 | compute({ id, email }) {
9 | return () => prisma.user.update({ where: { id }, data: { email } });
10 | },
11 | },
12 |
13 | delete: {
14 | needs: { id: true },
15 | compute({ id }) {
16 | return () => prisma.user.delete({ where: { id } });
17 | },
18 | },
19 | },
20 | },
21 | });
22 |
23 | async function main() {
24 | const user = await prisma.user.create({
25 | data: { email: "test@example.com" },
26 | });
27 | user.email = "updated@example.com";
28 |
29 | await user.save();
30 | console.info(
31 | "Updated object: ",
32 | await prisma.user.findUnique({ where: { id: user.id } })
33 | );
34 |
35 | await user.delete();
36 | console.info(
37 | "Deleted object: ",
38 | await prisma.user.findUnique({ where: { id: user.id } })
39 | );
40 | }
41 |
42 | main()
43 | .then(async () => {
44 | await prisma.$disconnect();
45 | })
46 | .catch(async (e) => {
47 | console.error(e);
48 | await prisma.$disconnect();
49 | process.exit(1);
50 | });
51 |
--------------------------------------------------------------------------------
/instance-methods/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "exclude": ["node_modules"],
4 | "compilerOptions": {
5 | "noEmit": true,
6 | "lib": ["ES2019"],
7 | "isolatedModules": true,
8 | "esModuleInterop": true,
9 | "module": "CommonJS",
10 | "moduleResolution": "node",
11 | "resolveJsonModule": true,
12 | "target": "ES2019",
13 | "strict": true,
14 | "allowJs": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "skipLibCheck": true
17 | },
18 | "ts-node": {
19 | "compilerOptions": {
20 | "module": "commonjs"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/json-field-types/.env.example:
--------------------------------------------------------------------------------
1 | DATABASE_URL="postgresql://postgres:postgres@localhost:6003/postgres"
2 |
--------------------------------------------------------------------------------
/json-field-types/README.md:
--------------------------------------------------------------------------------
1 | # Prisma Client Extension - JSON Field Types
2 |
3 | This example shows how to use a Prisma Client extension to provide static and runtime types for a `Json` field. It uses [Zod](https://github.com/colinhacks/zod) to parse the field data and infer static TypeScript types.
4 |
5 | This example includes a `User` model with a JSON `profile` field, which has a sparse structure which may vary between each users. The extension has two parts:
6 |
7 | - A `result` extension that adds a computed `profileData` field. This field uses the `Profile` Zod parser to parse the untyped `profile` field. TypeScript infers the static data type from the parser, so query results have both static and runtime type safety.
8 | - A `query` extension that parses the `profile` field of input data for the `User` model's write methods like `create` and `update`.
9 |
10 | ## Caveats
11 |
12 | > **NOTE**: Query extensions do not currently work for nested operations. In this example, validations are only run on the top level `data` object passed to methods such as `prisma.user.create()`. Validations implemented this way do not automatically run for [nested writes](https://www.prisma.io/docs/concepts/components/prisma-client/relation-queries#nested-writes).
13 |
14 | This extension is provided as an example only. It is not intended to be used in production environments.
15 |
16 | Please read the documentation on [`query` extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/query) and [`result` extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/query) for more information.
17 |
18 | ## How to use
19 |
20 | ### Prerequisites
21 |
22 | - Install [Node.js](https://nodejs.org/en/download/)
23 | - Install [Docker](https://docs.docker.com/get-docker/)
24 |
25 | ### 1. Download example & install dependencies
26 |
27 | Clone this repository:
28 |
29 | ```sh
30 | git clone git@github.com:sbking/prisma-client-extensions.git
31 | ```
32 |
33 | Create a `.env` file and install dependencies:
34 |
35 | ```sh
36 | cd json-field-types
37 | cp .env.example .env
38 | npm install
39 | ```
40 |
41 | ### 2. Start the database
42 |
43 | Run the following command to start a new Postgres database in a Docker container:
44 |
45 | ```sh
46 | docker compose up -d
47 | ```
48 |
49 | ### 3. Run migrations
50 |
51 | Run this command to apply migrations to the database:
52 |
53 | ```sh
54 | npx prisma migrate deploy
55 | ```
56 |
57 | ### 4. Seed the database
58 |
59 | Run the following command to add seed data to the database:
60 |
61 | ```sh
62 | npx prisma db seed
63 | ```
64 |
65 | ### 5. Run the `dev` script
66 |
67 | To run the `script.ts` file, run the following command:
68 |
69 | ```sh
70 | npm run dev
71 | ```
72 |
--------------------------------------------------------------------------------
/json-field-types/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.7"
2 | services:
3 | postgres:
4 | image: postgres:latest
5 | restart: always
6 | environment:
7 | - POSTGRES_USER=postgres
8 | - POSTGRES_PASSWORD=postgres
9 | - POSTGRES_DB=postgres
10 | ports:
11 | - "6003:5432"
12 | volumes:
13 | - postgres-data:/var/lib/postgresql/data
14 |
15 | volumes:
16 | postgres-data:
17 |
--------------------------------------------------------------------------------
/json-field-types/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "json-field-types",
3 | "private": true,
4 | "scripts": {
5 | "dev": "ts-node src/index.ts"
6 | },
7 | "dependencies": {
8 | "@prisma/client": "6.0.1",
9 | "zod": "3.23.8"
10 | },
11 | "devDependencies": {
12 | "@faker-js/faker": "9.0.2",
13 | "@types/node": "22.8.2",
14 | "prisma": "6.0.1",
15 | "ts-node": "10.9.2",
16 | "typescript": "5.6.2"
17 | },
18 | "prisma": {
19 | "seed": "ts-node prisma/seed.ts"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/json-field-types/prisma/migrations/20221212001724_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" TEXT NOT NULL,
4 | "email" TEXT NOT NULL,
5 | "profile" JSONB NOT NULL,
6 |
7 | CONSTRAINT "User_pkey" PRIMARY KEY ("id")
8 | );
9 |
10 | -- CreateIndex
11 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
12 |
--------------------------------------------------------------------------------
/json-field-types/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/json-field-types/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "postgresql"
7 | url = env("DATABASE_URL")
8 | }
9 |
10 | model User {
11 | id String @id @default(cuid())
12 | email String @unique
13 | profile Json
14 | }
15 |
--------------------------------------------------------------------------------
/json-field-types/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | import { faker } from "@faker-js/faker";
2 | import { PrismaClient } from "@prisma/client";
3 | import { Profile } from "../src/schemas";
4 |
5 | const prisma = new PrismaClient();
6 |
7 | async function main() {
8 | // Cleanup existing users
9 | await prisma.user.deleteMany({});
10 |
11 | // Create 100 random users
12 | await prisma.user.createMany({
13 | data: Array.from({ length: 100 }, () => ({
14 | email: faker.helpers.unique(faker.internet.email),
15 | profile: buildProfile(),
16 | })),
17 | });
18 |
19 | console.log(`Database has been seeded. 🌱`);
20 | }
21 |
22 | /** Creates a random `Profile` object which may have optional fields omitted */
23 | function buildProfile() {
24 | const profile: Profile = {};
25 |
26 | maybe(0.9, () => {
27 | profile.firstName = faker.name.firstName();
28 | });
29 | maybe(0.8, () => {
30 | profile.lastName = faker.name.lastName();
31 | });
32 | maybe(0.5, () => {
33 | profile.avatar = {
34 | url: faker.image.unsplash.people(),
35 | crop: {
36 | top: faker.datatype.float({ min: 0, max: 0.4, precision: 0.01 }),
37 | right: faker.datatype.float({ min: 0, max: 0.4, precision: 0.01 }),
38 | bottom: faker.datatype.float({ min: 0, max: 0.4, precision: 0.01 }),
39 | left: faker.datatype.float({ min: 0, max: 0.4, precision: 0.01 }),
40 | },
41 | };
42 | });
43 | maybe(0.8, () => {
44 | (profile.contactInfo ??= {}).email = faker.internet.email();
45 | });
46 | maybe(0.4, () => {
47 | (profile.contactInfo ??= {}).phone = faker.phone.number();
48 | });
49 | maybe(0.3, () => {
50 | (profile.contactInfo ??= {}).address = {
51 | street: faker.address.streetAddress(),
52 | city: faker.address.city(),
53 | region: faker.address.state(),
54 | postalCode: faker.address.zipCode(),
55 | country: faker.address.country(),
56 | };
57 | });
58 | maybe(0.5, () => {
59 | (profile.socialLinks ??= {}).twitter = faker.internet.url();
60 | });
61 | maybe(0.5, () => {
62 | (profile.socialLinks ??= {}).github = faker.internet.url();
63 | });
64 | maybe(0.5, () => {
65 | (profile.socialLinks ??= {}).website = faker.internet.url();
66 | });
67 | maybe(0.5, () => {
68 | (profile.socialLinks ??= {}).linkedin = faker.internet.url();
69 | });
70 |
71 | return Profile.parse(profile);
72 | }
73 |
74 | function maybe(probability: number, callback: () => T) {
75 | return faker.helpers.maybe(callback, { probability });
76 | }
77 |
78 | main()
79 | .then(async () => {
80 | await prisma.$disconnect();
81 | })
82 | .catch(async (e) => {
83 | console.error(e);
84 | await prisma.$disconnect();
85 | process.exit(1);
86 | });
87 |
--------------------------------------------------------------------------------
/json-field-types/src/index.ts:
--------------------------------------------------------------------------------
1 | import { Prisma, PrismaClient } from "@prisma/client";
2 | import { Profile } from "./schemas";
3 |
4 | const prisma = new PrismaClient().$extends({
5 | result: {
6 | user: {
7 | profile: {
8 | needs: { profile: true },
9 | compute({ profile }) {
10 | return Profile.parse(profile);
11 | },
12 | },
13 | },
14 | },
15 |
16 | query: {
17 | user: {
18 | create({ args, query }) {
19 | args.data.profile = Profile.parse(args.data.profile);
20 | return query(args);
21 | },
22 | createMany({ args, query }) {
23 | const users = Array.isArray(args.data) ? args.data : [args.data];
24 | for (const user of users) {
25 | user.profile = Profile.parse(user.profile);
26 | }
27 | return query(args);
28 | },
29 | update({ args, query }) {
30 | if (args.data.profile !== undefined) {
31 | args.data.profile = Profile.parse(args.data.profile);
32 | }
33 | return query(args);
34 | },
35 | updateMany({ args, query }) {
36 | if (args.data.profile !== undefined) {
37 | args.data.profile = Profile.parse(args.data.profile);
38 | }
39 | return query(args);
40 | },
41 | upsert({ args, query }) {
42 | args.create.profile = Profile.parse(args.create.profile);
43 | if (args.update.profile !== undefined) {
44 | args.update.profile = Profile.parse(args.update.profile);
45 | }
46 | return query(args);
47 | },
48 | },
49 | },
50 | });
51 |
52 | type User = Prisma.Result;
53 |
54 | async function main() {
55 | const users = await prisma.user.findMany({ take: 10 });
56 | users.forEach(renderUser);
57 | }
58 |
59 | function renderUser({
60 | id,
61 | email,
62 | profile: { firstName, lastName, avatar, contactInfo, socialLinks },
63 | }: User) {
64 | const card = [
65 | "===============================================================================",
66 | `User: ${email}`,
67 | `ID: ${id}`,
68 | "-------------------------------------------------------------------------------",
69 | (firstName || lastName) &&
70 | `Name: ${firstName ? firstName + " " : ""}${lastName || ""}`,
71 | avatar &&
72 | `
73 | Avatar:
74 | URL: ${avatar.url}
75 | Cropping Info:
76 | Top: ${(avatar.crop.top * 100).toFixed(0)}%
77 | Right: ${(avatar.crop.right * 100).toFixed(0)}%
78 | Bottom: ${(avatar.crop.bottom * 100).toFixed(0)}%
79 | Left: ${(avatar.crop.left * 100).toFixed(0)}%`,
80 | contactInfo && "\nContact Info:",
81 | contactInfo?.email && ` Email: ${contactInfo.email}`,
82 | contactInfo?.phone && ` Phone #: ${contactInfo.phone}`,
83 | contactInfo?.address &&
84 | ` Address: ${contactInfo.address.street}
85 | ${contactInfo.address.city}, ${contactInfo.address.region} ${contactInfo.address.postalCode}
86 | ${contactInfo.address.country}`,
87 | socialLinks && "\nSocial Links:",
88 | socialLinks?.website && ` Website: ${socialLinks.website}`,
89 | socialLinks?.twitter && ` Twitter: ${socialLinks.twitter}`,
90 | socialLinks?.github && ` GitHub: ${socialLinks.github}`,
91 | socialLinks?.linkedin && ` LinkedIn: ${socialLinks.linkedin}`,
92 | "===============================================================================\n\n",
93 | ]
94 | .filter((line) => line)
95 | .join("\n");
96 |
97 | console.info(card);
98 | }
99 |
100 | main()
101 | .then(async () => {
102 | await prisma.$disconnect();
103 | })
104 | .catch(async (e) => {
105 | console.error(e);
106 | await prisma.$disconnect();
107 | process.exit(1);
108 | });
109 |
--------------------------------------------------------------------------------
/json-field-types/src/schemas.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export type Avatar = z.infer;
4 | export const Avatar = z.object({
5 | url: z.string().url(),
6 | crop: z.object({
7 | top: z.number().min(0).max(1),
8 | right: z.number().min(0).max(1),
9 | bottom: z.number().min(0).max(1),
10 | left: z.number().min(0).max(1),
11 | }),
12 | });
13 |
14 | export type Address = z.infer;
15 | export const Address = z.object({
16 | street: z.string(),
17 | city: z.string(),
18 | region: z.string(),
19 | postalCode: z.string(),
20 | country: z.string(),
21 | });
22 |
23 | export type ContactInfo = z.infer;
24 | export const ContactInfo = z
25 | .object({
26 | email: z.string().email(),
27 | phone: z.string(),
28 | address: Address,
29 | })
30 | .partial();
31 |
32 | export type SocialLinks = z.infer;
33 | export const SocialLinks = z
34 | .object({
35 | twitter: z.string().url(),
36 | github: z.string().url(),
37 | website: z.string().url(),
38 | linkedin: z.string().url(),
39 | })
40 | .partial();
41 |
42 | export type Profile = z.infer;
43 | export const Profile = z
44 | .object({
45 | firstName: z.string(),
46 | lastName: z.string(),
47 | avatar: Avatar,
48 | contactInfo: ContactInfo,
49 | socialLinks: SocialLinks,
50 | })
51 | .partial();
52 |
--------------------------------------------------------------------------------
/json-field-types/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "exclude": ["node_modules"],
4 | "compilerOptions": {
5 | "noEmit": true,
6 | "lib": ["ES2019"],
7 | "isolatedModules": true,
8 | "esModuleInterop": true,
9 | "module": "CommonJS",
10 | "moduleResolution": "node",
11 | "resolveJsonModule": true,
12 | "target": "ES2019",
13 | "strict": true,
14 | "allowJs": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "skipLibCheck": true
17 | },
18 | "ts-node": {
19 | "compilerOptions": {
20 | "module": "commonjs"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/model-filters/README.md:
--------------------------------------------------------------------------------
1 | # Prisma Client Extension - Model Filters
2 |
3 | This example demonstrates a Prisma Client extension which adds reusable filters for a model which can be composed and passed to a `where` condition. Complex, frequently used filtering conditions can be written once and accessed in many queries through the extended Prisma Client instance.
4 |
5 | ## Caveats
6 |
7 | This extension is provided as an example only. It is not intended to be used in production environments.
8 |
9 | Please read [the documentation on `model` extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/model) for more information.
10 |
11 | ## How to use
12 |
13 | ### Prerequisites
14 |
15 | - Install [Node.js](https://nodejs.org/en/download/)
16 |
17 | ### 1. Download example & install dependencies
18 |
19 | Clone this repository:
20 |
21 | ```sh
22 | git clone git@github.com:sbking/prisma-client-extensions.git
23 | ```
24 |
25 | Install dependencies:
26 |
27 | ```sh
28 | cd model-filters
29 | npm install
30 | ```
31 |
32 | ### 2. Create an SQLite database and run migrations
33 |
34 | Run the following command. An SQLite database will be created automatically:
35 |
36 | ```sh
37 | npx prisma migrate deploy
38 | ```
39 |
40 | ### 3. Seed the database
41 |
42 | Run the following command to add seed data to the database:
43 |
44 | ```sh
45 | npx prisma db seed
46 | ```
47 |
48 | ### 4. Run the `dev` script
49 |
50 | To run the `script.ts` file, run the following command:
51 |
52 | ```sh
53 | npm run dev
54 | ```
55 |
--------------------------------------------------------------------------------
/model-filters/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "model-filters",
3 | "private": true,
4 | "scripts": {
5 | "dev": "ts-node script.ts"
6 | },
7 | "dependencies": {
8 | "@prisma/client": "6.0.1",
9 | "cuid": "3.0.0",
10 | "date-fns": "4.1.0"
11 | },
12 | "devDependencies": {
13 | "@faker-js/faker": "9.0.2",
14 | "@types/node": "22.8.2",
15 | "prisma": "6.0.1",
16 | "ts-node": "10.9.2",
17 | "typescript": "5.6.2"
18 | },
19 | "prisma": {
20 | "seed": "ts-node prisma/seed.ts"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/model-filters/prisma/migrations/20221212025553_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" TEXT NOT NULL PRIMARY KEY,
4 | "firstName" TEXT NOT NULL,
5 | "lastName" TEXT NOT NULL,
6 | "email" TEXT NOT NULL
7 | );
8 |
9 | -- CreateTable
10 | CREATE TABLE "Post" (
11 | "id" TEXT NOT NULL PRIMARY KEY,
12 | "title" TEXT NOT NULL,
13 | "published" BOOLEAN NOT NULL,
14 | "authorId" TEXT NOT NULL,
15 | CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
16 | );
17 |
18 | -- CreateTable
19 | CREATE TABLE "Comment" (
20 | "id" TEXT NOT NULL PRIMARY KEY,
21 | "text" TEXT NOT NULL,
22 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
23 | "commenterId" TEXT NOT NULL,
24 | "postId" TEXT NOT NULL,
25 | CONSTRAINT "Comment_commenterId_fkey" FOREIGN KEY ("commenterId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
26 | CONSTRAINT "Comment_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
27 | );
28 |
--------------------------------------------------------------------------------
/model-filters/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "sqlite"
--------------------------------------------------------------------------------
/model-filters/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "sqlite"
7 | url = "file:./dev.db"
8 | }
9 |
10 | model User {
11 | id String @id @default(cuid())
12 | firstName String
13 | lastName String
14 | email String
15 |
16 | posts Post[]
17 | comments Comment[]
18 | }
19 |
20 | model Post {
21 | id String @id @default(cuid())
22 | title String
23 | published Boolean
24 | authorId String
25 |
26 | author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
27 | comments Comment[]
28 | }
29 |
30 | model Comment {
31 | id String @id @default(cuid())
32 | text String
33 | createdAt DateTime @default(now())
34 | commenterId String
35 | postId String
36 |
37 | commenter User @relation(fields: [commenterId], references: [id], onDelete: Cascade)
38 | post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
39 | }
40 |
--------------------------------------------------------------------------------
/model-filters/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | import { faker } from "@faker-js/faker";
2 | import { Prisma, PrismaClient } from "@prisma/client";
3 | import cuid from "cuid";
4 |
5 | const prisma = new PrismaClient();
6 |
7 | async function main() {
8 | // Cleanup existing users
9 | await prisma.user.deleteMany({});
10 |
11 | // Create 100 random users
12 | const users = Array.from({ length: 100 }, () => ({
13 | id: cuid(),
14 | firstName: faker.name.firstName(),
15 | lastName: faker.name.lastName(),
16 | email: faker.internet.email(),
17 | })) satisfies Prisma.UserCreateInput[];
18 |
19 | // Create 40 posts with random authors and comments
20 | const posts = Array.from({ length: 40 }, () => ({
21 | title: faker.hacker.phrase(),
22 | published: faker.datatype.boolean(),
23 | authorId: faker.helpers.arrayElement(users).id,
24 | comments: {
25 | create: Array.from(
26 | { length: faker.datatype.number({ min: 0, max: 5 }) },
27 | () => ({
28 | text: faker.lorem.paragraph(),
29 | commenterId: faker.helpers.arrayElement(users).id,
30 | createdAt: faker.date.recent(365),
31 | })
32 | ),
33 | },
34 | })) satisfies Prisma.PostUncheckedCreateInput[];
35 |
36 | // Seed the database
37 | for (const user of users) {
38 | await prisma.user.create({ data: user });
39 | }
40 | for (const post of posts) {
41 | await prisma.post.create({ data: post });
42 | }
43 |
44 | console.log(`Database has been seeded. 🌱`);
45 | }
46 |
47 | main()
48 | .then(async () => {
49 | await prisma.$disconnect();
50 | })
51 | .catch(async (e) => {
52 | console.error(e);
53 | await prisma.$disconnect();
54 | process.exit(1);
55 | });
56 |
--------------------------------------------------------------------------------
/model-filters/script.ts:
--------------------------------------------------------------------------------
1 | import { Prisma, PrismaClient } from "@prisma/client";
2 | import { sub } from "date-fns";
3 |
4 | const prisma = new PrismaClient({ log: ["query"] }).$extends({
5 | model: {
6 | post: {
7 | unpublished: () => ({ published: false }),
8 | published: () => ({ published: true }),
9 | byAuthor: (authorId: string) => ({ authorId }),
10 | byAuthorDomain: (domain: string) => ({
11 | author: { email: { endsWith: `@${domain}` } },
12 | }),
13 | hasComments: () => ({ comments: { some: {} } }),
14 | hasRecentComments: (date: Date) => ({
15 | comments: { some: { createdAt: { gte: date } } },
16 | }),
17 | titleContains: (search: string) => ({ title: { contains: search } }),
18 | } satisfies Record Prisma.PostWhereInput>,
19 | },
20 | });
21 |
22 | async function main() {
23 | const posts = await prisma.post.findMany({
24 | where: {
25 | AND: [
26 | prisma.post.published(),
27 | prisma.post.byAuthorDomain("prisma.io"),
28 | prisma.post.hasRecentComments(sub(new Date(), { weeks: 1 })),
29 | prisma.post.titleContains("GraphQL"),
30 | ],
31 | },
32 | });
33 | }
34 |
35 | main()
36 | .then(async () => {
37 | await prisma.$disconnect();
38 | })
39 | .catch(async (e) => {
40 | console.error(e);
41 | await prisma.$disconnect();
42 | process.exit(1);
43 | });
44 |
--------------------------------------------------------------------------------
/model-filters/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "exclude": ["node_modules"],
4 | "compilerOptions": {
5 | "noEmit": true,
6 | "lib": ["ES2019"],
7 | "isolatedModules": true,
8 | "esModuleInterop": true,
9 | "module": "CommonJS",
10 | "moduleResolution": "node",
11 | "resolveJsonModule": true,
12 | "target": "ES2019",
13 | "strict": true,
14 | "allowJs": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "skipLibCheck": true
17 | },
18 | "ts-node": {
19 | "compilerOptions": {
20 | "module": "commonjs"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/obfuscated-fields/README.md:
--------------------------------------------------------------------------------
1 | # Prisma Client Extension - Obfuscated Fields
2 |
3 | This example uses an extension to obfuscate a sensitive `password` field on a `User` model. The `password` column is not included in selected columns in the underlying SQL queries, and it will resolve to `undefined` when accessed on a user result object. It could also resolve to any other value, such as an obfuscated string like `"********"`.
4 |
5 | ## Caveats
6 |
7 | This extension is provided as an example only. It is not intended to be used in production environments.
8 |
9 | Please read [the documentation on `result` extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/result) for more information.
10 |
11 | ## How to use
12 |
13 | ### Prerequisites
14 |
15 | - Install [Node.js](https://nodejs.org/en/download/)
16 |
17 | ### 1. Download example & install dependencies
18 |
19 | Clone this repository:
20 |
21 | ```sh
22 | git clone git@github.com:sbking/prisma-client-extensions.git
23 | ```
24 |
25 | Install dependencies:
26 |
27 | ```sh
28 | cd obfuscated-fields
29 | npm install
30 | ```
31 |
32 | ### 2. Create an SQLite database and run migrations
33 |
34 | Run the following command. An SQLite database will be created automatically:
35 |
36 | ```sh
37 | npx prisma migrate deploy
38 | ```
39 |
40 | ### 3. Seed the database
41 |
42 | Run the following command to add seed data to the database:
43 |
44 | ```sh
45 | npx prisma db seed
46 | ```
47 |
48 | ### 4. Run the `dev` script
49 |
50 | To run the `script.ts` file, run the following command:
51 |
52 | ```sh
53 | npm run dev
54 | ```
55 |
--------------------------------------------------------------------------------
/obfuscated-fields/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "obfuscated-fields",
3 | "private": true,
4 | "scripts": {
5 | "dev": "ts-node script.ts"
6 | },
7 | "dependencies": {
8 | "@prisma/client": "6.0.1",
9 | "bcryptjs": "3.0.2"
10 | },
11 | "devDependencies": {
12 | "@faker-js/faker": "9.0.2",
13 | "@types/bcryptjs": "2.4.6",
14 | "@types/node": "22.8.2",
15 | "prisma": "6.0.1",
16 | "ts-node": "10.9.2",
17 | "typescript": "5.6.2"
18 | },
19 | "prisma": {
20 | "seed": "ts-node prisma/seed.ts"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/obfuscated-fields/prisma/migrations/20221209223744_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" TEXT NOT NULL PRIMARY KEY,
4 | "email" TEXT NOT NULL,
5 | "password" TEXT NOT NULL
6 | );
7 |
--------------------------------------------------------------------------------
/obfuscated-fields/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "sqlite"
--------------------------------------------------------------------------------
/obfuscated-fields/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "sqlite"
7 | url = "file:./dev.db"
8 | }
9 |
10 | model User {
11 | id String @id @default(cuid())
12 | email String
13 | password String
14 | }
15 |
--------------------------------------------------------------------------------
/obfuscated-fields/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | import bcrypt from "bcryptjs";
2 | import { faker } from "@faker-js/faker";
3 | import { PrismaClient } from "@prisma/client";
4 |
5 | const prisma = new PrismaClient();
6 |
7 | async function main() {
8 | // Cleanup existing users
9 | await prisma.user.deleteMany({});
10 |
11 | // Create a random user
12 | await prisma.user.create({
13 | data: {
14 | email: faker.internet.email(),
15 | password: await bcrypt.hash(faker.internet.password(), 10),
16 | },
17 | });
18 |
19 | console.log(`Database has been seeded. 🌱`);
20 | }
21 |
22 | main()
23 | .then(async () => {
24 | await prisma.$disconnect();
25 | })
26 | .catch(async (e) => {
27 | console.error(e);
28 | await prisma.$disconnect();
29 | process.exit(1);
30 | });
31 |
--------------------------------------------------------------------------------
/obfuscated-fields/script.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | const prisma = new PrismaClient().$extends({
4 | result: {
5 | user: {
6 | password: {
7 | needs: {},
8 | compute() {
9 | return undefined;
10 | },
11 | },
12 | },
13 | },
14 | });
15 |
16 | async function main() {
17 | const user = await prisma.user.findFirstOrThrow();
18 | console.info("Email: ", user.email);
19 | console.info("Password: ", user.password);
20 | }
21 |
22 | main()
23 | .then(async () => {
24 | await prisma.$disconnect();
25 | })
26 | .catch(async (e) => {
27 | console.error(e);
28 | await prisma.$disconnect();
29 | process.exit(1);
30 | });
31 |
--------------------------------------------------------------------------------
/obfuscated-fields/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "exclude": ["node_modules"],
4 | "compilerOptions": {
5 | "noEmit": true,
6 | "lib": ["ES2019"],
7 | "isolatedModules": true,
8 | "esModuleInterop": true,
9 | "module": "CommonJS",
10 | "moduleResolution": "node",
11 | "resolveJsonModule": true,
12 | "target": "ES2019",
13 | "strict": true,
14 | "allowJs": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "skipLibCheck": true
17 | },
18 | "ts-node": {
19 | "compilerOptions": {
20 | "module": "commonjs"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/query-logging/README.md:
--------------------------------------------------------------------------------
1 | # Prisma Client Extension - Query Logging
2 |
3 | This example shows how to use Prisma Client extensions to perform similar tasks as [middleware](https://www.prisma.io/docs/concepts/components/prisma-client/middleware). In this example, a `query` extension tracks the time it takes to fulfill each query, and logs the results along with the query and arguments themselves.
4 |
5 | This technique could be used to perform generic logging, emit events, track usage, etc.
6 |
7 | ## Caveats
8 |
9 | > **NOTE**: The [OpenTelemetry tracing](https://www.prisma.io/docs/concepts/components/prisma-client/opentelemetry-tracing) and [Metrics](https://www.prisma.io/docs/concepts/components/prisma-client/metrics) features (currently in [Preview](https://www.prisma.io/docs/about/prisma/releases#preview)) can provide detailed insights into performance and how Prisma interacts with the database. This example shows how to use Prisma Client extensions to perform actions before or after queries, and is not a fully-featured logging / tracing solution.
10 |
11 | This extension is provided as an example only. It is not intended to be used in production environments.
12 |
13 | Please read [the documentation on `query` extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/query) for more information.
14 |
15 | ## How to use
16 |
17 | ### Prerequisites
18 |
19 | - Install [Node.js](https://nodejs.org/en/download/)
20 |
21 | ### 1. Download example & install dependencies
22 |
23 | Clone this repository:
24 |
25 | ```sh
26 | git clone git@github.com:sbking/prisma-client-extensions.git
27 | ```
28 |
29 | Install dependencies:
30 |
31 | ```sh
32 | cd query-logging
33 | npm install
34 | ```
35 |
36 | ### 2. Create an SQLite database and run migrations
37 |
38 | Run the following command. An SQLite database will be created automatically:
39 |
40 | ```sh
41 | npx prisma migrate deploy
42 | ```
43 |
44 | ### 3. Seed the database
45 |
46 | Run the following command to add seed data to the database:
47 |
48 | ```sh
49 | npx prisma db seed
50 | ```
51 |
52 | ### 4. Run the `dev` script
53 |
54 | To run the `script.ts` file, run the following command:
55 |
56 | ```sh
57 | npm run dev
58 | ```
59 |
--------------------------------------------------------------------------------
/query-logging/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "query-logging",
3 | "private": true,
4 | "scripts": {
5 | "dev": "ts-node script.ts"
6 | },
7 | "dependencies": {
8 | "@prisma/client": "6.0.1"
9 | },
10 | "devDependencies": {
11 | "@faker-js/faker": "9.0.2",
12 | "@types/node": "22.8.2",
13 | "prisma": "6.0.1",
14 | "ts-node": "10.9.2",
15 | "typescript": "5.6.2"
16 | },
17 | "prisma": {
18 | "seed": "ts-node prisma/seed.ts"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/query-logging/prisma/migrations/20221211185834_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" TEXT NOT NULL PRIMARY KEY,
4 | "firstName" TEXT NOT NULL,
5 | "lastName" TEXT NOT NULL
6 | );
7 |
--------------------------------------------------------------------------------
/query-logging/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "sqlite"
--------------------------------------------------------------------------------
/query-logging/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "sqlite"
7 | url = "file:./dev.db"
8 | }
9 |
10 | model User {
11 | id String @id @default(cuid())
12 | firstName String
13 | lastName String
14 | }
15 |
--------------------------------------------------------------------------------
/query-logging/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | import { faker } from "@faker-js/faker";
2 | import { Prisma, PrismaClient } from "@prisma/client";
3 |
4 | const prisma = new PrismaClient();
5 |
6 | async function main() {
7 | // Cleanup existing users
8 | await prisma.user.deleteMany({});
9 |
10 | // Create 100 random users
11 | const users = Array.from({ length: 100 }, () => ({
12 | firstName: faker.name.firstName(),
13 | lastName: faker.name.lastName(),
14 | })) satisfies Prisma.UserCreateInput[];
15 |
16 | // Seed the database
17 | for (const user of users) {
18 | await prisma.user.create({ data: user });
19 | }
20 |
21 | console.log(`Database has been seeded. 🌱`);
22 | }
23 |
24 | main()
25 | .then(async () => {
26 | await prisma.$disconnect();
27 | })
28 | .catch(async (e) => {
29 | console.error(e);
30 | await prisma.$disconnect();
31 | process.exit(1);
32 | });
33 |
--------------------------------------------------------------------------------
/query-logging/script.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 | import { performance } from "perf_hooks";
3 | import * as util from "util";
4 |
5 | const prisma = new PrismaClient().$extends({
6 | query: {
7 | $allModels: {
8 | async $allOperations({ operation, model, args, query }) {
9 | const start = performance.now();
10 | const result = await query(args);
11 | const end = performance.now();
12 | const time = end - start;
13 | console.log(
14 | util.inspect(
15 | { model, operation, args, time },
16 | { showHidden: false, depth: null, colors: true }
17 | )
18 | );
19 | return result;
20 | },
21 | },
22 | },
23 | });
24 |
25 | async function main() {
26 | await prisma.user.findMany({
27 | orderBy: [{ lastName: "asc" }, { firstName: "asc" }],
28 | take: 5,
29 | });
30 |
31 | await prisma.user.groupBy({ by: ["lastName"], _count: true });
32 | }
33 |
34 | main()
35 | .then(async () => {
36 | await prisma.$disconnect();
37 | })
38 | .catch(async (e) => {
39 | console.error(e);
40 | await prisma.$disconnect();
41 | process.exit(1);
42 | });
43 |
--------------------------------------------------------------------------------
/query-logging/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "exclude": ["node_modules"],
4 | "compilerOptions": {
5 | "noEmit": true,
6 | "lib": ["ES2019"],
7 | "isolatedModules": true,
8 | "esModuleInterop": true,
9 | "module": "CommonJS",
10 | "moduleResolution": "node",
11 | "resolveJsonModule": true,
12 | "target": "ES2019",
13 | "strict": true,
14 | "allowJs": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "skipLibCheck": true
17 | },
18 | "ts-node": {
19 | "compilerOptions": {
20 | "module": "commonjs"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/readonly-client/README.md:
--------------------------------------------------------------------------------
1 | # Prisma Client Extension - Readonly Client
2 |
3 | This example creates a client that only allows read operations like `findMany` and `count`, not write operations like `create` or `update`. Calling write operations will result in an error at runtime and at compile time with TypeScript.
4 |
5 | ## Caveats
6 |
7 | This extension is provided as an example only. It is not intended to be used in production environments.
8 |
9 | Please read [the documentation on `query` extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/query) for more information.
10 |
11 | ## How to use
12 |
13 | ### Prerequisites
14 |
15 | - Install [Node.js](https://nodejs.org/en/download/)
16 |
17 | ### 1. Download example & install dependencies
18 |
19 | Clone this repository:
20 |
21 | ```sh
22 | git clone git@github.com:sbking/prisma-client-extensions.git
23 | ```
24 |
25 | Install dependencies:
26 |
27 | ```sh
28 | cd readonly-client
29 | npm install
30 | ```
31 |
32 | ### 2. Create an SQLite database and run migrations
33 |
34 | Run the following command. An SQLite database will be created automatically:
35 |
36 | ```sh
37 | npx prisma migrate deploy
38 | ```
39 |
40 | ### 3. Seed the database
41 |
42 | Run the following command to add seed data to the database:
43 |
44 | ```sh
45 | npx prisma db seed
46 | ```
47 |
48 | ### 4. Run the `dev` script
49 |
50 | To run the `script.ts` file, run the following command:
51 |
52 | ```sh
53 | npm run dev
54 | ```
55 |
--------------------------------------------------------------------------------
/readonly-client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "readonly-client",
3 | "private": true,
4 | "scripts": {
5 | "dev": "ts-node script.ts"
6 | },
7 | "dependencies": {
8 | "@prisma/client": "6.0.1"
9 | },
10 | "devDependencies": {
11 | "@faker-js/faker": "9.0.2",
12 | "@types/node": "22.8.2",
13 | "prisma": "6.0.1",
14 | "ts-node": "10.9.2",
15 | "typescript": "5.6.2"
16 | },
17 | "prisma": {
18 | "seed": "ts-node prisma/seed.ts"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/readonly-client/prisma/migrations/20221209213844_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "Post" (
3 | "id" TEXT NOT NULL PRIMARY KEY,
4 | "title" TEXT NOT NULL,
5 | "published" BOOLEAN NOT NULL
6 | );
7 |
--------------------------------------------------------------------------------
/readonly-client/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "sqlite"
--------------------------------------------------------------------------------
/readonly-client/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "sqlite"
7 | url = "file:./dev.db"
8 | }
9 |
10 | model Post {
11 | id String @id @default(cuid())
12 | title String
13 | published Boolean
14 | }
15 |
--------------------------------------------------------------------------------
/readonly-client/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | import { faker } from "@faker-js/faker";
2 | import { Prisma, PrismaClient } from "@prisma/client";
3 |
4 | const prisma = new PrismaClient();
5 |
6 | async function main() {
7 | // Cleanup existing users
8 | await prisma.post.deleteMany({});
9 |
10 | // Create 100 random posts
11 | const posts = Array.from({ length: 100 }, () => ({
12 | title: faker.hacker.phrase(),
13 | published: faker.datatype.boolean(),
14 | })) satisfies Prisma.PostCreateInput[];
15 |
16 | // Seed the database
17 | for (const post of posts) {
18 | await prisma.post.create({ data: post });
19 | }
20 |
21 | console.log(`Database has been seeded. 🌱`);
22 | }
23 |
24 | main()
25 | .then(async () => {
26 | await prisma.$disconnect();
27 | })
28 | .catch(async (e) => {
29 | console.error(e);
30 | await prisma.$disconnect();
31 | process.exit(1);
32 | });
33 |
--------------------------------------------------------------------------------
/readonly-client/script.ts:
--------------------------------------------------------------------------------
1 | import { Prisma, PrismaClient } from "@prisma/client";
2 |
3 | const WRITE_METHODS = [
4 | "create",
5 | "update",
6 | "upsert",
7 | "delete",
8 | "createMany",
9 | "createManyAndReturn",
10 | "updateMany",
11 | "deleteMany",
12 | ] as const;
13 |
14 | const GLOBAL_WRITE_METHODS = [
15 | '$executeRaw',
16 | '$queryRawUnsafe',
17 | '$executeRawUnsafe',
18 | '$runCommandRaw',
19 | ] as const;
20 |
21 | const ReadonlyClient = Prisma.defineExtension({
22 | name: "ReadonlyClient",
23 | model: {
24 | $allModels: Object.fromEntries(
25 | WRITE_METHODS.map((method) => [
26 | method,
27 | function (args: never) {
28 | throw new Error(
29 | `Calling the \`${method}\` method on a readonly client is not allowed`
30 | );
31 | },
32 | ])
33 | ) as {
34 | [K in typeof WRITE_METHODS[number]]: (
35 | args: `Calling the \`${K}\` method on a readonly client is not allowed`
36 | ) => never;
37 | },
38 | },
39 | query: Object.fromEntries(
40 | GLOBAL_WRITE_METHODS.map((method) => [
41 | method,
42 | function (args: never) {
43 | throw new Error(`Calling the \`${method}\` method on a readonly client is not allowed`);
44 | }
45 | ])) as {
46 | [K in typeof GLOBAL_WRITE_METHODS[number]]: (args: `Calling the \`${K}\` method on a readonly client is not allowed`) => never;
47 | }
48 | });
49 |
50 | const prisma = new PrismaClient();
51 | const readonlyPrisma = prisma.$extends(ReadonlyClient);
52 |
53 | async function main() {
54 | const posts = await readonlyPrisma.post.findMany({ take: 5 });
55 | console.log(posts);
56 |
57 | // @ts-expect-error:
58 | // Argument of type '{ data: { title: string; published: boolean; }; }'
59 | // is not assignable to parameter of type '"Calling the `create` method
60 | // on a readonly client is not allowed"'.
61 | await readonlyPrisma.post.create({
62 | data: { title: "New post", published: false },
63 | });
64 |
65 | await readonlyPrisma.$executeRaw`INSERT INTO post(id,title, published) VALUES(12345,'New post', false)`
66 | }
67 |
68 | main()
69 | .then(async () => {
70 | await prisma.$disconnect();
71 | })
72 | .catch(async (e) => {
73 | console.error(e);
74 | await prisma.$disconnect();
75 | process.exit(1);
76 | });
77 |
--------------------------------------------------------------------------------
/readonly-client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "exclude": ["node_modules"],
4 | "compilerOptions": {
5 | "noEmit": true,
6 | "lib": ["ES2019"],
7 | "isolatedModules": true,
8 | "esModuleInterop": true,
9 | "module": "CommonJS",
10 | "moduleResolution": "node",
11 | "resolveJsonModule": true,
12 | "target": "ES2019",
13 | "strict": true,
14 | "allowJs": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "skipLibCheck": true
17 | },
18 | "ts-node": {
19 | "compilerOptions": {
20 | "module": "commonjs"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": ["config:base", ":disableRateLimiting", "docker:disable"],
4 | "automerge": true,
5 | "major": {
6 | "automerge": false
7 | },
8 | "rangeStrategy": "pin",
9 | "baseBranches": ["main"],
10 | "reviewers": ["jharrell"],
11 | "packageRules": [
12 | {
13 | "baseBranchList": ["latest"],
14 | "packageNames": ["prisma", "@prisma/client"],
15 | "enabled": true,
16 | "updateTypes": ["major"]
17 | }
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/retry-transactions/.env.example:
--------------------------------------------------------------------------------
1 | DATABASE_URL="postgresql://postgres:postgres@localhost:6001/postgres"
2 |
--------------------------------------------------------------------------------
/retry-transactions/README.md:
--------------------------------------------------------------------------------
1 | # Prisma Client Extension - Retry Transactions
2 |
3 | This example shows how to use a Prisma Client extension to automatically retry transactions that fail due to a write conflict / deadlock timeout. Failed transactions will be retried with [exponential backoff and jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) to spread out the database load under heavy traffic contention.
4 |
5 | ## Caveats
6 |
7 | > **NOTE**: This extension overwrites the `$transaction` method on a Prisma Client instance, and relies on this method being defined on a generated Prisma Client. It may not currently have correct type definitions when packaged and distributed as a reusable extension.
8 |
9 | This extension is provided as an example only. It is not intended to be used in production environments.
10 |
11 | Please read [the documentation on `client` extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/client) for more information.
12 |
13 | ## How to use
14 |
15 | ### Prerequisites
16 |
17 | - Install [Node.js](https://nodejs.org/en/download/)
18 | - Install [Docker](https://docs.docker.com/get-docker/)
19 |
20 | ### 1. Download example & install dependencies
21 |
22 | Clone this repository:
23 |
24 | ```sh
25 | git clone git@github.com:sbking/prisma-client-extensions.git
26 | ```
27 |
28 | Create a `.env` file and install dependencies:
29 |
30 | ```sh
31 | cd retry-transactions
32 | cp .env.example .env
33 | npm install
34 | ```
35 |
36 | ### 2. Start the database
37 |
38 | Run the following command to start a new Postgres database in a Docker container:
39 |
40 | ```sh
41 | docker compose up -d
42 | ```
43 |
44 | ### 3. Run migrations
45 |
46 | Run this command to apply migrations to the database:
47 |
48 | ```sh
49 | npx prisma migrate deploy
50 | ```
51 |
52 | ### 4. Seed the database
53 |
54 | Run the following command to add seed data to the database:
55 |
56 | ```sh
57 | npx prisma db seed
58 | ```
59 |
60 | ### 5. Run the `dev` script
61 |
62 | To run the `script.ts` file, run the following command:
63 |
64 | ```sh
65 | npm run dev
66 | ```
67 |
--------------------------------------------------------------------------------
/retry-transactions/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.7"
2 | services:
3 | postgres:
4 | image: postgres:latest
5 | restart: always
6 | environment:
7 | - POSTGRES_USER=postgres
8 | - POSTGRES_PASSWORD=postgres
9 | - POSTGRES_DB=postgres
10 | ports:
11 | - "6001:5432"
12 | volumes:
13 | - postgres-data:/var/lib/postgresql/data
14 |
15 | volumes:
16 | postgres-data:
17 |
--------------------------------------------------------------------------------
/retry-transactions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "retry-transactions",
3 | "private": true,
4 | "scripts": {
5 | "dev": "ts-node script.ts"
6 | },
7 | "dependencies": {
8 | "@prisma/client": "6.0.1",
9 | "exponential-backoff": "3.1.1"
10 | },
11 | "devDependencies": {
12 | "@faker-js/faker": "9.0.2",
13 | "@types/node": "22.8.2",
14 | "prisma": "6.0.1",
15 | "ts-node": "10.9.2",
16 | "typescript": "5.6.2"
17 | },
18 | "prisma": {
19 | "seed": "ts-node prisma/seed.ts"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/retry-transactions/prisma/migrations/20221211193956_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" TEXT NOT NULL,
4 | "firstName" TEXT NOT NULL,
5 | "lastName" TEXT NOT NULL,
6 |
7 | CONSTRAINT "User_pkey" PRIMARY KEY ("id")
8 | );
9 |
--------------------------------------------------------------------------------
/retry-transactions/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/retry-transactions/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "postgresql"
7 | url = env("DATABASE_URL")
8 | }
9 |
10 | model User {
11 | id String @id @default(cuid())
12 | firstName String
13 | lastName String
14 | }
15 |
--------------------------------------------------------------------------------
/retry-transactions/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | import { faker } from "@faker-js/faker";
2 | import { Prisma, PrismaClient } from "@prisma/client";
3 |
4 | const prisma = new PrismaClient();
5 |
6 | async function main() {
7 | // Cleanup existing users
8 | await prisma.user.deleteMany({});
9 |
10 | // Create 100 random users
11 | await prisma.user.createMany({
12 | data: Array.from({ length: 100 }, () => ({
13 | firstName: faker.name.firstName(),
14 | lastName: faker.name.lastName(),
15 | })),
16 | });
17 |
18 | console.log(`Database has been seeded. 🌱`);
19 | }
20 |
21 | main()
22 | .then(async () => {
23 | await prisma.$disconnect();
24 | })
25 | .catch(async (e) => {
26 | console.error(e);
27 | await prisma.$disconnect();
28 | process.exit(1);
29 | });
30 |
--------------------------------------------------------------------------------
/retry-transactions/script.ts:
--------------------------------------------------------------------------------
1 | import { backOff, IBackOffOptions } from "exponential-backoff";
2 | import { Prisma, PrismaClient } from "@prisma/client";
3 |
4 | function RetryTransactions(options?: Partial) {
5 | return Prisma.defineExtension((prisma) =>
6 | prisma.$extends({
7 | client: {
8 | $transaction(...args: any) {
9 | return backOff(() => prisma.$transaction.apply(prisma, args), {
10 | retry: (e) => {
11 | // Retry the transaction only if the error was due to a write conflict or deadlock
12 | // See: https://www.prisma.io/docs/reference/api-reference/error-reference#p2034
13 | return e.code === "P2034";
14 | },
15 | ...options,
16 | });
17 | },
18 | } as { $transaction: typeof prisma["$transaction"] },
19 | })
20 | );
21 | }
22 |
23 | const prisma = new PrismaClient().$extends(
24 | RetryTransactions({
25 | jitter: "full",
26 | numOfAttempts: 5,
27 | })
28 | );
29 |
30 | async function main() {
31 | const before = await prisma.user.findFirstOrThrow();
32 | console.log("Before: ", before);
33 |
34 | await Promise.allSettled([
35 | firstTransaction(before.id),
36 | secondTransaction(before.id),
37 | ]);
38 |
39 | const after = await prisma.user.findUniqueOrThrow({
40 | where: { id: before.id },
41 | });
42 | console.log("After: ", after);
43 | }
44 |
45 | // Runs a read-modify-write transaction, where the "modify" step takes 2000ms,
46 | // enough time for the second transaction to cause a serialization failure
47 | async function firstTransaction(id: string) {
48 | await prisma.$transaction(
49 | async (tx) => {
50 | console.log("First transaction started");
51 | try {
52 | // Read
53 | const user = await tx.user.findUniqueOrThrow({ where: { id } });
54 | // "Modify"
55 | await sleep(2000);
56 | // Write
57 | await tx.user.update({ where: { id }, data: { firstName: "Albert" } });
58 | } catch (e) {
59 | console.log("First transaction failed");
60 | throw e;
61 | }
62 | },
63 | { isolationLevel: Prisma.TransactionIsolationLevel.Serializable }
64 | );
65 | console.log("First transaction committed");
66 | }
67 |
68 | // Waits 1000ms, then writes to the database, causing a conflict with the first transaction
69 | async function secondTransaction(id: string) {
70 | await sleep(1000);
71 |
72 | await prisma.$transaction(
73 | async (tx) => {
74 | console.log("Second transaction started");
75 | try {
76 | await tx.user.update({ where: { id }, data: { firstName: "Beto" } });
77 | } catch (e) {
78 | console.log("Second transaction failed");
79 | throw e;
80 | }
81 | },
82 | { isolationLevel: Prisma.TransactionIsolationLevel.Serializable }
83 | );
84 | console.log("Second transaction committed");
85 | }
86 |
87 | function sleep(ms: number) {
88 | return new Promise((resolve) => setTimeout(resolve, ms));
89 | }
90 |
91 | main()
92 | .then(async () => {
93 | await prisma.$disconnect();
94 | })
95 | .catch(async (e) => {
96 | console.error(e);
97 | await prisma.$disconnect();
98 | process.exit(1);
99 | });
100 |
--------------------------------------------------------------------------------
/retry-transactions/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "exclude": ["node_modules"],
4 | "compilerOptions": {
5 | "noEmit": true,
6 | "lib": ["ES2019"],
7 | "isolatedModules": true,
8 | "esModuleInterop": true,
9 | "module": "CommonJS",
10 | "moduleResolution": "node",
11 | "resolveJsonModule": true,
12 | "target": "ES2019",
13 | "strict": true,
14 | "allowJs": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "skipLibCheck": true
17 | },
18 | "ts-node": {
19 | "compilerOptions": {
20 | "module": "commonjs"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/row-level-security/.env.example:
--------------------------------------------------------------------------------
1 | DATABASE_URL="postgresql://app:s3cret@localhost:6002/app"
2 |
--------------------------------------------------------------------------------
/row-level-security/README.md:
--------------------------------------------------------------------------------
1 | # Prisma Client Extension - Row Level Security
2 |
3 | This example shows how to use a Prisma Client extension to isolate data between tenants in a multi-tenant app using Row Level Security (RLS) in Postgres.
4 |
5 | ## Caveats
6 |
7 | > **NOTE**: Because this example extension wraps every query in a new batch transaction, explicitly running transactions with `companyPrisma.$transaction()` may not work as intended. In a future version of Prisma Client, `query` extensions will have access to information about whether they are run inside a transaction, similar to [the `runInTransaction` parameter provided to Prisma middleware](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#params). When this is available, this example will be updated to work for queries run inside explicit transactions.
8 |
9 | This extension is provided as an example only. It is not intended to be used in production environments.
10 |
11 | Please read [the documentation on `query` extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/query) for more information.
12 |
13 | ## Background
14 |
15 | In a multi-tenant application, the data for multiple tenants (customers, companies, users, etc) is stored in a shared database. This approach can reduce costs and simplify infrastructure. However, it is important to ensure that the data for separate tenants is isolated, so that users can not view or modify another customer's data.
16 |
17 | The AWS blog post, ["Multi-tenant data isolation with PostgreSQL Row Level Security"](https://aws.amazon.com/blogs/database/multi-tenant-data-isolation-with-postgresql-row-level-security/) describes two approaches to using Row Level Security in Postgres to isolate data in a multi-tenant app. This example uses the "Alternative approach" described in the article, using a Postgres runtime parameter to set the current tenant ID, which is referenced in table security policies. This approach allows the application to use a shared connection pool, rather than creating a new database connection for each tenant.
18 |
19 | ## How it works
20 |
21 | There are a few steps required to set up Row Level Security in Postgres and then use it in Prisma:
22 |
23 | ### 1. Create a database user with limited permissions
24 |
25 | Superusers and roles with the `BYPASSRLS` attribute always bypass the row security system in Postgres. Your application should connect to the database as a user with limited permissions that do not allow bypassing RLS. In this example, the database is defined by extending the `postgres` image in [a custom `Dockerfile`](docker/Dockerfile), which adds [a shell script under `/docker-entrypoint-initdb.d/init-app-db.sh`](docker/init-app-db.sh) to create a new database user and database.
26 |
27 | ### 2. Enable Row Level Security on tables you want to secure
28 |
29 | Create [a custom migration file](https://www.prisma.io/docs/guides/database/developing-with-prisma-migrate/customizing-migrations) that includes SQL commands to enable Row Level Security for each table you want to secure. See [this migration file](prisma/migrations/20221211203153_row_level_security/migration.sql), where the following commands are run for each table:
30 |
31 | ```sql
32 | ALTER TABLE "User" ENABLE ROW LEVEL SECURITY;
33 | ALTER TABLE "User" FORCE ROW LEVEL SECURITY;
34 | ```
35 |
36 | The first command (`ENABLE ROW LEVEL SECURITY`) tells Postgres to deny access to the table's rows for `SELECT`, `INSERT`, `UPDATE`, or `DELETE` commands, except for rows that are allowed by a row security policy on the table.
37 |
38 | The second command (`FORCE ROW LEVEL SECURITY`) tells Postgres to apply row level security even for the table's owner, which normally bypasses row security policies. This is important if the database user that you run migrations with is the same user that your application uses to connect to the database.
39 |
40 | ### 3. Create row security policies for each table that has RLS enabled
41 |
42 | Enabling Row Level Security on a table creates a default-deny policy, meaning no rows on the table are visible or can be modified. To allow access to the table, you must define one or more row security policies that define the conditions when access is allowed. In the same migration file as above, a row security policy is defined for each table as follows:
43 |
44 | ```sql
45 | CREATE POLICY tenant_isolation_policy ON "User" USING ("companyId" = current_setting('app.current_company_id', TRUE)::uuid);
46 | ```
47 |
48 | This policy tells Postgres to allow a `SELECT`, `INSERT`, `UPDATE`, or `DELETE` on rows where the `"companyId"` column matches the runtime parameter named `app.current_company_id`, which we will set at runtime in a Prisma Client extension.
49 |
50 | ### 4. Create row security policies to bypass RLS (optional)
51 |
52 | Your application may need to run queries that bypass RLS. Examples might include looking up the current user to determine their tenant ID, admin page queries, or aggregations across multiple tenants.
53 |
54 | One option is to run these queries with a database user that has a role with the `BYPASSRLS` attribute. However, this requires managing multiple different database connection pools. Another option is to add policies that allow access to all rows when a certain runtime parameter is set:
55 |
56 | ```sql
57 | CREATE POLICY bypass_rls_policy ON "User" USING (current_setting('app.bypass_rls', TRUE)::text = 'on');
58 | ```
59 |
60 | This policy and the previous policy are both "permissive" policies by default, meaning Postgres combines them with an `OR` condition. To access a row in this table, either the `"companyId"` column must match the `app.current_company_id` setting, OR the `app.bypass_rls` setting must be set to the string `'on'`.
61 |
62 | ### 5. Set default values for columns associated with row security policies (optional)
63 |
64 | You might want to automatically populate columns associated with row level security policies. In the [Prisma schema file](prisma/schema.prisma) for this example, we set default values for the `companyId` columns, so that the default value for these columns can be inferred from the same runtime parameter used in the row security policy:
65 |
66 | ```prisma
67 | model User {
68 | // ...
69 | companyId String @default(dbgenerated("(current_setting('app.current_company_id'::text))::uuid")) @db.Uuid
70 | // ...
71 | }
72 | ```
73 |
74 | Setting a default value in the Prisma schema allows us to omit the value when creating a row in Prisma Client queries. An `INSERT` command will only succeed in the following scenarios:
75 |
76 | - The `companyId` is omitted, but the `app.current_company_id` setting is set
77 | - A `companyId` is provided, and it matches the `app.current_company_id` setting
78 | - A `companyId` is provided, and the `app.bypass_rls` is set to `'on'`
79 |
80 | ### 6. Create a Prisma Client extension that wraps queries in a transaction and sets the RLS runtime parameter(s)
81 |
82 | In order to set a runtime parameter in Postgres, you can execute a raw query with Prisma:
83 |
84 | ```typescript
85 | await prisma.$executeRaw`SELECT set_config('app.current_company_id', ${companyId}, TRUE)`;
86 | ```
87 |
88 | However, each time you run a query, Prisma may use a different connection from the connection pool. In order to associate the parameter with all of the queries in the context of a given request in your application, you should:
89 |
90 | 1. Start a transaction.
91 | 2. Set the runtime parameter as a `LOCAL` setting, which lasts only until the end of the current transaction. This may be done by passing `TRUE` to the third argument (`is_local`) of the `set_config()` function.
92 | 3. Run all queries for the duration of the request inside this transaction.
93 |
94 | All queries for a given transaction will use the same database connection, and because the setting is local, it won't affect any other transactions.
95 |
96 | Prisma Client extensions provide a way to easily ensure all queries run inside a transaction with a setting enabled for RLS. See [the `script.ts` file](script.ts) for an example extension, which allows you to create RLS-enabled client instances:
97 |
98 | ```typescript
99 | const companyPrisma = prisma.forCompany(companyId);
100 |
101 | const projects = await companyPrisma.project.findMany();
102 | ```
103 |
104 | ## How to use
105 |
106 | ### Prerequisites
107 |
108 | - Install [Node.js](https://nodejs.org/en/download/)
109 | - Install [Docker](https://docs.docker.com/get-docker/)
110 |
111 | ### 1. Download example & install dependencies
112 |
113 | Clone this repository:
114 |
115 | ```sh
116 | git clone git@github.com:sbking/prisma-client-extensions.git
117 | ```
118 |
119 | Create a `.env` file and install dependencies:
120 |
121 | ```sh
122 | cd row-level-security
123 | cp .env.example .env
124 | npm install
125 | ```
126 |
127 | ### 2. Start the database
128 |
129 | Run the following command to start a new Postgres database in a Docker container:
130 |
131 | ```sh
132 | docker compose up -d
133 | ```
134 |
135 | ### 3. Run migrations
136 |
137 | Run this command to apply migrations to the database:
138 |
139 | ```sh
140 | npx prisma migrate deploy
141 | ```
142 |
143 | ### 4. Seed the database
144 |
145 | Run the following command to add seed data to the database:
146 |
147 | ```sh
148 | npx prisma db seed
149 | ```
150 |
151 | ### 5. Run the `dev` script
152 |
153 | To run the `script.ts` file, run the following command:
154 |
155 | ```sh
156 | npm run dev
157 | ```
158 |
--------------------------------------------------------------------------------
/row-level-security/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.7"
2 | services:
3 | postgres:
4 | build: ./docker
5 | restart: always
6 | environment:
7 | - POSTGRES_USER=postgres
8 | - POSTGRES_PASSWORD=postgres
9 | - POSTGRES_DB=postgres
10 | - APP_USER=app
11 | - APP_PASSWORD=s3cret
12 | - APP_DB=app
13 | ports:
14 | - "6002:5432"
15 | volumes:
16 | - postgres-data:/var/lib/postgresql/data
17 |
18 | volumes:
19 | postgres-data:
20 |
--------------------------------------------------------------------------------
/row-level-security/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM postgres:latest
2 |
3 | COPY init-app-db.sh /docker-entrypoint-initdb.d/
4 |
--------------------------------------------------------------------------------
/row-level-security/docker/init-app-db.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
5 | CREATE USER $APP_USER WITH PASSWORD '$APP_PASSWORD';
6 | ALTER USER $APP_USER CREATEDB;
7 | GRANT ALL PRIVILEGES ON DATABASE $APP_DB TO $APP_USER;
8 | EOSQL
9 |
--------------------------------------------------------------------------------
/row-level-security/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "row-level-security",
3 | "private": true,
4 | "scripts": {
5 | "dev": "ts-node script.ts"
6 | },
7 | "dependencies": {
8 | "@prisma/client": "6.0.1"
9 | },
10 | "devDependencies": {
11 | "@faker-js/faker": "9.0.2",
12 | "@types/node": "22.8.2",
13 | "prisma": "6.0.1",
14 | "ts-node": "10.9.2",
15 | "typescript": "5.6.2"
16 | },
17 | "prisma": {
18 | "seed": "ts-node prisma/seed.ts"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/row-level-security/prisma/migrations/20221211203130_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateEnum
2 | CREATE TYPE "TaskStatus" AS ENUM ('Pending', 'InProgress', 'Complete', 'WontDo');
3 |
4 | -- CreateTable
5 | CREATE TABLE "Company" (
6 | "id" UUID NOT NULL DEFAULT gen_random_uuid(),
7 | "name" TEXT NOT NULL,
8 |
9 | CONSTRAINT "Company_pkey" PRIMARY KEY ("id")
10 | );
11 |
12 | -- CreateTable
13 | CREATE TABLE "User" (
14 | "id" UUID NOT NULL DEFAULT gen_random_uuid(),
15 | "companyId" UUID NOT NULL DEFAULT (current_setting('app.current_company_id'::text))::uuid,
16 | "email" TEXT NOT NULL,
17 |
18 | CONSTRAINT "User_pkey" PRIMARY KEY ("id")
19 | );
20 |
21 | -- CreateTable
22 | CREATE TABLE "Project" (
23 | "id" UUID NOT NULL DEFAULT gen_random_uuid(),
24 | "companyId" UUID NOT NULL DEFAULT (current_setting('app.current_company_id'::text))::uuid,
25 | "userId" UUID,
26 | "title" TEXT NOT NULL,
27 |
28 | CONSTRAINT "Project_pkey" PRIMARY KEY ("id")
29 | );
30 |
31 | -- CreateTable
32 | CREATE TABLE "Task" (
33 | "id" UUID NOT NULL DEFAULT gen_random_uuid(),
34 | "companyId" UUID NOT NULL DEFAULT (current_setting('app.current_company_id'::text))::uuid,
35 | "projectId" UUID NOT NULL,
36 | "userId" UUID,
37 | "title" TEXT NOT NULL,
38 | "status" "TaskStatus" NOT NULL,
39 |
40 | CONSTRAINT "Task_pkey" PRIMARY KEY ("id")
41 | );
42 |
43 | -- CreateIndex
44 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
45 |
46 | -- AddForeignKey
47 | ALTER TABLE "User" ADD CONSTRAINT "User_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE CASCADE ON UPDATE CASCADE;
48 |
49 | -- AddForeignKey
50 | ALTER TABLE "Project" ADD CONSTRAINT "Project_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE CASCADE ON UPDATE CASCADE;
51 |
52 | -- AddForeignKey
53 | ALTER TABLE "Project" ADD CONSTRAINT "Project_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
54 |
55 | -- AddForeignKey
56 | ALTER TABLE "Task" ADD CONSTRAINT "Task_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE CASCADE ON UPDATE CASCADE;
57 |
58 | -- AddForeignKey
59 | ALTER TABLE "Task" ADD CONSTRAINT "Task_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
60 |
61 | -- AddForeignKey
62 | ALTER TABLE "Task" ADD CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
63 |
--------------------------------------------------------------------------------
/row-level-security/prisma/migrations/20221211203153_row_level_security/migration.sql:
--------------------------------------------------------------------------------
1 | -- Enable Row Level Security
2 | ALTER TABLE "Company" ENABLE ROW LEVEL SECURITY;
3 | ALTER TABLE "User" ENABLE ROW LEVEL SECURITY;
4 | ALTER TABLE "Project" ENABLE ROW LEVEL SECURITY;
5 | ALTER TABLE "Task" ENABLE ROW LEVEL SECURITY;
6 |
7 | -- Force Row Level Security for table owners
8 | ALTER TABLE "Company" FORCE ROW LEVEL SECURITY;
9 | ALTER TABLE "User" FORCE ROW LEVEL SECURITY;
10 | ALTER TABLE "Project" FORCE ROW LEVEL SECURITY;
11 | ALTER TABLE "Task" FORCE ROW LEVEL SECURITY;
12 |
13 | -- Create row security policies
14 | CREATE POLICY tenant_isolation_policy ON "Company" USING ("id" = current_setting('app.current_company_id', TRUE)::uuid);
15 | CREATE POLICY tenant_isolation_policy ON "User" USING ("companyId" = current_setting('app.current_company_id', TRUE)::uuid);
16 | CREATE POLICY tenant_isolation_policy ON "Project" USING ("companyId" = current_setting('app.current_company_id', TRUE)::uuid);
17 | CREATE POLICY tenant_isolation_policy ON "Task" USING ("companyId" = current_setting('app.current_company_id', TRUE)::uuid);
18 |
19 | -- Create policies to bypass RLS (optional)
20 | CREATE POLICY bypass_rls_policy ON "Company" USING (current_setting('app.bypass_rls', TRUE)::text = 'on');
21 | CREATE POLICY bypass_rls_policy ON "User" USING (current_setting('app.bypass_rls', TRUE)::text = 'on');
22 | CREATE POLICY bypass_rls_policy ON "Project" USING (current_setting('app.bypass_rls', TRUE)::text = 'on');
23 | CREATE POLICY bypass_rls_policy ON "Task" USING (current_setting('app.bypass_rls', TRUE)::text = 'on');
24 |
--------------------------------------------------------------------------------
/row-level-security/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/row-level-security/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "postgresql"
7 | url = env("DATABASE_URL")
8 | }
9 |
10 | model Company {
11 | id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
12 | name String
13 |
14 | users User[]
15 | projects Project[]
16 | tasks Task[]
17 | }
18 |
19 | model User {
20 | id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
21 | companyId String @default(dbgenerated("(current_setting('app.current_company_id'::text))::uuid")) @db.Uuid
22 | email String @unique
23 |
24 | company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
25 | ownedProjects Project[]
26 | assignedTasks Task[]
27 | }
28 |
29 | model Project {
30 | id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
31 | companyId String @default(dbgenerated("(current_setting('app.current_company_id'::text))::uuid")) @db.Uuid
32 | userId String? @db.Uuid
33 | title String
34 |
35 | company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
36 | owner User? @relation(fields: [userId], references: [id], onDelete: SetNull)
37 | tasks Task[]
38 | }
39 |
40 | enum TaskStatus {
41 | Pending
42 | InProgress
43 | Complete
44 | WontDo
45 | }
46 |
47 | model Task {
48 | id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
49 | companyId String @default(dbgenerated("(current_setting('app.current_company_id'::text))::uuid")) @db.Uuid
50 | projectId String @db.Uuid
51 | userId String? @db.Uuid
52 | title String
53 | status TaskStatus
54 |
55 | company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
56 | project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
57 | assignee User? @relation(fields: [userId], references: [id], onDelete: SetNull)
58 | }
59 |
--------------------------------------------------------------------------------
/row-level-security/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | import { faker } from "@faker-js/faker";
2 | import { Prisma, PrismaClient, TaskStatus } from "@prisma/client";
3 |
4 | const prisma = new PrismaClient();
5 |
6 | async function main() {
7 | // Create 100 companies
8 | const data = Array.from({ length: 100 }, () => {
9 | const company = {
10 | id: faker.datatype.uuid(),
11 | name: faker.company.name(),
12 | } satisfies Prisma.CompanyCreateManyInput;
13 |
14 | // Create 20 users per company
15 | const users = Array.from({ length: 10 }, () => ({
16 | id: faker.datatype.uuid(),
17 | companyId: company.id,
18 | email: faker.helpers.unique(faker.internet.email),
19 | })) satisfies Prisma.UserCreateManyInput[];
20 |
21 | // Create 50 projects per company
22 | const projects = Array.from({ length: 20 }, () => ({
23 | id: faker.datatype.uuid(),
24 | companyId: company.id,
25 | userId: faker.helpers.arrayElement(users).id,
26 | title: faker.commerce.productName(),
27 | })) satisfies Prisma.ProjectCreateManyInput[];
28 |
29 | // Create 500 tasks per company
30 | const tasks = Array.from({ length: 200 }, () => ({
31 | id: faker.datatype.uuid(),
32 | companyId: company.id,
33 | projectId: faker.helpers.arrayElement(projects).id,
34 | userId: faker.helpers.arrayElement(users).id,
35 | title: faker.hacker.phrase(),
36 | status: faker.helpers.objectValue(TaskStatus),
37 | })) satisfies Prisma.TaskCreateManyInput[];
38 |
39 | return { company, users, projects, tasks };
40 | });
41 |
42 | await prisma.$transaction([
43 | prisma.$executeRaw`SELECT set_config('app.bypass_rls', 'on', TRUE)`,
44 | prisma.company.deleteMany(),
45 | prisma.company.createMany({ data: data.map((d) => d.company) }),
46 | prisma.user.createMany({ data: data.flatMap((d) => d.users) }),
47 | prisma.project.createMany({ data: data.flatMap((d) => d.projects) }),
48 | prisma.task.createMany({ data: data.flatMap((d) => d.tasks) }),
49 | ]);
50 |
51 | console.log(`Database has been seeded. 🌱`);
52 | }
53 |
54 | main()
55 | .then(async () => {
56 | await prisma.$disconnect();
57 | })
58 | .catch(async (e) => {
59 | console.error(e);
60 | await prisma.$disconnect();
61 | process.exit(1);
62 | });
63 |
--------------------------------------------------------------------------------
/row-level-security/script.ts:
--------------------------------------------------------------------------------
1 | import { Prisma, PrismaClient, TaskStatus } from "@prisma/client";
2 |
3 | function bypassRLS() {
4 | return Prisma.defineExtension((prisma) =>
5 | prisma.$extends({
6 | query: {
7 | $allModels: {
8 | async $allOperations({ args, query }) {
9 | const [, result] = await prisma.$transaction([
10 | prisma.$executeRaw`SELECT set_config('app.bypass_rls', 'on', TRUE)`,
11 | query(args),
12 | ]);
13 | return result;
14 | },
15 | },
16 | },
17 | })
18 | );
19 | }
20 |
21 | function forCompany(companyId: string) {
22 | return Prisma.defineExtension((prisma) =>
23 | prisma.$extends({
24 | query: {
25 | $allModels: {
26 | async $allOperations({ args, query }) {
27 | const [, result] = await prisma.$transaction([
28 | prisma.$executeRaw`SELECT set_config('app.current_company_id', ${companyId}, TRUE)`,
29 | query(args),
30 | ]);
31 | return result;
32 | },
33 | },
34 | },
35 | })
36 | );
37 | }
38 |
39 | const prisma = new PrismaClient();
40 |
41 | async function main() {
42 | const user = await prisma.$extends(bypassRLS()).user.findFirstOrThrow();
43 |
44 | const companyPrisma = prisma.$extends(forCompany(user.companyId));
45 |
46 | const projectInclude = {
47 | owner: true,
48 | tasks: {
49 | include: {
50 | assignee: true,
51 | },
52 | },
53 | } satisfies Prisma.ProjectInclude;
54 |
55 | const projects = await companyPrisma.project.findMany({
56 | include: projectInclude,
57 | });
58 |
59 | invariant(projects.every((project) => project.companyId === user.companyId));
60 |
61 | const newProject = await companyPrisma.project.create({
62 | include: projectInclude,
63 | data: {
64 | title: "New project",
65 | owner: {
66 | connect: { id: user.id },
67 | },
68 | tasks: {
69 | createMany: {
70 | data: [
71 | { title: "Task A", status: TaskStatus.Pending, userId: user.id },
72 | { title: "Task B", status: TaskStatus.Pending, userId: user.id },
73 | { title: "Task C", status: TaskStatus.Pending, userId: user.id },
74 | ],
75 | },
76 | },
77 | },
78 | });
79 |
80 | invariant(newProject.companyId === user.companyId);
81 | invariant(
82 | newProject.tasks.every((task) => task.companyId === user.companyId)
83 | );
84 | }
85 |
86 | function invariant(condition: T): asserts condition {
87 | if (!condition) throw new Error("Invariant failed");
88 | }
89 |
90 | main()
91 | .then(async () => {
92 | await prisma.$disconnect();
93 | })
94 | .catch(async (e) => {
95 | console.error(e);
96 | await prisma.$disconnect();
97 | process.exit(1);
98 | });
99 |
--------------------------------------------------------------------------------
/row-level-security/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "exclude": ["node_modules"],
4 | "compilerOptions": {
5 | "noEmit": true,
6 | "lib": ["ES2019"],
7 | "isolatedModules": true,
8 | "esModuleInterop": true,
9 | "module": "CommonJS",
10 | "moduleResolution": "node",
11 | "resolveJsonModule": true,
12 | "target": "ES2019",
13 | "strict": true,
14 | "allowJs": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "skipLibCheck": true
17 | },
18 | "ts-node": {
19 | "compilerOptions": {
20 | "module": "commonjs"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/static-methods/README.md:
--------------------------------------------------------------------------------
1 | # Prisma Client Extension - Static Methods
2 |
3 | This example demonstrates how to create a Prisma Client extension that adds `signUp()` and `findManyByDomain()` methods to a `User` model.
4 |
5 | This technique can be used to abstract the logic for common queries / operations, create repository-like interfaces, or do anything you might do with a static class method.
6 |
7 | ## Caveats
8 |
9 | This extension is provided as an example only. It is not intended to be used in production environments.
10 |
11 | Please read [the documentation on `model` extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/model) for more information.
12 |
13 | ## How to use
14 |
15 | ### Prerequisites
16 |
17 | - Install [Node.js](https://nodejs.org/en/download/)
18 |
19 | ### 1. Download example & install dependencies
20 |
21 | Clone this repository:
22 |
23 | ```sh
24 | git clone git@github.com:sbking/prisma-client-extensions.git
25 | ```
26 |
27 | Install dependencies:
28 |
29 | ```sh
30 | cd static-methods
31 | npm install
32 | ```
33 |
34 | ### 2. Create an SQLite database and run migrations
35 |
36 | Run the following command. An SQLite database will be created automatically:
37 |
38 | ```sh
39 | npx prisma migrate deploy
40 | ```
41 |
42 | ### 3. Run the `dev` script
43 |
44 | To run the `script.ts` file, run the following command:
45 |
46 | ```sh
47 | npm run dev
48 | ```
49 |
--------------------------------------------------------------------------------
/static-methods/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "static-methods",
3 | "private": true,
4 | "scripts": {
5 | "dev": "ts-node script.ts"
6 | },
7 | "dependencies": {
8 | "@prisma/client": "6.0.1",
9 | "bcryptjs": "3.0.2"
10 | },
11 | "devDependencies": {
12 | "@faker-js/faker": "9.0.2",
13 | "@types/bcryptjs": "2.4.6",
14 | "@types/node": "22.8.2",
15 | "prisma": "6.0.1",
16 | "ts-node": "10.9.2",
17 | "typescript": "5.6.2"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/static-methods/prisma/migrations/20221209172935_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" TEXT NOT NULL PRIMARY KEY,
4 | "email" TEXT NOT NULL
5 | );
6 |
7 | -- CreateTable
8 | CREATE TABLE "Password" (
9 | "hash" TEXT NOT NULL,
10 | "userId" TEXT NOT NULL,
11 | CONSTRAINT "Password_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
12 | );
13 |
14 | -- CreateIndex
15 | CREATE UNIQUE INDEX "Password_userId_key" ON "Password"("userId");
16 |
--------------------------------------------------------------------------------
/static-methods/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "sqlite"
--------------------------------------------------------------------------------
/static-methods/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "sqlite"
7 | url = "file:./dev.db"
8 | }
9 |
10 | model User {
11 | id String @id @default(cuid())
12 | email String
13 | password Password?
14 | }
15 |
16 | model Password {
17 | hash String
18 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
19 | userId String @unique
20 | }
21 |
--------------------------------------------------------------------------------
/static-methods/script.ts:
--------------------------------------------------------------------------------
1 | import bcrypt from "bcryptjs";
2 | import { PrismaClient } from "@prisma/client";
3 |
4 | const prisma = new PrismaClient().$extends({
5 | model: {
6 | user: {
7 | async signUp(email: string, password: string) {
8 | const hash = await bcrypt.hash(password, 10);
9 | return prisma.user.create({
10 | data: {
11 | email,
12 | password: {
13 | create: {
14 | hash,
15 | },
16 | },
17 | },
18 | });
19 | },
20 |
21 | async findManyByDomain(domain: string) {
22 | return prisma.user.findMany({
23 | where: { email: { endsWith: `@${domain}` } },
24 | });
25 | },
26 | },
27 | },
28 | });
29 |
30 | async function main() {
31 | await prisma.user.signUp("user1@example1.com", "p4ssword");
32 | await prisma.user.signUp("user2@example2.com", "s3cret");
33 |
34 | const users = await prisma.user.findManyByDomain("example2.com");
35 | console.log(users);
36 | }
37 |
38 | main()
39 | .then(async () => {
40 | await prisma.$disconnect();
41 | })
42 | .catch(async (e) => {
43 | console.error(e);
44 | await prisma.$disconnect();
45 | process.exit(1);
46 | });
47 |
--------------------------------------------------------------------------------
/static-methods/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "exclude": ["node_modules"],
4 | "compilerOptions": {
5 | "noEmit": true,
6 | "lib": ["ES2019"],
7 | "isolatedModules": true,
8 | "esModuleInterop": true,
9 | "module": "CommonJS",
10 | "moduleResolution": "node",
11 | "resolveJsonModule": true,
12 | "target": "ES2019",
13 | "strict": true,
14 | "allowJs": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "skipLibCheck": true
17 | },
18 | "ts-node": {
19 | "compilerOptions": {
20 | "module": "commonjs"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/transformed-fields/README.md:
--------------------------------------------------------------------------------
1 | # Prisma Client Extension - Transformed Fields
2 |
3 | This example shows how to use a Prisma Client extension to transform result fields returned by queries. In this example, a date field is transformed to a relative string for a specific locale.
4 |
5 | This example shows a way to implement internationalization (i18n) at the data access layer in your application. However, this technique allows you to implement any kind of custom transformation or serialization/deserialization of fields on your query results.
6 |
7 | ## Caveats
8 |
9 | This extension is provided as an example only. It is not intended to be used in production environments.
10 |
11 | Please read [the documentation on `result` extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/result) for more information.
12 |
13 | ## How to use
14 |
15 | ### Prerequisites
16 |
17 | - Install [Node.js](https://nodejs.org/en/download/)
18 |
19 | ### 1. Download example & install dependencies
20 |
21 | Clone this repository:
22 |
23 | ```sh
24 | git clone git@github.com:sbking/prisma-client-extensions.git
25 | ```
26 |
27 | Install dependencies:
28 |
29 | ```sh
30 | cd transformed-fields
31 | npm install
32 | ```
33 |
34 | ### 2. Create an SQLite database and run migrations
35 |
36 | Run the following command. An SQLite database will be created automatically:
37 |
38 | ```sh
39 | npx prisma migrate deploy
40 | ```
41 |
42 | ### 3. Seed the database
43 |
44 | Run the following command to add seed data to the database:
45 |
46 | ```sh
47 | npx prisma db seed
48 | ```
49 |
50 | ### 4. Run the `dev` script
51 |
52 | To run the `script.ts` file, run the following command:
53 |
54 | ```sh
55 | npm run dev
56 | ```
57 |
--------------------------------------------------------------------------------
/transformed-fields/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "transformed-fields",
3 | "private": true,
4 | "scripts": {
5 | "dev": "ts-node script.ts"
6 | },
7 | "dependencies": {
8 | "@prisma/client": "6.0.1",
9 | "date-fns": "4.1.0"
10 | },
11 | "devDependencies": {
12 | "@faker-js/faker": "9.0.2",
13 | "@types/node": "22.8.2",
14 | "prisma": "6.0.1",
15 | "ts-node": "10.9.2",
16 | "typescript": "5.6.2"
17 | },
18 | "prisma": {
19 | "seed": "ts-node prisma/seed.ts"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/transformed-fields/prisma/migrations/20221211201016_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "Post" (
3 | "id" TEXT NOT NULL PRIMARY KEY,
4 | "title" TEXT NOT NULL,
5 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
6 | );
7 |
--------------------------------------------------------------------------------
/transformed-fields/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "sqlite"
--------------------------------------------------------------------------------
/transformed-fields/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "sqlite"
7 | url = "file:./dev.db"
8 | }
9 |
10 | model Post {
11 | id String @id @default(cuid())
12 | title String
13 | createdAt DateTime @default(now())
14 | }
15 |
--------------------------------------------------------------------------------
/transformed-fields/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | import { faker } from "@faker-js/faker";
2 | import { Prisma, PrismaClient } from "@prisma/client";
3 |
4 | const prisma = new PrismaClient();
5 |
6 | async function main() {
7 | // Cleanup existing posts
8 | await prisma.post.deleteMany({});
9 |
10 | // Create 100 random posts
11 | const posts = Array.from({ length: 100 }, () => ({
12 | title: faker.hacker.phrase(),
13 | createdAt: faker.date.recent(365),
14 | })) satisfies Prisma.PostCreateInput[];
15 |
16 | // Seed the database
17 | for (const post of posts) {
18 | await prisma.post.create({ data: post });
19 | }
20 |
21 | console.log(`Database has been seeded. 🌱`);
22 | }
23 |
24 | main()
25 | .then(async () => {
26 | await prisma.$disconnect();
27 | })
28 | .catch(async (e) => {
29 | console.error(e);
30 | await prisma.$disconnect();
31 | process.exit(1);
32 | });
33 |
--------------------------------------------------------------------------------
/transformed-fields/script.ts:
--------------------------------------------------------------------------------
1 | import { formatDistanceToNow } from "date-fns";
2 | import { de } from "date-fns/locale";
3 | import { PrismaClient } from "@prisma/client";
4 |
5 | const prisma = new PrismaClient().$extends({
6 | result: {
7 | post: {
8 | createdAt: {
9 | needs: { createdAt: true },
10 | compute(post) {
11 | return formatDistanceToNow(post.createdAt, {
12 | addSuffix: true,
13 | locale: de,
14 | });
15 | },
16 | },
17 | },
18 | },
19 | });
20 |
21 | async function main() {
22 | const posts = await prisma.post.findMany({ take: 5 });
23 |
24 | for (const post of posts) {
25 | console.info(`- ${post.title} (${post.createdAt})`);
26 | }
27 | }
28 |
29 | main()
30 | .then(async () => {
31 | await prisma.$disconnect();
32 | })
33 | .catch(async (e) => {
34 | console.error(e);
35 | await prisma.$disconnect();
36 | process.exit(1);
37 | });
38 |
--------------------------------------------------------------------------------
/transformed-fields/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "exclude": ["node_modules"],
4 | "compilerOptions": {
5 | "noEmit": true,
6 | "lib": ["ES2019"],
7 | "isolatedModules": true,
8 | "esModuleInterop": true,
9 | "module": "CommonJS",
10 | "moduleResolution": "node",
11 | "resolveJsonModule": true,
12 | "target": "ES2019",
13 | "strict": true,
14 | "allowJs": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "skipLibCheck": true
17 | },
18 | "ts-node": {
19 | "compilerOptions": {
20 | "module": "commonjs"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/update-delete-ignore-not-found/README.md:
--------------------------------------------------------------------------------
1 | # Prisma Client Extension - Update and Delete ignore on not found
2 |
3 | This example adds the `updateIgnoreOnNotFound` and `deleteIgnoreOnNotFound` methods to all your models. The two methods allow you to return `null` if a record is not found when updating or deleting a record instead of throwing an error.
4 |
5 | Please read [the documentation on `model` extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/model) for more information.
6 |
7 | ## How to use
8 |
9 | ### Prerequisites
10 |
11 | - Install [Node.js](https://nodejs.org/en/download/)
12 |
13 | ### 1. Download example & install dependencies
14 |
15 | Clone this repository:
16 |
17 | ```sh
18 | git clone git@github.com:prisma/prisma-client-extensions.git
19 | ```
20 |
21 | Install dependencies:
22 |
23 | ```sh
24 | cd update-delete-ignore-not-found
25 | npm install
26 | ```
27 |
28 | ### 2. Create an SQLite database and run migrations
29 |
30 | Run the following command to create a SQLite database and seed your database with sample data:
31 |
32 | ```sh
33 | npx prisma migrate dev
34 | ```
35 |
36 | ### 3. Run the `dev` script
37 |
38 | To run the `script.ts` file, run the following command:
39 |
40 | ```sh
41 | npm run dev
42 | ```
43 |
--------------------------------------------------------------------------------
/update-delete-ignore-not-found/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "update-delete-ignore-not-found",
3 | "scripts": {
4 | "dev": "ts-node script.ts"
5 | },
6 | "dependencies": {
7 | "@prisma/client": "6.0.1"
8 | },
9 | "devDependencies": {
10 | "@faker-js/faker": "9.0.2",
11 | "@types/node": "22.8.2",
12 | "prisma": "6.0.1",
13 | "ts-node": "10.9.2",
14 | "typescript": "5.6.2"
15 | },
16 | "prisma": {
17 | "seed": "ts-node prisma/seed.ts"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/update-delete-ignore-not-found/prisma/migrations/20221211185834_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" TEXT NOT NULL PRIMARY KEY,
4 | "firstName" TEXT NOT NULL,
5 | "lastName" TEXT NOT NULL
6 | );
7 |
--------------------------------------------------------------------------------
/update-delete-ignore-not-found/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "sqlite"
--------------------------------------------------------------------------------
/update-delete-ignore-not-found/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "sqlite"
7 | url = "file:./dev.db"
8 | }
9 |
10 | model User {
11 | id String @id @default(cuid())
12 | firstName String
13 | lastName String
14 | }
15 |
--------------------------------------------------------------------------------
/update-delete-ignore-not-found/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | import { faker } from "@faker-js/faker";
2 | import { Prisma, PrismaClient } from "@prisma/client";
3 |
4 | const prisma = new PrismaClient();
5 |
6 | async function main() {
7 | // Cleanup existing users
8 | await prisma.user.deleteMany({});
9 |
10 | // Create 100 random users
11 | const users = Array.from({ length: 100 }, () => ({
12 | firstName: faker.name.firstName(),
13 | lastName: faker.name.lastName(),
14 | })) satisfies Prisma.UserCreateInput[];
15 |
16 | // Seed the database
17 | for (const user of users) {
18 | await prisma.user.create({ data: user });
19 | }
20 |
21 | console.log(`Database has been seeded. 🌱`);
22 | }
23 |
24 | main()
25 | .then(async () => {
26 | await prisma.$disconnect();
27 | })
28 | .catch(async (e) => {
29 | console.error(e);
30 | await prisma.$disconnect();
31 | process.exit(1);
32 | });
33 |
--------------------------------------------------------------------------------
/update-delete-ignore-not-found/script.ts:
--------------------------------------------------------------------------------
1 | import { Prisma, PrismaClient } from "@prisma/client";
2 |
3 | const prisma = new PrismaClient().$extends({
4 | name: "UpdateAndDeleteIgnoreNotFound",
5 | model: {
6 | $allModels: {
7 | async updateIgnoreOnNotFound(
8 | this: T,
9 | args: Prisma.Exact>
10 | ): Promise | null> {
11 | try {
12 | const context = Prisma.getExtensionContext(this) as any;
13 | return await context.update(args);
14 | } catch (err) {
15 | if (
16 | err instanceof Prisma.PrismaClientKnownRequestError &&
17 | err.code === "P2025"
18 | ) {
19 | return null;
20 | }
21 | throw err;
22 | }
23 | },
24 | async deleteIgnoreOnNotFound(
25 | this: T,
26 | args: Prisma.Exact>
27 | ): Promise | null> {
28 | try {
29 | const context = Prisma.getExtensionContext(this) as any;
30 | return await context.delete(args);
31 | } catch (err) {
32 | if (
33 | err instanceof Prisma.PrismaClientKnownRequestError &&
34 | err.code === "P2025"
35 | ) {
36 | return null;
37 | }
38 | throw err;
39 | }
40 | },
41 | },
42 | },
43 | });
44 |
45 | async function main() {
46 | const updateOp = await prisma.user.updateIgnoreOnNotFound({
47 | where : {
48 | id: "-1"
49 | },
50 | data: {
51 | firstName: "Alex P."
52 | }
53 | });
54 |
55 | const deleteOp = await prisma.user.deleteIgnoreOnNotFound({
56 | where : {
57 | id: "-1"
58 | },
59 | });
60 |
61 | console.log({
62 | updateOp,
63 | deleteOp
64 | })
65 |
66 | }
67 |
68 | main()
69 | .then(async () => {
70 | await prisma.$disconnect();
71 | })
72 | .catch(async (e) => {
73 | console.error(e);
74 | await prisma.$disconnect();
75 | process.exit(1);
76 | });
77 |
--------------------------------------------------------------------------------
/update-delete-ignore-not-found/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "exclude": ["node_modules"],
4 | "compilerOptions": {
5 | "noEmit": true,
6 | "lib": ["ES2019"],
7 | "isolatedModules": true,
8 | "esModuleInterop": true,
9 | "module": "CommonJS",
10 | "moduleResolution": "node",
11 | "resolveJsonModule": true,
12 | "target": "ES2019",
13 | "strict": true,
14 | "allowJs": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "skipLibCheck": true
17 | },
18 | "ts-node": {
19 | "compilerOptions": {
20 | "module": "commonjs"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------