Skip to content

Commit 5a64efe

Browse files
authored
Merge pull request #7 from devcodex-labs/cursor/gridfs-error-policy-5906
fix(gridfs): map driver errors and match write-path iid
2 parents aec3b9c + aa1b70f commit 5a64efe

11 files changed

Lines changed: 225 additions & 28 deletions

File tree

‎changelogs/unreleased.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
- Add GridFS buckets on `msq.gridfs()` and `model.gridfs()`, including upload, download, ranges, `ensureIndexes`, and phase-1 resumable upload. `writePathPolicy` gains a `gridfs` field and category. Policy violations stay `INVALID_OPERATION` with `reason: 'WRITE_PATH_POLICY'`. These notes stage the work after 3.4.0; the npm package version is unchanged and this file is not a 3.5.0 release. The Search index lifecycle API remains in the [3.4.0 versioned notes](./v3.4.0.md).
66
- Raise the CJS and ESM runtime package budgets for GridFS 3.5.0. `gridfs()` stays synchronous, so the single-file build keeps that code in the main bundle. The CJS ceiling is 873000 bytes after `contentType` moved onto file metadata.
7+
- Map driver failures from GridFS `getFile`, `findFiles`, `abortResumableUpload`, and index creation to `MONGODB_ERROR`, with the driver error kept as `cause`. Existing `GRIDFS_*` errors stay unchanged.
8+
- Pass the collection-style internal instance id (`iid`) into GridFS `writePathPolicy` matching. Model-only reads stay allowed, and writes still require `gridfs: 'allow'`.
9+
- Raise the CJS runtime ceiling from 873000 to 874000 bytes and the ESM ceiling from 870000 to 871000 bytes for that driver-error mapping and iid matching. The CLI budget stays 50000 bytes.
710

811
## Archived preparation notes for the 3.3.2 candidate
912

‎config/package-budget.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
2-
"comment": "GridFS 3.5.0 stays in the main bundle: sync gridfs() cannot be split out of the single-file build.",
2+
"comment": "GridFS 3.5.0 stays in the main bundle: sync gridfs() cannot be split out of the single-file build. CJS 874000 and ESM 871000 cover driver-error mapping and write-path iid matching.",
33
"artifacts": {
4-
"dist/cjs/index.cjs": 873000,
5-
"dist/esm/index.mjs": 870000,
4+
"dist/cjs/index.cjs": 874000,
5+
"dist/esm/index.mjs": 871000,
66
"dist/cjs/cli/data-task.cjs": 50000
77
}
88
}

