@proteinjs/db-file 1.10.0 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,205 @@
1
+ import { getDbAsSystem } from '@proteinjs/db';
2
+ import { SourceRepository } from '@proteinjs/reflection';
3
+ import { UserAuth, UserRepo, User } from '@proteinjs/user';
4
+ import { File } from '../src/tables/FileTable';
5
+ import { tables } from '../src/tables/tables';
6
+ import { FileStorage } from '../src/FileStorage';
7
+ import { FileStorageDriver } from '../src/FileStorageDriver';
8
+ import { FileVariantKind } from '../src/FileVariantMaker';
9
+ import { FileTestEnvironment } from './FileTestEnvironment';
10
+
11
+ /**
12
+ * A derived File of a picture — a variant the seam makes (the preview at ingest, the stage on the
13
+ * read path's first request) and the copy for others — is a RENDITION of the picture's pixels, so
14
+ * it carries what the picture's pixels are: whether they are see-through (`transparent`). A
15
+ * thumbnail of a mark made on a transparent background is see-through (the maker resizes, it does
16
+ * not flatten); a photograph's thumbnail is opaque, as the photograph is; a picture nothing probed
17
+ * derives renditions with no answer — nothing is invented. The library that makes the row writes
18
+ * it, beside the provenance it carries the same way (`FileTable.renditionColumns` beside
19
+ * `FileTable.provenanceColumns`), so a consumer seats a rendition on the surface its picture sat
20
+ * on by reading the row it draws, without a join to the original. The maker's own answer (the
21
+ * bytes, their type, their dimensions) stays the rendition's own.
22
+ */
23
+
24
+ const originalBytes = Buffer.concat([Buffer.from('ORIGINAL:'), Buffer.from(Array.from({ length: 64 }, (_, i) => i))]);
25
+ /** What the copy-for-others maker takes off a copy (the location marker of the real one). */
26
+ const COPY_MARK = Buffer.from('-FOR-OTHERS');
27
+
28
+ /** In-memory proxy-shape driver (no signed URLs) — the DbFileStorageDriver serving shape. */
29
+ class ProxyDriver implements FileStorageDriver {
30
+ readonly store = new Map<string, Buffer>();
31
+
32
+ async createFile(file: File, fileData: string): Promise<void> {
33
+ this.store.set(file.id, Buffer.from(fileData, 'base64'));
34
+ }
35
+
36
+ async getFileData(fileId: string): Promise<string> {
37
+ const data = this.store.get(fileId);
38
+ if (data === undefined) {
39
+ throw new Error(`No such object: ${fileId}`);
40
+ }
41
+ return data.toString('base64');
42
+ }
43
+
44
+ async updateFileData(fileId: string, data: string): Promise<void> {
45
+ this.store.set(fileId, Buffer.from(data, 'base64'));
46
+ }
47
+
48
+ async deleteFile(fileId: string): Promise<void> {
49
+ this.store.delete(fileId);
50
+ }
51
+ }
52
+
53
+ const testEnv = new FileTestEnvironment();
54
+ type UserAuthInternals = { userRepo?: unknown };
55
+ type SourceRepositoryInternals = { objectCache: Record<string, unknown[]> };
56
+ const objectCache = () => (SourceRepository.get() as unknown as SourceRepositoryInternals).objectCache;
57
+
58
+ let owner: User;
59
+ let recipient: User;
60
+ const reachableFileIds = new Set<string>();
61
+
62
+ /** The stub variant maker: a picture's `preview` and `stage` — bytes marked with the kind, a type, dimensions; it answers nothing about the pixels' alpha. */
63
+ const registerVariantMaker = () => {
64
+ objectCache()['@proteinjs/db-file/FileVariantMaker'] = [
65
+ {
66
+ appliesTo: (file: File) => file.type.startsWith('image/'),
67
+ make: async (file: File, bytes: Buffer, kind: FileVariantKind) => ({
68
+ bytes: Buffer.concat([Buffer.from(`${kind.toUpperCase()}:`), bytes.subarray(0, 8), COPY_MARK]),
69
+ type: 'image/webp',
70
+ width: kind === 'stage' ? 1600 : 512,
71
+ height: kind === 'stage' ? 800 : 256,
72
+ }),
73
+ },
74
+ ];
75
+ };
76
+
77
+ /** The stub copy maker: a picture's copy is its bytes with the mark taken off. */
78
+ const registerCopyMaker = () => {
79
+ objectCache()['@proteinjs/db-file/FileCopyForOthers'] = [
80
+ {
81
+ appliesTo: (file: File) => file.type.startsWith('image/'),
82
+ make: async (file: File, bytes: Buffer) => bytes.subarray(0, bytes.length - COPY_MARK.length),
83
+ },
84
+ ];
85
+ };
86
+
87
+ beforeAll(async () => {
88
+ await testEnv.beforeAll();
89
+ objectCache()['@proteinjs/user-auth/AuthenticatedUserRepo'] = [new UserRepo()];
90
+ (UserAuth as unknown as UserAuthInternals).userRepo = undefined;
91
+ objectCache()['@proteinjs/db-file/FileReachabilityResolver'] = [
92
+ // The content leg vouches for the recipient alone (a share they accepted).
93
+ {
94
+ canReadViaReference: async (fileId: string) =>
95
+ reachableFileIds.has(fileId) && new UserRepo().getUser().id === recipient.id,
96
+ },
97
+ ];
98
+ owner = await testEnv.createUser({ name: 'File owner', email: 'transparent-owner@test.local' });
99
+ recipient = await testEnv.createUser({ name: 'Share recipient', email: 'transparent-recipient@test.local' });
100
+ testEnv.actAs(owner);
101
+ });
102
+
103
+ afterAll(async () => {
104
+ delete objectCache()['@proteinjs/db-file/FileReachabilityResolver'];
105
+ delete objectCache()['@proteinjs/db-file/FileVariantMaker'];
106
+ delete objectCache()['@proteinjs/db-file/FileCopyForOthers'];
107
+ (UserAuth as unknown as UserAuthInternals).userRepo = undefined;
108
+ await testEnv.afterAll();
109
+ });
110
+
111
+ const driver = new ProxyDriver();
112
+
113
+ beforeEach(() => {
114
+ reachableFileIds.clear();
115
+ registerVariantMaker();
116
+ registerCopyMaker();
117
+ testEnv.setDriver(driver);
118
+ testEnv.actAs(owner);
119
+ });
120
+
121
+ const pictureBytes = Buffer.concat([originalBytes, COPY_MARK]);
122
+
123
+ /** A picture of the owner's — with the answer ingest read off its pixels, or without one — stored with no variant. */
124
+ const createOwnerPicture = async (name: string, pixels: Pick<File, 'transparent'> = {}): Promise<File> =>
125
+ await new FileStorage().createFile(
126
+ { name, type: 'image/png', size: pictureBytes.length, width: 1024, height: 1024, ...pixels } as File,
127
+ pictureBytes.toString('base64')
128
+ );
129
+
130
+ const rowAsSystem = async (fileId: string): Promise<File> => (await getDbAsSystem().get(tables.File, { id: fileId }))!;
131
+
132
+ /** The answer a row carries — `null` for a row that carries none. */
133
+ const transparentOf = (row: File): boolean | null => row.transparent ?? null;
134
+
135
+ /** The copy for others of a file, made by a recipient's read of its bytes. */
136
+ const copyForOthersOf = async (file: File): Promise<File> => {
137
+ reachableFileIds.add(file.id);
138
+ testEnv.actAs(recipient);
139
+ await new FileStorage().getFileData(file.id);
140
+ testEnv.actAs(owner);
141
+ const copyId = (await rowAsSystem(file.id)).copyForOthers?._id;
142
+ expect(copyId).toBeTruthy();
143
+ return await rowAsSystem(copyId!);
144
+ };
145
+
146
+ describe('a rendition of a picture carries whether its pixels are see-through', () => {
147
+ it('the ingest door: the preview and the stage deriveVariants makes of a see-through mark read `transparent` true off their own rows; the maker’s own answer stays its own', async () => {
148
+ const mark = await createOwnerPicture('mark.png', { transparent: true });
149
+
150
+ const { variants } = await new FileStorage().deriveVariants(mark, pictureBytes);
151
+
152
+ expect(Object.keys(variants).sort()).toEqual(['preview', 'stage']);
153
+ for (const kind of ['preview', 'stage'] as const) {
154
+ const variant = await rowAsSystem(variants[kind]!.id);
155
+ expect(transparentOf(variant)).toBe(true);
156
+ expect([variant.type, variant.width, variant.height]).toEqual([
157
+ 'image/webp',
158
+ kind === 'stage' ? 1600 : 512,
159
+ kind === 'stage' ? 800 : 256,
160
+ ]);
161
+ }
162
+ });
163
+
164
+ it('the read path’s door: the stage getVariant derives on the first request reads true', async () => {
165
+ const mark = await createOwnerPicture('existing-mark.png', { transparent: true });
166
+ expect((await rowAsSystem(mark.id)).stage?._id ?? null).toBeNull();
167
+
168
+ const stage = (await new FileStorage().getVariant(mark.id, 'stage'))!;
169
+
170
+ expect(stage.variantOf?._id).toEqual(mark.id);
171
+ expect(transparentOf(await rowAsSystem(stage.id))).toBe(true);
172
+ });
173
+
174
+ it('the copy for others reads true — the same pixels, the metadata off', async () => {
175
+ const mark = await createOwnerPicture('shared-mark.png', { transparent: true });
176
+
177
+ const copy = await copyForOthersOf(mark);
178
+
179
+ expect(copy.scope).toEqual(owner.id);
180
+ expect(transparentOf(copy)).toBe(true);
181
+ });
182
+
183
+ it('an opaque picture’s renditions read false — the answer travels as it is, never dropped to unset', async () => {
184
+ const photo = await createOwnerPicture('photo.png', { transparent: false });
185
+
186
+ const { variants } = await new FileStorage().deriveVariants(photo, pictureBytes);
187
+ const copy = await copyForOthersOf(photo);
188
+
189
+ expect(transparentOf(await rowAsSystem(variants.preview!.id))).toBe(false);
190
+ expect(transparentOf(await rowAsSystem(variants.stage!.id))).toBe(false);
191
+ expect(transparentOf(copy)).toBe(false);
192
+ });
193
+
194
+ it('a picture nothing probed derives renditions with no answer — nothing is invented, at any door', async () => {
195
+ const unprobed = await createOwnerPicture('unprobed.png');
196
+ expect(transparentOf(await rowAsSystem(unprobed.id))).toBeNull();
197
+
198
+ const { variants } = await new FileStorage().deriveVariants(unprobed, pictureBytes);
199
+ const copy = await copyForOthersOf(unprobed);
200
+
201
+ expect(transparentOf(await rowAsSystem(variants.preview!.id))).toBeNull();
202
+ expect(transparentOf(await rowAsSystem(variants.stage!.id))).toBeNull();
203
+ expect(transparentOf(copy)).toBeNull();
204
+ });
205
+ });