Skip to content

Latest commit

 

History

95 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Factory Drive logo

Factory drive module for NestJS framework

NPM Version Package License CI Coverage llms.txt MCP


@ficsysfr/nestjs_module_factorydrive

nestjs_module_factorydrive provides a simple storage abstraction for NestJS:

  • configure one or many disks
  • select a default disk
  • use built-in local filesystem driver
  • declare custom drivers (S3, Spaces, etc.) directly in forRoot() / forRootAsync()

Maintained Packages

Current maintained packages in the Factorydrive ecosystem:

Requirements

  • Node.js >= 22
  • Yarn 1.22.22 (used for development in this repository)
  • NestJS ^6 to ^12 (@nestjs/common and @nestjs/core)
  • With NestJS 12 (ESM-only), Node.js >= 22.12 so the CommonJS build can require() Nest

Architecture and Portability

Application and business services should depend on FactorydriveService, not on a physical storage provider. Keep filesystem roots, buckets, endpoints, credentials, and driver selection in module configuration so the same business code can use local, S3, or SFTP storage.

Business service
      |
FactorydriveService
      |
AbstractStorage
  /      |      \
local    S3     SFTP

For application storage, avoid importing node:fs, S3Client, or an SFTP client into business services. Provider-specific SDKs belong inside Factorydrive drivers or narrowly justified infrastructure code.

Installation

yarn add @ficsysfr/nestjs_module_factorydrive

Development

This repository uses Yarn, Vitest, TypeScript, and Biome:

yarn install --frozen-lockfile
yarn lint
yarn typecheck
yarn test
yarn test:coverage
yarn build
yarn mcp:test
yarn docs:build
yarn docs:check
yarn test:scripts
yarn changelog:check
yarn package:check

The equivalent aggregate command is make check. Build audited core and MCP tarballs with yarn package or make package; outputs and SHA256SUMS.txt are written under .artifacts/npm/.

Maintainers dispatch an exact manual release with make release VERSION=2.0.0 CHANNEL=latest WATCH=1. The privileged workflow uses the protected npm environment and npm Trusted Publishing/OIDC after the one-time bootstrap.

Quick Start (synchronous config)

// app.module.ts
import { Module } from '@nestjs/common'
import { FactorydriveModule } from '@ficsysfr/nestjs_module_factorydrive'

@Module({
  imports: [
    FactorydriveModule.forRoot({
      default: 'local',
      disks: {
        local: {
          driver: 'local',
          config: {
            root: `${process.cwd()}/storage`,
          },
        },
      },
    }),
  ],
})
export class AppModule {}

Async Configuration (forRootAsync)

// app.module.ts
import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { FactorydriveModule } from '@ficsysfr/nestjs_module_factorydrive'

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    FactorydriveModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (config: ConfigService) => ({
        default: config.get<string>('factorydrive.default', 'local'),
        disks: {
          local: {
            driver: 'local',
            config: {
              root: config.get<string>('factorydrive.localRoot', `${process.cwd()}/storage`),
            },
          },
        },
      }),
    }),
  ],
})
export class AppModule {}

Usage

Inject FactorydriveService and interact with a disk instance:

// file-storage.service.ts
import { Injectable } from '@nestjs/common'
import { FactorydriveService } from '@ficsysfr/nestjs_module_factorydrive'

@Injectable()
export class FileStorageService {
  public constructor(private readonly factorydrive: FactorydriveService) {}

  public async uploadFile(path: string, buffer: Buffer): Promise<void> {
    await this.factorydrive.getDisk().put(path, buffer)
  }

  public async readFile(path: string): Promise<string> {
    const { content } = await this.factorydrive.getDisk().get(path)
    return content
  }

  public async deleteFile(path: string): Promise<boolean | null> {
    const { wasDeleted } = await this.factorydrive.getDisk().delete(path)
    return wasDeleted
  }
}

If no disk name is provided, the configured default disk is used. Prefer this form so business code remains portable across storage providers:

const disk = this.factorydrive.getDisk()

Select a named disk only when the use case intentionally targets it:

const archive = this.factorydrive.getDisk('archive')

Built-in Local Driver

The package includes a local driver with the following operations:

  • append(location, content)
  • copy(src, dest)
  • delete(location)
  • exists(location)
  • get(location, encoding?)
  • getBuffer(location)
  • getStat(location)
  • getStream(location)
  • move(src, dest)
  • prepend(location, content)
  • put(location, content)
  • flatList(prefix?)
  • getUrl(location) — unsigned URL built from the disk baseUrl
  • getSignedUrl(location, { expiresIn? }) — time-limited HMAC-signed URL (default expiresIn = 900)
  • verifySignedUrl(location, { expires, signature }) — constant-time signature + expiry check

content for put accepts Buffer | ReadableStream | string.

Signed URLs (local)

The local driver has no HTTP server: getSignedUrl returns a URL pointing at a baseUrl endpoint that you expose and which must call verifySignedUrl before streaming the file. Configure the disk with a signatureSecret and a baseUrl:

disks: {
  local: {
    driver: 'local',
    config: {
      root: '/var/data',
      signatureSecret: process.env.STORAGE_URL_SECRET,
      baseUrl: 'https://api.example.com/files',
    },
  },
}

const { signedUrl } = await storage.getSignedUrl('threads/abc', { expiresIn: 3600 })
// -> https://api.example.com/files/threads/abc?expires=...&signature=...

// in the /files endpoint:
const ok = storage.verifySignedUrl('threads/abc', { expires, signature })

Both signatureSecret and baseUrl are required for signing; otherwise getSignedUrl throws InvalidConfigException.

Register a Custom Driver

