@cedarjs/storage 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +123 -0
- package/dist/UrlSigner.d.ts +46 -0
- package/dist/UrlSigner.d.ts.map +1 -0
- package/dist/UrlSigner.js +106 -0
- package/dist/adapters/BaseStorageAdapter.d.ts +31 -0
- package/dist/adapters/BaseStorageAdapter.d.ts.map +1 -0
- package/dist/adapters/BaseStorageAdapter.js +19 -0
- package/dist/adapters/FileSystemStorage/FileSystemStorage.d.ts +16 -0
- package/dist/adapters/FileSystemStorage/FileSystemStorage.d.ts.map +1 -0
- package/dist/adapters/FileSystemStorage/FileSystemStorage.js +38 -0
- package/dist/adapters/MemoryStorage/MemoryStorage.d.ts +15 -0
- package/dist/adapters/MemoryStorage/MemoryStorage.d.ts.map +1 -0
- package/dist/adapters/MemoryStorage/MemoryStorage.js +33 -0
- package/dist/cjs/UrlSigner.d.ts +46 -0
- package/dist/cjs/UrlSigner.d.ts.map +1 -0
- package/dist/cjs/UrlSigner.js +142 -0
- package/dist/cjs/adapters/BaseStorageAdapter.d.ts +31 -0
- package/dist/cjs/adapters/BaseStorageAdapter.d.ts.map +1 -0
- package/dist/cjs/adapters/BaseStorageAdapter.js +53 -0
- package/dist/cjs/adapters/FileSystemStorage/FileSystemStorage.d.ts +16 -0
- package/dist/cjs/adapters/FileSystemStorage/FileSystemStorage.d.ts.map +1 -0
- package/dist/cjs/adapters/FileSystemStorage/FileSystemStorage.js +72 -0
- package/dist/cjs/adapters/MemoryStorage/MemoryStorage.d.ts +15 -0
- package/dist/cjs/adapters/MemoryStorage/MemoryStorage.d.ts.map +1 -0
- package/dist/cjs/adapters/MemoryStorage/MemoryStorage.js +67 -0
- package/dist/cjs/createSavers.d.ts +15 -0
- package/dist/cjs/createSavers.d.ts.map +1 -0
- package/dist/cjs/createSavers.js +75 -0
- package/dist/cjs/fileToDataUri.d.ts +3 -0
- package/dist/cjs/fileToDataUri.d.ts.map +1 -0
- package/dist/cjs/fileToDataUri.js +32 -0
- package/dist/cjs/index.d.ts +40 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +50 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/prismaExtension.d.ts +29 -0
- package/dist/cjs/prismaExtension.d.ts.map +1 -0
- package/dist/cjs/prismaExtension.js +240 -0
- package/dist/createSavers.d.ts +15 -0
- package/dist/createSavers.d.ts.map +1 -0
- package/dist/createSavers.js +50 -0
- package/dist/fileToDataUri.d.ts +3 -0
- package/dist/fileToDataUri.d.ts.map +1 -0
- package/dist/fileToDataUri.js +8 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/prismaExtension.d.ts +29 -0
- package/dist/prismaExtension.d.ts.map +1 -0
- package/dist/prismaExtension.js +216 -0
- package/package.json +77 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var prismaExtension_exports = {};
|
|
20
|
+
__export(prismaExtension_exports, {
|
|
21
|
+
createUploadsExtension: () => createUploadsExtension
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(prismaExtension_exports);
|
|
24
|
+
var import_client = require("@prisma/client");
|
|
25
|
+
var import_extension = require("@prisma/client/extension");
|
|
26
|
+
var import_fileToDataUri = require("./fileToDataUri.js");
|
|
27
|
+
const createUploadsExtension = (config, storageAdapter, urlSigner) => {
|
|
28
|
+
const prismaInstance = new import_client.PrismaClient();
|
|
29
|
+
const queryExtends = {};
|
|
30
|
+
const resultExtends = {};
|
|
31
|
+
for (const modelName in config) {
|
|
32
|
+
const modelConfig = config[modelName];
|
|
33
|
+
if (!modelConfig) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const uploadFields = Array.isArray(modelConfig.fields) ? modelConfig.fields : [modelConfig.fields];
|
|
37
|
+
queryExtends[modelName] = {
|
|
38
|
+
async create({ query, args }) {
|
|
39
|
+
try {
|
|
40
|
+
const result = await query(args);
|
|
41
|
+
return result;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
await removeUploadedFiles(
|
|
44
|
+
uploadFields,
|
|
45
|
+
args.data
|
|
46
|
+
);
|
|
47
|
+
throw e;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
async createMany({ query, args }) {
|
|
51
|
+
try {
|
|
52
|
+
const result = await query(args);
|
|
53
|
+
return result;
|
|
54
|
+
} catch (e) {
|
|
55
|
+
const createDatas = args.data;
|
|
56
|
+
for (const createData of createDatas) {
|
|
57
|
+
await removeUploadedFiles(uploadFields, createData);
|
|
58
|
+
}
|
|
59
|
+
throw e;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
async update({ query, model, args }) {
|
|
63
|
+
const uploadFieldsToUpdate = uploadFields.filter(
|
|
64
|
+
(field) => (
|
|
65
|
+
// All of this non-sense is to make typescript happy. I'm not sure how data could be anything but an object
|
|
66
|
+
typeof args.data === "object" && args.data !== null && field in args.data
|
|
67
|
+
)
|
|
68
|
+
);
|
|
69
|
+
if (uploadFieldsToUpdate.length == 0) {
|
|
70
|
+
return query(args);
|
|
71
|
+
} else {
|
|
72
|
+
const originalRecord = await prismaInstance[
|
|
73
|
+
model
|
|
74
|
+
// @ts-expect-error TS in strict mode will error due to union type. We cannot narrow it down here.
|
|
75
|
+
].findFirstOrThrow({
|
|
76
|
+
where: args.where
|
|
77
|
+
// @TODO: should we select here to reduce the amount of data we're handling
|
|
78
|
+
});
|
|
79
|
+
try {
|
|
80
|
+
const result = await query(args);
|
|
81
|
+
await removeUploadedFiles(uploadFieldsToUpdate, originalRecord);
|
|
82
|
+
return result;
|
|
83
|
+
} catch (e) {
|
|
84
|
+
await removeUploadedFiles(
|
|
85
|
+
uploadFieldsToUpdate,
|
|
86
|
+
args.data
|
|
87
|
+
);
|
|
88
|
+
throw e;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
async updateMany({ query, model, args }) {
|
|
93
|
+
const uploadFieldsToUpdate = uploadFields.filter(
|
|
94
|
+
(field) => (
|
|
95
|
+
// All of this non-sense is to make typescript happy. I'm not sure how data could be anything but an object
|
|
96
|
+
typeof args.data === "object" && args.data !== null && field in args.data
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
if (uploadFieldsToUpdate.length == 0) {
|
|
100
|
+
return query(args);
|
|
101
|
+
} else {
|
|
102
|
+
const originalRecords = await prismaInstance[
|
|
103
|
+
model
|
|
104
|
+
// @ts-expect-error TS in strict mode will error due to union type. We cannot narrow it down here.
|
|
105
|
+
].findMany({
|
|
106
|
+
where: args.where
|
|
107
|
+
// @TODO: should we select here to reduce the amount of data we're handling
|
|
108
|
+
});
|
|
109
|
+
try {
|
|
110
|
+
const result = await query(args);
|
|
111
|
+
for await (const originalRecord of originalRecords) {
|
|
112
|
+
await removeUploadedFiles(uploadFieldsToUpdate, originalRecord);
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
} catch (e) {
|
|
116
|
+
await removeUploadedFiles(
|
|
117
|
+
uploadFieldsToUpdate,
|
|
118
|
+
args.data
|
|
119
|
+
);
|
|
120
|
+
throw e;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
async upsert({ query, model, args }) {
|
|
125
|
+
let isUpdate;
|
|
126
|
+
const uploadFieldsToUpdate = uploadFields.filter(
|
|
127
|
+
(field) => typeof args.update === "object" && args.update !== null && field in args.update
|
|
128
|
+
);
|
|
129
|
+
try {
|
|
130
|
+
let existingRecord;
|
|
131
|
+
if (args.update) {
|
|
132
|
+
existingRecord = await prismaInstance[
|
|
133
|
+
model
|
|
134
|
+
// @ts-expect-error TS in strict mode will error due to union type. We cannot narrow it down here.
|
|
135
|
+
].findUnique({
|
|
136
|
+
where: args.where
|
|
137
|
+
});
|
|
138
|
+
isUpdate = !!existingRecord;
|
|
139
|
+
}
|
|
140
|
+
const result = await query(args);
|
|
141
|
+
if (isUpdate && existingRecord) {
|
|
142
|
+
await removeUploadedFiles(uploadFieldsToUpdate, existingRecord);
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
} catch (e) {
|
|
146
|
+
await removeUploadedFiles(
|
|
147
|
+
// Only delete files we're updating on update
|
|
148
|
+
isUpdate ? uploadFieldsToUpdate : uploadFields,
|
|
149
|
+
isUpdate ? args.update : args.create
|
|
150
|
+
);
|
|
151
|
+
throw e;
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
async delete({ query, args }) {
|
|
155
|
+
const deleteResult = await query(args);
|
|
156
|
+
await removeUploadedFiles(
|
|
157
|
+
uploadFields,
|
|
158
|
+
// We don't know the exact type here
|
|
159
|
+
deleteResult
|
|
160
|
+
);
|
|
161
|
+
return deleteResult;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const needs = Object.fromEntries(uploadFields.map((field) => [field, true]));
|
|
165
|
+
resultExtends[modelName] = {
|
|
166
|
+
withDataUri: {
|
|
167
|
+
needs,
|
|
168
|
+
compute(modelData) {
|
|
169
|
+
return async () => {
|
|
170
|
+
const base64UploadFields = {};
|
|
171
|
+
for (const field of uploadFields) {
|
|
172
|
+
base64UploadFields[field] = await (0, import_fileToDataUri.fileToDataUri)(
|
|
173
|
+
modelData[field],
|
|
174
|
+
storageAdapter
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
// modelData is of type unknown at this point
|
|
179
|
+
...modelData,
|
|
180
|
+
...base64UploadFields
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
withSignedUrl: {
|
|
186
|
+
needs,
|
|
187
|
+
compute(modelData) {
|
|
188
|
+
return ({ expiresIn } = {}) => {
|
|
189
|
+
if (!urlSigner) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
"Please supply signed url settings in setupUpload()"
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
const signedUrlFields = {};
|
|
195
|
+
for (const field of uploadFields) {
|
|
196
|
+
if (!modelData[field]) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
signedUrlFields[field] = urlSigner.generateSignedUrl(
|
|
200
|
+
modelData[field],
|
|
201
|
+
expiresIn
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
// modelData is of type unknown at this point
|
|
206
|
+
...modelData,
|
|
207
|
+
...signedUrlFields
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return import_extension.Prisma.defineExtension((client) => {
|
|
215
|
+
return client.$extends({
|
|
216
|
+
name: "redwood-upload-prisma-plugin",
|
|
217
|
+
query: queryExtends,
|
|
218
|
+
result: resultExtends
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
async function removeUploadedFiles(fieldsToDelete, data) {
|
|
222
|
+
if (!data) {
|
|
223
|
+
console.warn("Empty data object passed to removeUploadedFiles");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
for (const field of fieldsToDelete) {
|
|
227
|
+
const uploadLocation = data?.[field];
|
|
228
|
+
if (uploadLocation) {
|
|
229
|
+
try {
|
|
230
|
+
await storageAdapter.remove(uploadLocation);
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
238
|
+
0 && (module.exports = {
|
|
239
|
+
createUploadsExtension
|
|
240
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SaveOptionsOverride, BaseStorageAdapter } from './adapters/BaseStorageAdapter.js';
|
|
2
|
+
import type { ModelNames, UploadsConfig } from './prismaExtension.js';
|
|
3
|
+
type MakeFilesString<T> = {
|
|
4
|
+
[K in keyof T]: T[K] extends File ? string : T[K];
|
|
5
|
+
};
|
|
6
|
+
export declare const createFileListSaver: (storage: BaseStorageAdapter) => (files?: File[], pathOverrideOnly?: {
|
|
7
|
+
path?: string;
|
|
8
|
+
}) => Promise<string[]>;
|
|
9
|
+
export declare const createUploadSavers: <MNames extends ModelNames = ModelNames>(uploadConfig: UploadsConfig<MNames>, storage: BaseStorageAdapter) => { [K in `for${Capitalize<MNames>}`]: <T extends Record<string, any>>(data: T, overrideSaveOptions?: SaveOptionsOverride) => Promise<MakeFilesString<T>>; } & {
|
|
10
|
+
inList: (files?: File[], pathOverrideOnly?: {
|
|
11
|
+
path?: string;
|
|
12
|
+
}) => Promise<string[]>;
|
|
13
|
+
};
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=createSavers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createSavers.d.ts","sourceRoot":"","sources":["../src/createSavers.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,mBAAmB,EACnB,kBAAkB,EACnB,MAAM,kCAAkC,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAGrE,KAAK,eAAe,CAAC,CAAC,IAAI;KACvB,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,IAAI,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;CAClD,CAAA;AAED,eAAO,MAAM,mBAAmB,YAAa,kBAAkB,cACxC,IAAI,EAAE,qBAA0B;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,sBAUvE,CAAA;AAMD,eAAO,MAAM,kBAAkB,GAAI,MAAM,SAAS,UAAU,6BAC5C,aAAa,CAAC,MAAM,CAAC,WAC1B,kBAAkB,QAMxB,CAAC,kCAAwB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,QAC/C,CAAC,wBACe,mBAAmB,KACtC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;qBA3Bb,IAAI,EAAE,qBAA0B;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;CA2EvE,CAAA"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ulid } from "ulid";
|
|
2
|
+
const createFileListSaver = (storage) => {
|
|
3
|
+
return async (files = [], pathOverrideOnly) => {
|
|
4
|
+
const locations = await Promise.all(
|
|
5
|
+
files.map(async (file) => {
|
|
6
|
+
const { location } = await storage.save(file, pathOverrideOnly);
|
|
7
|
+
return location;
|
|
8
|
+
})
|
|
9
|
+
);
|
|
10
|
+
return locations;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
const createUploadSavers = (uploadConfig, storage) => {
|
|
14
|
+
const savers = {};
|
|
15
|
+
Object.keys(uploadConfig).forEach((model) => {
|
|
16
|
+
const modelKey = model;
|
|
17
|
+
const currentModelConfig = uploadConfig[modelKey];
|
|
18
|
+
if (!currentModelConfig) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const currentModelUploadFields = Array.isArray(currentModelConfig.fields) ? currentModelConfig.fields : [currentModelConfig.fields];
|
|
22
|
+
const capitalCaseModel = `${model.charAt(0).toUpperCase() + model.slice(1)}`;
|
|
23
|
+
const saverKey = `for${capitalCaseModel}`;
|
|
24
|
+
savers[saverKey] = async (data, overrideSaveOptions) => {
|
|
25
|
+
const updatedFields = {};
|
|
26
|
+
for (const field of currentModelUploadFields) {
|
|
27
|
+
if (data[field]) {
|
|
28
|
+
const file = data[field];
|
|
29
|
+
const saveOptions = overrideSaveOptions || {
|
|
30
|
+
fileName: `${model}-${field}-${ulid()}`
|
|
31
|
+
};
|
|
32
|
+
const { location } = await storage.save(file, saveOptions);
|
|
33
|
+
updatedFields[field] = location;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
...data,
|
|
38
|
+
...updatedFields
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
...savers,
|
|
44
|
+
inList: createFileListSaver(storage)
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
export {
|
|
48
|
+
createFileListSaver,
|
|
49
|
+
createUploadSavers
|
|
50
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileToDataUri.d.ts","sourceRoot":"","sources":["../src/fileToDataUri.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAA;AAE1E,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,kBAAkB,mBAO5B"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
async function fileToDataUri(filePath, storage) {
|
|
2
|
+
const { contents, type: mimeType } = await storage.read(filePath);
|
|
3
|
+
const base64Data = Buffer.from(contents).toString("base64");
|
|
4
|
+
return `data:${mimeType};base64,${base64Data}`;
|
|
5
|
+
}
|
|
6
|
+
export {
|
|
7
|
+
fileToDataUri
|
|
8
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { BaseStorageAdapter } from './adapters/BaseStorageAdapter.js';
|
|
2
|
+
import type { ModelNames, UploadConfigForModel, UploadsConfig } from './prismaExtension.js';
|
|
3
|
+
import type { UrlSigner } from './UrlSigner.js';
|
|
4
|
+
type SetupStorageOptions<MNames extends ModelNames> = {
|
|
5
|
+
uploadsConfig: UploadsConfig<MNames>;
|
|
6
|
+
storageAdapter: BaseStorageAdapter;
|
|
7
|
+
urlSigner?: UrlSigner;
|
|
8
|
+
};
|
|
9
|
+
export declare const setupStorage: <MNames extends ModelNames>({ uploadsConfig, storageAdapter, urlSigner, }: SetupStorageOptions<MNames>) => {
|
|
10
|
+
storagePrismaExtension: (client: any) => import("@prisma/client/extension").PrismaClientExtends<import("@prisma/client/runtime/library").InternalArgs<{ [K in MNames]: {
|
|
11
|
+
withDataUri: {
|
|
12
|
+
needs: Record<string, boolean>;
|
|
13
|
+
compute: (modelData: Record<string, unknown>) => <T>(this: T) => Promise<T>;
|
|
14
|
+
};
|
|
15
|
+
withSignedUrl: {
|
|
16
|
+
needs: Record<string, boolean>;
|
|
17
|
+
compute: (modelData: Record<string, unknown>) => <T>(this: T, signArgs?: {
|
|
18
|
+
expiresIn?: number;
|
|
19
|
+
}) => T;
|
|
20
|
+
};
|
|
21
|
+
}; }, {}, {}, {}> & import("@prisma/client/runtime/library").DefaultArgs>;
|
|
22
|
+
saveFiles: { [K in `for${Capitalize<MNames>}`]: <T extends Record<string, any>>(data: T, overrideSaveOptions?: import("./adapters/BaseStorageAdapter.js").SaveOptionsOverride) => Promise<{ [K_1 in keyof T]: T[K_1] extends File ? string : T[K_1]; }>; } & {
|
|
23
|
+
inList: (files?: File[], pathOverrideOnly?: {
|
|
24
|
+
path?: string;
|
|
25
|
+
}) => Promise<string[]>;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* This utility function ensures that you receive accurate type suggestions for your savers.
|
|
30
|
+
* If you use the type UploadsConfig directly, you may receive suggestions for saveFiles.forY where Y hasn't been configured.
|
|
31
|
+
* By using this utility function, you will only receive suggestions for the models that you have configured.
|
|
32
|
+
*
|
|
33
|
+
* @param uploadsConfig The uploads configuration object.
|
|
34
|
+
* @returns The same uploads configuration object, but with filtered types
|
|
35
|
+
*/
|
|
36
|
+
export declare function createUploadsConfig<T extends Partial<{
|
|
37
|
+
[K in ModelNames]?: UploadConfigForModel<K>;
|
|
38
|
+
}>>(uploadsConfig: T): T;
|
|
39
|
+
export type { ModelNames, UploadsConfig } from './prismaExtension.js';
|
|
40
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAA;AAE1E,OAAO,KAAK,EACV,UAAU,EACV,oBAAoB,EACpB,aAAa,EACd,MAAM,sBAAsB,CAAA;AAE7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE/C,KAAK,mBAAmB,CAAC,MAAM,SAAS,UAAU,IAAI;IACpD,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACpC,cAAc,EAAE,kBAAkB,CAAA;IAClC,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB,CAAA;AAED,eAAO,MAAM,YAAY,GAAI,MAAM,SAAS,UAAU,iDAInD,mBAAmB,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;gBARV,CAAC;;;CAqBpB,CAAA;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,CAAC,SAAS,OAAO,CAAC;KACf,CAAC,IAAI,UAAU,CAAC,CAAC,EAAE,oBAAoB,CAAC,CAAC,CAAC;CAC5C,CAAC,EACF,aAAa,EAAE,CAAC,GAAG,CAAC,CAErB;AAED,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createUploadSavers } from "./createSavers.js";
|
|
2
|
+
import { createUploadsExtension } from "./prismaExtension.js";
|
|
3
|
+
const setupStorage = ({
|
|
4
|
+
uploadsConfig,
|
|
5
|
+
storageAdapter,
|
|
6
|
+
urlSigner
|
|
7
|
+
}) => {
|
|
8
|
+
const prismaExtension = createUploadsExtension(
|
|
9
|
+
uploadsConfig,
|
|
10
|
+
storageAdapter,
|
|
11
|
+
urlSigner
|
|
12
|
+
);
|
|
13
|
+
const saveFiles = createUploadSavers(uploadsConfig, storageAdapter);
|
|
14
|
+
return {
|
|
15
|
+
storagePrismaExtension: prismaExtension,
|
|
16
|
+
saveFiles
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
function createUploadsConfig(uploadsConfig) {
|
|
20
|
+
return uploadsConfig;
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
createUploadsConfig,
|
|
24
|
+
setupStorage
|
|
25
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PrismaClient } from '@prisma/client';
|
|
2
|
+
import type { Prisma } from '@prisma/client';
|
|
3
|
+
import type * as runtime from '@prisma/client/runtime/library';
|
|
4
|
+
import type { BaseStorageAdapter } from './adapters/BaseStorageAdapter.js';
|
|
5
|
+
import type { UrlSigner } from './UrlSigner.js';
|
|
6
|
+
type FilterOutDollarPrefixed<T> = T extends `$${string}` ? never : T extends symbol ? never : T;
|
|
7
|
+
export type ModelNames = FilterOutDollarPrefixed<keyof PrismaClient>;
|
|
8
|
+
type PrismaModelFields<MName extends ModelNames> = keyof Prisma.Result<PrismaClient[MName], any, 'findFirstOrThrow'>;
|
|
9
|
+
export type UploadConfigForModel<TPrismaModelName extends ModelNames> = {
|
|
10
|
+
fields: PrismaModelFields<TPrismaModelName> | PrismaModelFields<TPrismaModelName>[];
|
|
11
|
+
};
|
|
12
|
+
export type UploadsConfig<MNames extends ModelNames = ModelNames> = {
|
|
13
|
+
[K in MNames]?: UploadConfigForModel<K>;
|
|
14
|
+
};
|
|
15
|
+
type WithSignedUrlArgs = {
|
|
16
|
+
expiresIn?: number;
|
|
17
|
+
};
|
|
18
|
+
export declare const createUploadsExtension: <MNames extends ModelNames = ModelNames>(config: UploadsConfig<MNames>, storageAdapter: BaseStorageAdapter, urlSigner?: UrlSigner) => (client: any) => import("@prisma/client/extension").PrismaClientExtends<runtime.InternalArgs<{ [K in MNames]: {
|
|
19
|
+
withDataUri: {
|
|
20
|
+
needs: Record<string, boolean>;
|
|
21
|
+
compute: (modelData: Record<string, unknown>) => <T>(this: T) => Promise<T>;
|
|
22
|
+
};
|
|
23
|
+
withSignedUrl: {
|
|
24
|
+
needs: Record<string, boolean>;
|
|
25
|
+
compute: (modelData: Record<string, unknown>) => <T>(this: T, signArgs?: WithSignedUrlArgs) => T;
|
|
26
|
+
};
|
|
27
|
+
}; }, {}, {}, {}> & runtime.DefaultArgs>;
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=prismaExtension.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prismaExtension.d.ts","sourceRoot":"","sources":["../src/prismaExtension.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAE5C,OAAO,KAAK,KAAK,OAAO,MAAM,gCAAgC,CAAA;AAE9D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAA;AAE1E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE/C,KAAK,uBAAuB,CAAC,CAAC,IAAI,CAAC,SAAS,IAAI,MAAM,EAAE,GACpD,KAAK,GACL,CAAC,SAAS,MAAM,GACd,KAAK,GACL,CAAC,CAAA;AAGP,MAAM,MAAM,UAAU,GAAG,uBAAuB,CAAC,MAAM,YAAY,CAAC,CAAA;AAEpE,KAAK,iBAAiB,CAAC,KAAK,SAAS,UAAU,IAAI,MAAM,MAAM,CAAC,MAAM,CACpE,YAAY,CAAC,KAAK,CAAC,EACnB,GAAG,EACH,kBAAkB,CACnB,CAAA;AAED,MAAM,MAAM,oBAAoB,CAAC,gBAAgB,SAAS,UAAU,IAAI;IACtE,MAAM,EACF,iBAAiB,CAAC,gBAAgB,CAAC,GACnC,iBAAiB,CAAC,gBAAgB,CAAC,EAAE,CAAA;CAC1C,CAAA;AAED,MAAM,MAAM,aAAa,CAAC,MAAM,SAAS,UAAU,GAAG,UAAU,IAAI;KACjE,CAAC,IAAI,MAAM,CAAC,CAAC,EAAE,oBAAoB,CAAC,CAAC,CAAC;CACxC,CAAA;AAED,KAAK,iBAAiB,GAAG;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,eAAO,MAAM,sBAAsB,GAAI,MAAM,SAAS,UAAU,uBACtD,aAAa,CAAC,MAAM,CAAC,kBACb,kBAAkB,cACtB,SAAS,qGAOlB,CAAC;iBACa;QACX,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QAC9B,OAAO,EAAE,CACP,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC/B,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAA;KAChC;mBACc;QACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QAC9B,OAAO,EAAE,CACP,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC/B,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,CAAC,EAAE,iBAAiB,KAAK,CAAC,CAAA;KACrD;wCA4RN,CAAA"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { PrismaClient } from "@prisma/client";
|
|
2
|
+
import { Prisma as PrismaExtension } from "@prisma/client/extension";
|
|
3
|
+
import { fileToDataUri } from "./fileToDataUri.js";
|
|
4
|
+
const createUploadsExtension = (config, storageAdapter, urlSigner) => {
|
|
5
|
+
const prismaInstance = new PrismaClient();
|
|
6
|
+
const queryExtends = {};
|
|
7
|
+
const resultExtends = {};
|
|
8
|
+
for (const modelName in config) {
|
|
9
|
+
const modelConfig = config[modelName];
|
|
10
|
+
if (!modelConfig) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
const uploadFields = Array.isArray(modelConfig.fields) ? modelConfig.fields : [modelConfig.fields];
|
|
14
|
+
queryExtends[modelName] = {
|
|
15
|
+
async create({ query, args }) {
|
|
16
|
+
try {
|
|
17
|
+
const result = await query(args);
|
|
18
|
+
return result;
|
|
19
|
+
} catch (e) {
|
|
20
|
+
await removeUploadedFiles(
|
|
21
|
+
uploadFields,
|
|
22
|
+
args.data
|
|
23
|
+
);
|
|
24
|
+
throw e;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
async createMany({ query, args }) {
|
|
28
|
+
try {
|
|
29
|
+
const result = await query(args);
|
|
30
|
+
return result;
|
|
31
|
+
} catch (e) {
|
|
32
|
+
const createDatas = args.data;
|
|
33
|
+
for (const createData of createDatas) {
|
|
34
|
+
await removeUploadedFiles(uploadFields, createData);
|
|
35
|
+
}
|
|
36
|
+
throw e;
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
async update({ query, model, args }) {
|
|
40
|
+
const uploadFieldsToUpdate = uploadFields.filter(
|
|
41
|
+
(field) => (
|
|
42
|
+
// All of this non-sense is to make typescript happy. I'm not sure how data could be anything but an object
|
|
43
|
+
typeof args.data === "object" && args.data !== null && field in args.data
|
|
44
|
+
)
|
|
45
|
+
);
|
|
46
|
+
if (uploadFieldsToUpdate.length == 0) {
|
|
47
|
+
return query(args);
|
|
48
|
+
} else {
|
|
49
|
+
const originalRecord = await prismaInstance[
|
|
50
|
+
model
|
|
51
|
+
// @ts-expect-error TS in strict mode will error due to union type. We cannot narrow it down here.
|
|
52
|
+
].findFirstOrThrow({
|
|
53
|
+
where: args.where
|
|
54
|
+
// @TODO: should we select here to reduce the amount of data we're handling
|
|
55
|
+
});
|
|
56
|
+
try {
|
|
57
|
+
const result = await query(args);
|
|
58
|
+
await removeUploadedFiles(uploadFieldsToUpdate, originalRecord);
|
|
59
|
+
return result;
|
|
60
|
+
} catch (e) {
|
|
61
|
+
await removeUploadedFiles(
|
|
62
|
+
uploadFieldsToUpdate,
|
|
63
|
+
args.data
|
|
64
|
+
);
|
|
65
|
+
throw e;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
async updateMany({ query, model, args }) {
|
|
70
|
+
const uploadFieldsToUpdate = uploadFields.filter(
|
|
71
|
+
(field) => (
|
|
72
|
+
// All of this non-sense is to make typescript happy. I'm not sure how data could be anything but an object
|
|
73
|
+
typeof args.data === "object" && args.data !== null && field in args.data
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
if (uploadFieldsToUpdate.length == 0) {
|
|
77
|
+
return query(args);
|
|
78
|
+
} else {
|
|
79
|
+
const originalRecords = await prismaInstance[
|
|
80
|
+
model
|
|
81
|
+
// @ts-expect-error TS in strict mode will error due to union type. We cannot narrow it down here.
|
|
82
|
+
].findMany({
|
|
83
|
+
where: args.where
|
|
84
|
+
// @TODO: should we select here to reduce the amount of data we're handling
|
|
85
|
+
});
|
|
86
|
+
try {
|
|
87
|
+
const result = await query(args);
|
|
88
|
+
for await (const originalRecord of originalRecords) {
|
|
89
|
+
await removeUploadedFiles(uploadFieldsToUpdate, originalRecord);
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
} catch (e) {
|
|
93
|
+
await removeUploadedFiles(
|
|
94
|
+
uploadFieldsToUpdate,
|
|
95
|
+
args.data
|
|
96
|
+
);
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
async upsert({ query, model, args }) {
|
|
102
|
+
let isUpdate;
|
|
103
|
+
const uploadFieldsToUpdate = uploadFields.filter(
|
|
104
|
+
(field) => typeof args.update === "object" && args.update !== null && field in args.update
|
|
105
|
+
);
|
|
106
|
+
try {
|
|
107
|
+
let existingRecord;
|
|
108
|
+
if (args.update) {
|
|
109
|
+
existingRecord = await prismaInstance[
|
|
110
|
+
model
|
|
111
|
+
// @ts-expect-error TS in strict mode will error due to union type. We cannot narrow it down here.
|
|
112
|
+
].findUnique({
|
|
113
|
+
where: args.where
|
|
114
|
+
});
|
|
115
|
+
isUpdate = !!existingRecord;
|
|
116
|
+
}
|
|
117
|
+
const result = await query(args);
|
|
118
|
+
if (isUpdate && existingRecord) {
|
|
119
|
+
await removeUploadedFiles(uploadFieldsToUpdate, existingRecord);
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
} catch (e) {
|
|
123
|
+
await removeUploadedFiles(
|
|
124
|
+
// Only delete files we're updating on update
|
|
125
|
+
isUpdate ? uploadFieldsToUpdate : uploadFields,
|
|
126
|
+
isUpdate ? args.update : args.create
|
|
127
|
+
);
|
|
128
|
+
throw e;
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
async delete({ query, args }) {
|
|
132
|
+
const deleteResult = await query(args);
|
|
133
|
+
await removeUploadedFiles(
|
|
134
|
+
uploadFields,
|
|
135
|
+
// We don't know the exact type here
|
|
136
|
+
deleteResult
|
|
137
|
+
);
|
|
138
|
+
return deleteResult;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const needs = Object.fromEntries(uploadFields.map((field) => [field, true]));
|
|
142
|
+
resultExtends[modelName] = {
|
|
143
|
+
withDataUri: {
|
|
144
|
+
needs,
|
|
145
|
+
compute(modelData) {
|
|
146
|
+
return async () => {
|
|
147
|
+
const base64UploadFields = {};
|
|
148
|
+
for (const field of uploadFields) {
|
|
149
|
+
base64UploadFields[field] = await fileToDataUri(
|
|
150
|
+
modelData[field],
|
|
151
|
+
storageAdapter
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
// modelData is of type unknown at this point
|
|
156
|
+
...modelData,
|
|
157
|
+
...base64UploadFields
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
withSignedUrl: {
|
|
163
|
+
needs,
|
|
164
|
+
compute(modelData) {
|
|
165
|
+
return ({ expiresIn } = {}) => {
|
|
166
|
+
if (!urlSigner) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
"Please supply signed url settings in setupUpload()"
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const signedUrlFields = {};
|
|
172
|
+
for (const field of uploadFields) {
|
|
173
|
+
if (!modelData[field]) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
signedUrlFields[field] = urlSigner.generateSignedUrl(
|
|
177
|
+
modelData[field],
|
|
178
|
+
expiresIn
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
// modelData is of type unknown at this point
|
|
183
|
+
...modelData,
|
|
184
|
+
...signedUrlFields
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return PrismaExtension.defineExtension((client) => {
|
|
192
|
+
return client.$extends({
|
|
193
|
+
name: "redwood-upload-prisma-plugin",
|
|
194
|
+
query: queryExtends,
|
|
195
|
+
result: resultExtends
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
async function removeUploadedFiles(fieldsToDelete, data) {
|
|
199
|
+
if (!data) {
|
|
200
|
+
console.warn("Empty data object passed to removeUploadedFiles");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
for (const field of fieldsToDelete) {
|
|
204
|
+
const uploadLocation = data?.[field];
|
|
205
|
+
if (uploadLocation) {
|
|
206
|
+
try {
|
|
207
|
+
await storageAdapter.remove(uploadLocation);
|
|
208
|
+
} catch {
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
export {
|
|
215
|
+
createUploadsExtension
|
|
216
|
+
};
|