UNPKG

longcelot-sheet-db

Version:

Google Sheets-backed staging database adapter for Node.js with schema-first design

87 lines 4.55 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DriveStorageAdapter = void 0; const SchemaError_1 = require("../errors/SchemaError"); const driveTenancy_1 = require("./driveTenancy"); const driveMedia_1 = require("../utils/driveMedia"); class DriveStorageAdapter { constructor(options) { /** Cached Drive folder IDs, keyed by a scope-namespaced path so an actor-owned client's folders * never collide with the shared admin client's folders of the same name. */ this._folderCache = new Map(); /** Cached actor-owned SheetClients, keyed by userId — avoids rebuilding one per upload. */ this._actorClientCache = new Map(); this.defaultFolder = options?.folder ?? 'uploads'; } /** Called by SheetAdapter at construction time — injects the shared SheetClient + tenancy config. */ _setClient(client, tenancy) { this._client = client; this._tenancy = tenancy; } get client() { if (!this._client) { throw new SchemaError_1.SchemaError('DriveStorageAdapter has no client. Pass it as the storage option to createSheetAdapter().'); } return this._client; } async upload(file, options) { const actorContext = options.actorContext; const { client, scopeKey } = await this.resolveClient(actorContext); const baseFolderId = this._tenancy && actorContext ? await (0, driveTenancy_1.resolveRoleFolder)(client, this._tenancy.driveFolder, actorContext.actor, this._tenancy.sharedDriveId, this._folderCache, `${scopeKey}:role:${actorContext.actor}`) : undefined; const folderId = await this.resolveFolder(options.folder ?? this.defaultFolder, client, this._tenancy?.sharedDriveId, baseFolderId, scopeKey); const fileId = await client.uploadFile(file, options.filename, options.mimeType, folderId, options.public); if (options.linkFormat === 'download') { return (0, driveMedia_1.buildDriveDownloadUrl)(fileId); } return (0, driveMedia_1.buildDriveViewUrl)(fileId, (0, driveMedia_1.classifyDriveMediaKind)(options.mimeType)); } async delete(url, actorContext) { const fileId = (0, driveMedia_1.extractDriveFileId)(url); if (!fileId) return; const { client } = await this.resolveClient(actorContext); await client.deleteFile(fileId); } /** * Resolves which client (and folder-cache scope) an upload/delete should use for `actorContext`: * an actor on the actor-owned sheet model (resolved via `tokenStore`, same as `createUserSheet()`) * gets their own Drive; everyone else (no context, `actor: 'admin'`, or no tenancy config at all) * shares the single admin/default client — the pre-Phase-23 behavior. */ async resolveClient(actorContext) { if (!actorContext || actorContext.actor === 'admin' || !this._tenancy) { return { client: this.client, scopeKey: 'admin' }; } const cached = this._actorClientCache.get(actorContext.userId); if (cached) return { client: cached, scopeKey: `actor:${actorContext.userId}` }; const resolved = await (0, driveTenancy_1.resolveActorClient)(actorContext.userId, this.client, this._tenancy.credentials, this._tenancy.cacheConfig, this._tenancy.tokenStore, undefined); if (!resolved.actorOwned) { return { client: this.client, scopeKey: 'admin' }; } this._actorClientCache.set(actorContext.userId, resolved.client); return { client: resolved.client, scopeKey: `actor:${actorContext.userId}` }; } async resolveFolder(folderPath, client, sharedDriveId, baseFolderId, scopeKey) { const segments = folderPath.split('/').filter(Boolean); if (segments.length === 0) return baseFolderId; let parentId = baseFolderId; let cacheKey = `${scopeKey}:${baseFolderId ?? 'root'}`; for (const segment of segments) { cacheKey = `${cacheKey}/${segment}`; if (this._folderCache.has(cacheKey)) { parentId = this._folderCache.get(cacheKey); } else { parentId = await client.findOrCreateFolder(segment, parentId, sharedDriveId); this._folderCache.set(cacheKey, parentId); } } return parentId; } } exports.DriveStorageAdapter = DriveStorageAdapter; //# sourceMappingURL=driveStorageAdapter.js.map