‎src/capabilities/gridfs/bucket.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ import type {
1818
import { deleteFile, downloadFile, dropBucket, findFiles, getFile, openDownloadStream, openUploadStream, renameFile, uploadFile } from './operations';
1919
import { abortResumableUpload, appendChunks, beginResumableUpload, finalizeResumableUpload } from './resumable';
2020

21+
/** Same iid as collection write paths (`instanceId` prefix, then `pool:db.collection` or `db:collection`). */
22+
function collectionWritePathIid(instanceId: string | undefined, poolName: string | undefined, dbName: string, collection: string): string {
23+
const scopedName = poolName ? `${poolName}:${dbName}.${collection}` : `${dbName}:${collection}`;
24+
return instanceId ? `${instanceId}:${scopedName}` : scopedName;
25+
}
26+
2127
export class MonSQLizeGridFSBucketImpl {
2228
constructor(private readonly ctx: GridFSContext) {}
2329

@@ -96,12 +102,14 @@ export class MonSQLizeGridFSBucketImpl {
96102
}
97103

98104
private assertAllowed(operation: string, category: 'gridfs' | 'management'): void {
105+
const collection = `${this.ctx.bucketName}.files`;
99106
assertWritePathAllowed({
100107
policy: this.ctx.policy,
101108
namespace: {
109+
iid: collectionWritePathIid(this.ctx.instanceId, this.ctx.poolName, this.ctx.dbName, collection),
102110
...(this.ctx.poolName ? { pool: this.ctx.poolName } : {}),
103111
db: this.ctx.dbName,
104-
collection: `${this.ctx.bucketName}.files`,
112+
collection,
105113
},
106114
source: this.ctx.source,
107115
operation,

‎src/capabilities/gridfs/errors.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export function isDriverFileNotFound(error: unknown): boolean {
130130

131131
export function mapDriverError(error: unknown, operation: string, ctx: Pick<GridFSContext, 'bucketName'>, target: { id?: ObjectId; filename?: string; uploadId?: ObjectId } = {}): MonSQLizeError {
132132
if (isGridFSDomainError(error)) return error;
133+
if (error instanceof Error && (error as MonSQLizeError).code === ErrorCodes.MONGODB_ERROR) return error as MonSQLizeError;
133134
const cause = error instanceof Error ? error : new Error(String(error));
134135
if (isDriverFileNotFound(error)) {
135136
return fileNotFound(operation, ctx, target);

‎src/capabilities/gridfs/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface CreateGridFSBucketDeps {
1212
bucketName: string;
1313
dbName: string;
1414
poolName?: string;
15+
instanceId?: string;
1516
chunkSizeBytes?: number;
1617
source: 'db' | 'model';
1718
policy?: NormalizedWritePathPolicy;
@@ -30,6 +31,7 @@ export function createMonSQLizeGridFSBucket(deps: CreateGridFSBucketDeps): MonSQ
3031
bucketName: deps.bucketName,
3132
dbName: deps.dbName,
3233
poolName: deps.poolName,
34+
instanceId: deps.instanceId,
3335
chunkSizeBytes,
3436
source: deps.source,
3537
policy: deps.policy,

‎src/capabilities/gridfs/indexes.ts‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Db } from 'mongodb';
2-
import { UPLOADS_TTL_SECONDS } from './types';
2+
import { mapDriverError } from './errors';
33
import { chunksCollectionName, filesCollectionName, uploadsCollectionName } from './normalize';
4+
import { UPLOADS_TTL_SECONDS } from './types';
45

56
/**
67
* Idempotent GridFS indexes for one bucket instance.
@@ -28,9 +29,13 @@ export class GridFSIndexLatch {
2829
const files = this.db.collection(filesCollectionName(this.bucketName));
2930
const chunks = this.db.collection(chunksCollectionName(this.bucketName));
3031
const uploads = this.db.collection(uploadsCollectionName(this.bucketName));
31-
await files.createIndex({ filename: 1, uploadDate: 1 });
32-
await chunks.createIndex({ files_id: 1, n: 1 }, { unique: true });
33-
await files.createIndex({ 'metadata.sha256': 1 });
34-
await uploads.createIndex({ createdAt: 1 }, { expireAfterSeconds: UPLOADS_TTL_SECONDS });
32+
try {
33+
await files.createIndex({ filename: 1, uploadDate: 1 });
34+
await chunks.createIndex({ files_id: 1, n: 1 }, { unique: true });
35+
await files.createIndex({ 'metadata.sha256': 1 });
36+
await uploads.createIndex({ createdAt: 1 }, { expireAfterSeconds: UPLOADS_TTL_SECONDS });
37+
} catch (error) {
38+
throw mapDriverError(error, 'ensureIndexes', { bucketName: this.bucketName });
39+
}
3540
}
3641
}

‎src/capabilities/gridfs/operations.ts‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -157,19 +157,27 @@ export function openDownloadStream(ctx: GridFSContext, idOrName: GridFSIdOrName,
157157

158158
export async function getFile(ctx: GridFSContext, id: ObjectId): Promise<GridFSFileInfo | null> {
159159
assertObjectId(id, 'getFile', 'id');
160-
const doc = await ctx.db.collection(filesCollectionName(ctx.bucketName)).findOne({ _id: id });
161-
return doc ? toFileInfo(doc as Record<string, unknown>) : null;
160+
try {
161+
const doc = await ctx.db.collection(filesCollectionName(ctx.bucketName)).findOne({ _id: id });
162+
return doc ? toFileInfo(doc as Record<string, unknown>) : null;
163+
} catch (error) {
164+
throw mapDriverError(error, 'getFile', ctx, { id });
165+
}
162166
}
163167

164168
export async function findFiles(ctx: GridFSContext, filter?: GridFSFindFilesFilter, opts?: GridFSFindFilesOptions): Promise<GridFSFileInfo[]> {
165169
const query = sanitizeFilter(filter, 'findFiles');
166170
assertFindOptions(opts);
167-
let cursor = ctx.db.collection(filesCollectionName(ctx.bucketName)).find(query);
168-
if (opts?.sort) cursor = cursor.sort(opts.sort as Sort);
169-
if (opts?.skip) cursor = cursor.skip(opts.skip);
170-
if (opts?.limit) cursor = cursor.limit(opts.limit);
171-
const docs = await cursor.toArray();
172-
return docs.map((doc) => toFileInfo(doc as Record<string, unknown>));
171+
try {
172+
let cursor = ctx.db.collection(filesCollectionName(ctx.bucketName)).find(query);
173+
if (opts?.sort) cursor = cursor.sort(opts.sort as Sort);
174+
if (opts?.skip) cursor = cursor.skip(opts.skip);
175+
if (opts?.limit) cursor = cursor.limit(opts.limit);
176+
const docs = await cursor.toArray();
177+
return docs.map((doc) => toFileInfo(doc as Record<string, unknown>));
178+
} catch (error) {
179+
throw mapDriverError(error, 'findFiles', ctx);
180+
}
173181
}
174182

175183
export async function deleteFile(ctx: GridFSContext, id: ObjectId, opts?: GridFSDeleteOptions): Promise<void> {

‎src/capabilities/gridfs/resumable.ts‎

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -177,17 +177,21 @@ export async function finalizeResumableUpload(ctx: GridFSContext, uploadId: Obje
177177

178178
export async function abortResumableUpload(ctx: GridFSContext, uploadId: ObjectId): Promise<void> {
179179
assertObjectId(uploadId, 'abortResumableUpload', 'uploadId');
180-
const uploads = ctx.db.collection(uploadsCollectionName(ctx.bucketName));
181-
const session = await uploads.findOne({ _id: uploadId });
182-
if (!session || session.status === 'aborted' || session.status === 'finalized') return;
183-
const visible = await ctx.db.collection(filesCollectionName(ctx.bucketName)).findOne(
184-
{ _id: uploadId },
185-
{ projection: { _id: 1 } },
186-
);
187-
if (!visible) {
188-
await ctx.db.collection(chunksCollectionName(ctx.bucketName)).deleteMany({ files_id: uploadId });
180+
try {
181+
const uploads = ctx.db.collection(uploadsCollectionName(ctx.bucketName));
182+
const session = await uploads.findOne({ _id: uploadId });
183+
if (!session || session.status === 'aborted' || session.status === 'finalized') return;
184+
const visible = await ctx.db.collection(filesCollectionName(ctx.bucketName)).findOne(
185+
{ _id: uploadId },
186+
{ projection: { _id: 1 } },
187+
);
188+
if (!visible) {
189+
await ctx.db.collection(chunksCollectionName(ctx.bucketName)).deleteMany({ files_id: uploadId });
190+
}
191+
await uploads.deleteOne({ _id: uploadId });
192+
} catch (error) {
193+
throw mapDriverError(error, 'abortResumableUpload', ctx, { uploadId });
189194
}
190-
await uploads.deleteOne({ _id: uploadId });
191195
}
192196

193197
async function payloadBytes(data: Buffer | Uint8Array | Readable): Promise<Buffer> {

‎src/capabilities/gridfs/types.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ export interface GridFSContext {
114114
bucketName: string;
115115
dbName: string;
116116
poolName?: string;
117+
/** Configured namespace instance id. The write-path iid is derived the same way as collection writes. */
118+
instanceId?: string;
117119
chunkSizeBytes: number;
118120
source: Extract<WritePathSource, 'db' | 'model'>;
119121
policy?: NormalizedWritePathPolicy;

‎src/entry/runtime-gridfs.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ function openGridFS(runtime: unknown, request: {
6464
bucketName,
6565
dbName,
6666
poolName: request.poolName,
67+
instanceId: state._runtimeDefaults?.namespace?.instanceId,
6768
chunkSizeBytes: request.chunkSizeBytes,
6869
source: request.source,
6970
policy: state._runtimeDefaults?.writePathPolicy,

0 commit comments

Comments
 (0)