Custom drivers must extend AbstractStorage and implement the methods you need. A driver's constructor takes the disk's config as its single parameter, which is what lets it be typed with the exported StorageDriverConstructor<TConfig> helper.

// aws-s3.storage.ts
import { AbstractStorage, DeleteResponse, Response } from '@ficsysfr/nestjs_module_factorydrive'

export interface AwsS3StorageConfig {
  bucket: string
}

export class AwsS3Storage extends AbstractStorage {
  public constructor(private readonly config: AwsS3StorageConfig) {
    super()
  }

  public async put(location: string, content: Buffer | NodeJS.ReadableStream | string): Promise<Response> {
    // Upload implementation...
    return { raw: { location, uploaded: true, contentType: typeof content } }
  }

  public async delete(location: string): Promise<DeleteResponse> {
    // Delete implementation...
    return { raw: { location }, wasDeleted: true }
  }
}

Then declare it directly in forRoot(), alongside the disks that use it — no module constructor required just to register a driver:

// app.module.ts
import { Module } from '@nestjs/common'
import { FactorydriveModule } from '@ficsysfr/nestjs_module_factorydrive'
import { AwsS3Storage } from './aws-s3.storage'

@Module({
  imports: [
    FactorydriveModule.forRoot({
      default: 's3',
      drivers: {
        s3: AwsS3Storage,
      },
      disks: {
        s3: {
          driver: 's3',
          config: {
            bucket: 'example',
          },
        },
      },
    }),
  ],
})
export class AppModule {}

forRootAsync() accepts drivers the same way, from useFactory or useClass:

// app.module.ts
import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { FactorydriveModule } from '@ficsysfr/nestjs_module_factorydrive'
import { AwsS3Storage } from './aws-s3.storage'

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    FactorydriveModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        default: 'assets',
        drivers: { s3: AwsS3Storage },
        disks: {
          assets: {
            driver: 's3',
            config: { bucket: config.getOrThrow<string>('S3_BUCKET') },
          },
        },
      }),
    }),
  ],
})
export class AppModule {}

S3-compatible providers (MinIO, RustFS, Spaces, B2, R2, ...)

The @ficsysfr/nestjs_module_factorydrive-s3 driver's configuration extends AWS SDK v3 S3ClientConfig. Any S3-compatible endpoint works the same way — Factorydrive has no provider-specific code path, RustFS included:

FactorydriveModule.forRoot({
  default: 'assets',
  drivers: {
    s3: AwsS3Storage,
  },
  disks: {
    assets: {
      driver: 's3',
      config: {
        bucket: 'my-assets',
        endpoint: 'http://rustfs:9000',
        region: 'us-east-1',
        forcePathStyle: true,
        credentials: {
          accessKeyId: process.env.S3_ACCESS_KEY!,
          secretAccessKey: process.env.S3_SECRET_KEY!,
        },
      },
    },
  },
})

registerDriver() — still fully supported

FactorydriveService.registerDriver('key', DriverClass) remains available for dynamic registration and for existing applications: it does not need to change to keep working.

export class AppModule {
  public constructor(factorydrive: FactorydriveService) {
    factorydrive.registerDriver('s3', AwsS3Storage)
  }
}

Registering the same key with the same class through drivers and/or registerDriver() is a no-op, so a driver can be migrated to drivers one at a time without breaking a registerDriver() call left behind elsewhere. Registering the same key with two different classes throws InvalidConfigException as soon as either registration came from drivers; two conflicting registerDriver() calls (including one that replaces the built-in local driver) keep the pre-2.1 behavior — the last call wins — but now log a warning instead of replacing silently.

Exported API

Main exports from this package:

  • FactorydriveModule
  • FactorydriveService
  • AbstractStorage
  • StorageManager
  • StorageDriverConstructor — the driver constructor type used by drivers
  • storage config/types from factorydrive/types
  • exceptions from exceptions

Error Handling

The module provides dedicated exceptions (for example):

  • InvalidConfigException — invalid drivers entry, or a driver name registered twice with two different classes
  • DriverNotSupportedException — a disk's driver was never registered; the message names both the driver and the disk, e.g. Factorydrive driver "s3" required by disk "assets" is not registered. Declare it in "drivers" or call registerDriver() before module initialization.
  • FileNotFoundException
  • PermissionMissingException
  • MethodNotSupportedException

Catch and map them in your service/controller layers as needed.

AI Agent Skill

The npm package includes an Agent Skills pack at node_modules/@ficsysfr/nestjs_module_factorydrive/agent-skills/public. Installing the package with Yarn makes these two skill directories available to compatible coding agents; the package does not run an installer or copy skills during installation.

The English use-factorydrive skill teaches agents to explain, configure, audit, and implement Factorydrive without coupling business code to a storage provider.

Example prompts:

Use $use-factorydrive to configure local document storage in this NestJS application.
Use $use-factorydrive to migrate this service from S3Client to FactorydriveService.
Use $use-factorydrive to expose a verified local signed-download endpoint.

Driver authors should use the separate factorydrive-driver skill.

Documentation for AI agents

The documentation MCP exposes list_doc_sources, search_docs, and fetch_docs over stdio. It never receives storage configuration and cannot read or mutate application files.

Migrating to 2.0

Version 2.0.0 moves the maintained ecosystem to the @ficsysfr npm scope without changing exported TypeScript symbols. Replace package names and import specifiers, then upgrade the core and every installed driver together. See the migration guide.

License

Apache-2.0, see LICENSE. Copyright 2026 FicSys.

Versions up to and including 2.0.0 were published under the MIT License and remain available under those terms.

Releases

Used by

Contributors

Languages