longcelot-sheet-db
Version:
Google Sheets-backed staging database adapter for Node.js with schema-first design
87 lines • 4.55 kB
JavaScript
"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