mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-22 22:41:32 +03:00
feat(storage): binding jwst storage to node (#2808)
This commit is contained in:
parent
86616e152d
commit
2c95bfcc3d
@ -21,7 +21,8 @@
|
||||
"native",
|
||||
"templates",
|
||||
"y-indexeddb",
|
||||
"debug"
|
||||
"debug",
|
||||
"storage"
|
||||
]
|
||||
]
|
||||
}
|
||||
|
14
.github/actions/build-rust/action.yml
vendored
14
.github/actions/build-rust/action.yml
vendored
@ -27,11 +27,11 @@ runs:
|
||||
.cargo-cache
|
||||
target/${{ inputs.target }}
|
||||
key: stable-${{ inputs.target }}-cargo-cache
|
||||
|
||||
- name: Build
|
||||
if: ${{ inputs.target != 'x86_64-unknown-linux-gnu' && inputs.target != 'aarch64-unknown-linux-gnu' }}
|
||||
shell: bash
|
||||
run: yarn nx build @affine/native --target ${{ inputs.target }}
|
||||
run: |
|
||||
yarn nx build @affine/native --target ${{ inputs.target }}
|
||||
env:
|
||||
NX_CLOUD_ACCESS_TOKEN: ${{ inputs.nx_token }}
|
||||
|
||||
@ -41,10 +41,10 @@ runs:
|
||||
with:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
|
||||
options: --user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build -e NX_CLOUD_ACCESS_TOKEN=${{ inputs.nx_token }}
|
||||
run: >-
|
||||
export CC=x86_64-unknown-linux-gnu-gcc &&
|
||||
export CC_x86_64_unknown_linux_gnu=x86_64-unknown-linux-gnu-gcc &&
|
||||
yarn nx build @affine/native --target ${{ inputs.target }} &&
|
||||
run: |
|
||||
export CC=x86_64-unknown-linux-gnu-gcc
|
||||
export CC_x86_64_unknown_linux_gnu=x86_64-unknown-linux-gnu-gcc
|
||||
yarn nx build @affine/native --target ${{ inputs.target }}
|
||||
chmod -R 777 node_modules/.cache
|
||||
|
||||
- name: Build
|
||||
@ -53,6 +53,6 @@ runs:
|
||||
with:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
|
||||
options: --user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build -e NX_CLOUD_ACCESS_TOKEN=${{ inputs.nx_token }}
|
||||
run: >-
|
||||
run: |
|
||||
yarn nx build @affine/native --target ${{ inputs.target }}
|
||||
chmod -R 777 node_modules/.cache
|
||||
|
31
.github/actions/setup-rust/action.yml
vendored
Normal file
31
.github/actions/setup-rust/action.yml
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
name: 'AFFiNE Rust setup'
|
||||
description: 'Rust setup, including cache configuration'
|
||||
inputs:
|
||||
target:
|
||||
description: 'Cargo target'
|
||||
required: true
|
||||
toolchain:
|
||||
description: 'Rustup toolchain'
|
||||
required: false
|
||||
default: 'stable'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: ${{ inputs.toolchain }}
|
||||
targets: ${{ inputs.target }}
|
||||
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: cargo-cache-${{ runner.os }}-${{ inputs.toolchain }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
cargo-cache-${{ runner.os }}-${{ inputs.toolchain }}-
|
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@ -163,9 +163,14 @@ jobs:
|
||||
working-directory: apps/server
|
||||
env:
|
||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||
- name: Setup Rust
|
||||
uses: ./.github/actions/setup-rust
|
||||
with:
|
||||
target: 'x86_64-unknown-linux-gnu'
|
||||
- name: Run server tests
|
||||
run: yarn nx test:coverage @affine/server
|
||||
env:
|
||||
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
|
||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||
- name: Upload server test coverage results
|
||||
uses: codecov/codecov-action@v3
|
||||
|
@ -1,3 +1,23 @@
|
||||
# Server
|
||||
|
||||
The latest server code of AFFiNE is at https://github.com/toeverything/OctoBase/tree/master/apps/cloud
|
||||
## Get started
|
||||
|
||||
### Install dependencies
|
||||
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
|
||||
### Build Native binding
|
||||
|
||||
```bash
|
||||
yarn workspace @affine/storage build
|
||||
```
|
||||
|
||||
### Run server
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
now you can access the server GraphQL endpoint at http://localhost:3000/graphql
|
||||
|
@ -0,0 +1,52 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "blobs" (
|
||||
"hash" VARCHAR NOT NULL,
|
||||
"workspace_id" VARCHAR NOT NULL,
|
||||
"blob" BYTEA NOT NULL,
|
||||
"length" INTEGER NOT NULL,
|
||||
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "blobs_pkey" PRIMARY KEY ("hash")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "optimized_blobs" (
|
||||
"hash" VARCHAR NOT NULL,
|
||||
"workspace_id" VARCHAR NOT NULL,
|
||||
"params" VARCHAR NOT NULL,
|
||||
"blob" BYTEA NOT NULL,
|
||||
"length" INTEGER NOT NULL,
|
||||
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "optimized_blobs_pkey" PRIMARY KEY ("hash")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "docs" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"workspace_id" VARCHAR NOT NULL,
|
||||
"guid" VARCHAR NOT NULL,
|
||||
"is_workspace" BOOLEAN NOT NULL DEFAULT true,
|
||||
"blob" BYTEA NOT NULL,
|
||||
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "docs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "blobs_workspace_id_hash_key" ON "blobs"("workspace_id", "hash");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "optimized_blobs_workspace_id_hash_params_key" ON "optimized_blobs"("workspace_id", "hash", "params");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "docs_workspace_id_guid_idx" ON "docs"("workspace_id", "guid");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "blobs" ADD CONSTRAINT "blobs_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "optimized_blobs" ADD CONSTRAINT "optimized_blobs_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "docs" ADD CONSTRAINT "docs_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -15,6 +15,7 @@
|
||||
"postinstall": "prisma generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/storage": "workspace:*",
|
||||
"@apollo/server": "^4.7.4",
|
||||
"@auth/prisma-adapter": "^1.0.0",
|
||||
"@aws-sdk/client-s3": "^3.359.0",
|
||||
|
@ -8,10 +8,13 @@ datasource db {
|
||||
}
|
||||
|
||||
model Workspace {
|
||||
id String @id @default(uuid()) @db.VarChar
|
||||
public Boolean
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
users UserWorkspacePermission[]
|
||||
id String @id @default(uuid()) @db.VarChar
|
||||
public Boolean
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
users UserWorkspacePermission[]
|
||||
blobs Blob[]
|
||||
docs Doc[]
|
||||
optimizedBlobs OptimizedBlob[]
|
||||
|
||||
@@map("workspaces")
|
||||
}
|
||||
@ -86,3 +89,44 @@ model VerificationToken {
|
||||
@@unique([identifier, token])
|
||||
@@map("verificationtokens")
|
||||
}
|
||||
|
||||
model Blob {
|
||||
hash String @id @default(uuid()) @db.VarChar
|
||||
workspaceId String @map("workspace_id") @db.VarChar
|
||||
blob Bytes @db.ByteA
|
||||
length Int
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([workspaceId, hash])
|
||||
@@map("blobs")
|
||||
}
|
||||
|
||||
model OptimizedBlob {
|
||||
hash String @id @default(uuid()) @db.VarChar
|
||||
workspaceId String @map("workspace_id") @db.VarChar
|
||||
params String @db.VarChar
|
||||
blob Bytes @db.ByteA
|
||||
length Int
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([workspaceId, hash, params])
|
||||
@@map("optimized_blobs")
|
||||
}
|
||||
|
||||
model Doc {
|
||||
id Int @id @default(autoincrement()) @db.Integer
|
||||
workspaceId String @map("workspace_id") @db.VarChar
|
||||
guid String @db.VarChar
|
||||
is_workspace Boolean @default(true) @db.Boolean
|
||||
blob Bytes @db.ByteA
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([workspaceId, guid])
|
||||
@@map("docs")
|
||||
}
|
||||
|
@ -1,16 +1,17 @@
|
||||
/// <reference types="./global.d.ts" />
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ConfigModule } from './config';
|
||||
import { GqlModule } from './graphql.module';
|
||||
import { BusinessModules } from './modules';
|
||||
import { PrismaModule } from './prisma';
|
||||
import { StorageModule } from './storage';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PrismaModule,
|
||||
GqlModule,
|
||||
ConfigModule.forRoot(),
|
||||
StorageModule.forRoot(),
|
||||
...BusinessModules,
|
||||
],
|
||||
})
|
||||
|
@ -132,6 +132,13 @@ export interface AFFiNEConfig {
|
||||
*/
|
||||
get origin(): string;
|
||||
|
||||
/**
|
||||
* the database config
|
||||
*/
|
||||
db: {
|
||||
url: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* the apollo driver config
|
||||
*/
|
||||
|
@ -52,6 +52,9 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => ({
|
||||
get baseUrl() {
|
||||
return `${this.origin}${this.path}`;
|
||||
},
|
||||
db: {
|
||||
url: '',
|
||||
},
|
||||
graphql: {
|
||||
buildSchemaOptions: {
|
||||
numberScalarMode: 'integer',
|
||||
@ -81,3 +84,5 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => ({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export { registerEnvs } from './env';
|
||||
|
@ -2,14 +2,16 @@ import { set } from 'lodash-es';
|
||||
|
||||
import { parseEnvValue } from './def';
|
||||
|
||||
for (const env in AFFiNE.ENV_MAP) {
|
||||
const config = AFFiNE.ENV_MAP[env];
|
||||
const [path, value] =
|
||||
typeof config === 'string'
|
||||
? [config, process.env[env]]
|
||||
: [config[0], parseEnvValue(process.env[env], config[1])];
|
||||
export function registerEnvs() {
|
||||
for (const env in globalThis.AFFiNE.ENV_MAP) {
|
||||
const config = globalThis.AFFiNE.ENV_MAP[env];
|
||||
const [path, value] =
|
||||
typeof config === 'string'
|
||||
? [config, process.env[env]]
|
||||
: [config[0], parseEnvValue(process.env[env], config[1])];
|
||||
|
||||
if (typeof value !== 'undefined') {
|
||||
set(globalThis.AFFiNE, path, process.env[env]);
|
||||
if (typeof value !== 'undefined') {
|
||||
set(globalThis.AFFiNE, path, process.env[env]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
// eslint-disable-next-line simple-import-sort/imports
|
||||
import type { DynamicModule, FactoryProvider } from '@nestjs/common';
|
||||
import { merge } from 'lodash-es';
|
||||
|
||||
import type { DeepPartial } from '../utils/types';
|
||||
import type { AFFiNEConfig } from './def';
|
||||
|
||||
import '../prelude';
|
||||
|
||||
type ConstructorOf<T> = {
|
||||
new (): T;
|
||||
};
|
||||
@ -37,11 +40,14 @@ function createConfigProvider(
|
||||
provide: Config,
|
||||
useFactory: () => {
|
||||
const wrapper = new Config();
|
||||
const config = merge({}, AFFiNE, override);
|
||||
const config = merge({}, globalThis.AFFiNE, override);
|
||||
|
||||
const proxy: Config = new Proxy(wrapper, {
|
||||
get: (_target, property: keyof Config) => {
|
||||
const desc = Object.getOwnPropertyDescriptor(AFFiNE, property);
|
||||
const desc = Object.getOwnPropertyDescriptor(
|
||||
globalThis.AFFiNE,
|
||||
property
|
||||
);
|
||||
if (desc?.get) {
|
||||
return desc.get.call(proxy);
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import './prelude';
|
||||
|
||||
/// <reference types="./global.d.ts" />
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import type { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { static as staticMiddleware } from 'express';
|
||||
|
27
apps/server/src/modules/workspaces/controller.ts
Normal file
27
apps/server/src/modules/workspaces/controller.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Storage } from '@affine/storage';
|
||||
import { Controller, Get, NotFoundException, Param, Res } from '@nestjs/common';
|
||||
import type { Response } from 'express';
|
||||
|
||||
@Controller('/api/workspaces')
|
||||
export class WorkspacesController {
|
||||
constructor(private readonly storage: Storage) {}
|
||||
|
||||
@Get('/:id/blobs/:name')
|
||||
async blob(
|
||||
@Param('id') workspaceId: string,
|
||||
@Param('name') name: string,
|
||||
@Res() res: Response
|
||||
) {
|
||||
const blob = await this.storage.blob(workspaceId, name);
|
||||
|
||||
if (!blob) {
|
||||
throw new NotFoundException('Blob not found');
|
||||
}
|
||||
|
||||
res.setHeader('content-type', blob.contentType);
|
||||
res.setHeader('last-modified', blob.lastModified);
|
||||
res.setHeader('content-length', blob.size);
|
||||
|
||||
res.send(blob.data);
|
||||
}
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspacesController } from './controller';
|
||||
import { PermissionService } from './permission';
|
||||
import { WorkspaceResolver } from './resolver';
|
||||
|
||||
@Module({
|
||||
providers: [WorkspaceResolver, PermissionService],
|
||||
providers: [WorkspaceResolver, PermissionService, WorkspacesController],
|
||||
exports: [PermissionService],
|
||||
})
|
||||
export class WorkspaceModule {}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Storage } from '@affine/storage';
|
||||
import { ForbiddenException, NotFoundException } from '@nestjs/common';
|
||||
import {
|
||||
Args,
|
||||
@ -16,8 +17,11 @@ import {
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
import type { User, Workspace } from '@prisma/client';
|
||||
// @ts-expect-error graphql-upload is not typed
|
||||
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
||||
|
||||
import { PrismaService } from '../../prisma';
|
||||
import type { FileUpload } from '../../types';
|
||||
import { Auth, CurrentUser } from '../auth';
|
||||
import { UserType } from '../users/resolver';
|
||||
import { PermissionService } from './permission';
|
||||
@ -55,7 +59,8 @@ export class UpdateWorkspaceInput extends PickType(
|
||||
export class WorkspaceResolver {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly permissionProvider: PermissionService
|
||||
private readonly permissionProvider: PermissionService,
|
||||
private readonly storage: Storage
|
||||
) {}
|
||||
|
||||
@ResolveField(() => Permission, {
|
||||
@ -174,8 +179,25 @@ export class WorkspaceResolver {
|
||||
@Mutation(() => WorkspaceType, {
|
||||
description: 'Create a new workspace',
|
||||
})
|
||||
async createWorkspace(@CurrentUser() user: User) {
|
||||
return this.prisma.workspace.create({
|
||||
async createWorkspace(
|
||||
@CurrentUser() user: User,
|
||||
@Args({ name: 'init', type: () => GraphQLUpload })
|
||||
update: FileUpload
|
||||
) {
|
||||
// convert stream to buffer
|
||||
const buffer = await new Promise<Buffer>((resolve, reject) => {
|
||||
const stream = update.createReadStream();
|
||||
const chunks: Uint8Array[] = [];
|
||||
stream.on('data', chunk => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
stream.on('error', reject);
|
||||
stream.on('end', () => {
|
||||
resolve(Buffer.concat(chunks));
|
||||
});
|
||||
});
|
||||
|
||||
const workspace = await this.prisma.workspace.create({
|
||||
data: {
|
||||
public: false,
|
||||
users: {
|
||||
@ -191,6 +213,10 @@ export class WorkspaceResolver {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await this.storage.createWorkspace(workspace.id, buffer);
|
||||
|
||||
return workspace;
|
||||
}
|
||||
|
||||
@Mutation(() => WorkspaceType, {
|
||||
@ -221,8 +247,15 @@ export class WorkspaceResolver {
|
||||
},
|
||||
});
|
||||
|
||||
await this.prisma.userWorkspacePermission.deleteMany({
|
||||
where: {
|
||||
workspaceId: id,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO:
|
||||
// delete all related data, like websocket connections, blobs, etc.
|
||||
await this.storage.deleteWorkspace(id);
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -283,4 +316,28 @@ export class WorkspaceResolver {
|
||||
|
||||
return this.permissionProvider.revoke(workspaceId, user.id);
|
||||
}
|
||||
|
||||
@Mutation(() => String)
|
||||
async uploadBlob(
|
||||
@CurrentUser() user: User,
|
||||
@Args('workspaceId') workspaceId: string,
|
||||
@Args({ name: 'blob', type: () => GraphQLUpload })
|
||||
blob: FileUpload
|
||||
) {
|
||||
await this.permissionProvider.check(workspaceId, user.id);
|
||||
|
||||
const buffer = await new Promise<Buffer>((resolve, reject) => {
|
||||
const stream = blob.createReadStream();
|
||||
const chunks: Uint8Array[] = [];
|
||||
stream.on('data', chunk => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
stream.on('error', reject);
|
||||
stream.on('end', () => {
|
||||
resolve(Buffer.concat(chunks));
|
||||
});
|
||||
});
|
||||
|
||||
return this.storage.uploadBlob(workspaceId, buffer);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,12 @@
|
||||
import 'reflect-metadata';
|
||||
import 'dotenv/config';
|
||||
|
||||
import { getDefaultAFFiNEConfig } from './config/default';
|
||||
import { getDefaultAFFiNEConfig, registerEnvs } from './config/default';
|
||||
|
||||
globalThis.AFFiNE = getDefaultAFFiNEConfig();
|
||||
|
||||
globalThis.AFFiNE.ENV_MAP = {
|
||||
DATABASE_URL: 'db.url',
|
||||
};
|
||||
|
||||
registerEnvs();
|
||||
|
@ -106,7 +106,7 @@ type Mutation {
|
||||
"""
|
||||
Create a new workspace
|
||||
"""
|
||||
createWorkspace: WorkspaceType!
|
||||
createWorkspace(init: Upload!): WorkspaceType!
|
||||
|
||||
"""
|
||||
Update workspace
|
||||
@ -121,6 +121,7 @@ type Mutation {
|
||||
revoke(workspaceId: String!, userId: String!): Boolean!
|
||||
acceptInvite(workspaceId: String!): Boolean!
|
||||
leaveWorkspace(workspaceId: String!): Boolean!
|
||||
uploadBlob(workspaceId: String!, blob: Upload!): String!
|
||||
|
||||
"""
|
||||
Upload user avatar
|
||||
@ -128,6 +129,11 @@ type Mutation {
|
||||
uploadAvatar(id: String!, avatar: Upload!): UserType!
|
||||
}
|
||||
|
||||
"""
|
||||
The `Upload` scalar type represents a file upload.
|
||||
"""
|
||||
scalar Upload
|
||||
|
||||
input UpdateWorkspaceInput {
|
||||
"""
|
||||
is Public workspace
|
||||
@ -135,8 +141,3 @@ input UpdateWorkspaceInput {
|
||||
public: Boolean
|
||||
id: ID!
|
||||
}
|
||||
|
||||
"""
|
||||
The `Upload` scalar type represents a file upload.
|
||||
"""
|
||||
scalar Upload
|
||||
|
23
apps/server/src/storage/index.ts
Normal file
23
apps/server/src/storage/index.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Storage } from '@affine/storage';
|
||||
import { type DynamicModule, type FactoryProvider } from '@nestjs/common';
|
||||
|
||||
import { Config } from '../config';
|
||||
|
||||
export class StorageModule {
|
||||
static forRoot(): DynamicModule {
|
||||
const storageProvider: FactoryProvider = {
|
||||
provide: Storage,
|
||||
useFactory: async (config: Config) => {
|
||||
return Storage.connect(config.db.url);
|
||||
},
|
||||
inject: [Config],
|
||||
};
|
||||
|
||||
return {
|
||||
global: true,
|
||||
module: StorageModule,
|
||||
providers: [storageProvider],
|
||||
exports: [storageProvider],
|
||||
};
|
||||
}
|
||||
}
|
@ -12,12 +12,9 @@ import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
import request from 'supertest';
|
||||
|
||||
import { AppModule } from '../app';
|
||||
import { getDefaultAFFiNEConfig } from '../config/default';
|
||||
|
||||
const gql = '/graphql';
|
||||
|
||||
globalThis.AFFiNE = getDefaultAFFiNEConfig();
|
||||
|
||||
describe('AppModule', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
@ -76,33 +73,14 @@ describe('AppModule', () => {
|
||||
.auth(token, { type: 'bearer' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
createWorkspace {
|
||||
id
|
||||
public
|
||||
createdAt
|
||||
}
|
||||
query {
|
||||
__typename
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200)
|
||||
.expect(res => {
|
||||
ok(
|
||||
typeof res.body.data.createWorkspace === 'object',
|
||||
'res.body.data.createWorkspace is not an object'
|
||||
);
|
||||
ok(
|
||||
typeof res.body.data.createWorkspace.id === 'string',
|
||||
'res.body.data.createWorkspace.id is not a string'
|
||||
);
|
||||
ok(
|
||||
typeof res.body.data.createWorkspace.public === 'boolean',
|
||||
'res.body.data.createWorkspace.public is not a boolean'
|
||||
);
|
||||
ok(
|
||||
typeof res.body.data.createWorkspace.createdAt === 'string',
|
||||
'res.body.data.createWorkspace.created_at is not a string'
|
||||
);
|
||||
ok(res.body.data.__typename === 'Query');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
/// <reference types="../global.d.ts" />
|
||||
import { ok } from 'node:assert';
|
||||
import { beforeEach, test } from 'node:test';
|
||||
|
||||
@ -5,14 +6,11 @@ import { Test } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { ConfigModule } from '../config';
|
||||
import { getDefaultAFFiNEConfig } from '../config/default';
|
||||
import { GqlModule } from '../graphql.module';
|
||||
import { AuthModule } from '../modules/auth';
|
||||
import { AuthService } from '../modules/auth/service';
|
||||
import { PrismaModule } from '../prisma';
|
||||
|
||||
globalThis.AFFiNE = getDefaultAFFiNEConfig();
|
||||
|
||||
let auth: AuthService;
|
||||
|
||||
// cleanup database before each test
|
||||
|
@ -4,9 +4,6 @@ import { beforeEach, test } from 'node:test';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
import { Config, ConfigModule } from '../config';
|
||||
import { getDefaultAFFiNEConfig } from '../config/default';
|
||||
|
||||
globalThis.AFFiNE = getDefaultAFFiNEConfig();
|
||||
|
||||
let config: Config;
|
||||
beforeEach(async () => {
|
||||
|
@ -1,22 +1,21 @@
|
||||
import { ok } from 'node:assert';
|
||||
import { afterEach, beforeEach, describe, test } from 'node:test';
|
||||
import { afterEach, beforeEach, describe, it } from 'node:test';
|
||||
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
// @ts-expect-error graphql-upload is not typed
|
||||
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
import request from 'supertest';
|
||||
|
||||
import { AppModule } from '../app';
|
||||
import { getDefaultAFFiNEConfig } from '../config/default';
|
||||
import type { TokenType } from '../modules/auth';
|
||||
import type { UserType } from '../modules/users';
|
||||
import type { WorkspaceType } from '../modules/workspaces';
|
||||
|
||||
const gql = '/graphql';
|
||||
|
||||
globalThis.AFFiNE = getDefaultAFFiNEConfig();
|
||||
|
||||
describe('AppModule', () => {
|
||||
describe('Workspace Module', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
// cleanup database before each test
|
||||
@ -32,6 +31,12 @@ describe('AppModule', () => {
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
app = module.createNestApplication();
|
||||
app.use(
|
||||
graphqlUploadExpress({
|
||||
maxFileSize: 10 * 1024 * 1024,
|
||||
maxFiles: 5,
|
||||
})
|
||||
);
|
||||
await app.init();
|
||||
});
|
||||
|
||||
@ -63,15 +68,20 @@ describe('AppModule', () => {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
createWorkspace {
|
||||
.field(
|
||||
'operations',
|
||||
JSON.stringify({
|
||||
name: 'createWorkspace',
|
||||
query: `mutation createWorkspace($init: Upload!) {
|
||||
createWorkspace(init: $init) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
}`,
|
||||
variables: { init: null },
|
||||
})
|
||||
)
|
||||
.field('map', JSON.stringify({ '0': ['variables.init'] }))
|
||||
.attach('0', Buffer.from([0, 0]), 'init.data')
|
||||
.expect(200);
|
||||
return res.body.data.createWorkspace;
|
||||
}
|
||||
@ -151,21 +161,21 @@ describe('AppModule', () => {
|
||||
return res.body.data.revoke;
|
||||
}
|
||||
|
||||
test('should register a user', async () => {
|
||||
it('should register a user', async () => {
|
||||
const user = await registerUser('u1', 'u1@affine.pro', '123456');
|
||||
ok(typeof user.id === 'string', 'user.id is not a string');
|
||||
ok(user.name === 'u1', 'user.name is not valid');
|
||||
ok(user.email === 'u1@affine.pro', 'user.email is not valid');
|
||||
});
|
||||
|
||||
test('should create a workspace', async () => {
|
||||
it('should create a workspace', async () => {
|
||||
const user = await registerUser('u1', 'u1@affine.pro', '1');
|
||||
|
||||
const workspace = await createWorkspace(user.token.token);
|
||||
ok(typeof workspace.id === 'string', 'workspace.id is not a string');
|
||||
});
|
||||
|
||||
test('should invite a user', async () => {
|
||||
it('should invite a user', async () => {
|
||||
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
|
||||
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
|
||||
|
||||
@ -180,7 +190,7 @@ describe('AppModule', () => {
|
||||
ok(invite === true, 'failed to invite user');
|
||||
});
|
||||
|
||||
test('should accept an invite', async () => {
|
||||
it('should accept an invite', async () => {
|
||||
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
|
||||
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
|
||||
|
||||
@ -191,7 +201,7 @@ describe('AppModule', () => {
|
||||
ok(accept === true, 'failed to accept invite');
|
||||
});
|
||||
|
||||
test('should leave a workspace', async () => {
|
||||
it('should leave a workspace', async () => {
|
||||
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
|
||||
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
|
||||
|
||||
@ -203,7 +213,7 @@ describe('AppModule', () => {
|
||||
ok(leave === true, 'failed to leave workspace');
|
||||
});
|
||||
|
||||
test('should revoke a user', async () => {
|
||||
it('should revoke a user', async () => {
|
||||
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
|
||||
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
|
||||
|
||||
|
@ -20,6 +20,9 @@
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/storage/tsconfig.json"
|
||||
}
|
||||
],
|
||||
"ts-node": {
|
||||
|
1
nx.json
1
nx.json
@ -35,6 +35,7 @@
|
||||
"{workspaceRoot}/apps/storybook/storybook-static",
|
||||
"{workspaceRoot}/packages/i18n/src/i18n-generated.ts",
|
||||
"{workspaceRoot}/packages/native/affine.*.node",
|
||||
"{workspaceRoot}/packages/storage/*.node",
|
||||
"{workspaceRoot}/affine.db",
|
||||
"{workspaceRoot/apps/electron/dist"
|
||||
],
|
||||
|
@ -1,5 +1,5 @@
|
||||
mutation createWorkspace {
|
||||
createWorkspace {
|
||||
mutation createWorkspace($init: Upload!) {
|
||||
createWorkspace(init: $init) {
|
||||
id
|
||||
public
|
||||
createdAt
|
||||
|
@ -11,10 +11,10 @@ export const createWorkspaceMutation = {
|
||||
id: 'createWorkspaceMutation' as const,
|
||||
operationName: 'createWorkspace',
|
||||
definitionName: 'createWorkspace',
|
||||
containsFile: false,
|
||||
containsFile: true,
|
||||
query: `
|
||||
mutation createWorkspace {
|
||||
createWorkspace {
|
||||
mutation createWorkspace($init: Upload!) {
|
||||
createWorkspace(init: $init) {
|
||||
id
|
||||
public
|
||||
createdAt
|
||||
|
@ -46,7 +46,9 @@ export interface UpdateWorkspaceInput {
|
||||
public: InputMaybe<Scalars['Boolean']['input']>;
|
||||
}
|
||||
|
||||
export type CreateWorkspaceMutationVariables = Exact<{ [key: string]: never }>;
|
||||
export type CreateWorkspaceMutationVariables = Exact<{
|
||||
init: Scalars['Upload']['input'];
|
||||
}>;
|
||||
|
||||
export type CreateWorkspaceMutation = {
|
||||
__typename?: 'Mutation';
|
||||
|
4428
packages/storage/Cargo.lock
generated
Normal file
4428
packages/storage/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
packages/storage/Cargo.toml
Normal file
27
packages/storage/Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "affine_storage"
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
|
||||
# used to avoid sys dep conflict sqlx -> libsqlite-sys
|
||||
[workspace]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
jwst = { git = "https://github.com/toeverything/OctoBase.git", branch = "master" }
|
||||
jwst-storage = { git = "https://github.com/toeverything/OctoBase.git", branch = "master" }
|
||||
napi = { version = "2", default-features = false, features = [
|
||||
"napi5",
|
||||
"async",
|
||||
] }
|
||||
napi-derive = { version = "2", features = ["type-def"] }
|
||||
yrs = { version = "0.16.5" }
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "2"
|
||||
|
||||
[patch.crates-io]
|
||||
lib0 = { git = "https://github.com/toeverything/y-crdt", rev = "a700f09" }
|
||||
yrs = { git = "https://github.com/toeverything/y-crdt", rev = "a700f09" }
|
131
packages/storage/__tests__/storage.spec.js
Normal file
131
packages/storage/__tests__/storage.spec.js
Normal file
@ -0,0 +1,131 @@
|
||||
import assert from 'node:assert';
|
||||
import { beforeEach, describe, test } from 'node:test';
|
||||
|
||||
import { encoding } from 'lib0';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { Storage } from '../index.js';
|
||||
|
||||
// update binary by y.doc.text('content').insert('hello world')
|
||||
// prettier-ignore
|
||||
let init = Buffer.from([1,1,160,238,169,240,10,0,4,1,7,99,111,110,116,101,110,116,11,104,101,108,108,111,32,119,111,114,108,100,0])
|
||||
describe('Test jwst storage binding', () => {
|
||||
/** @type { Storage } */
|
||||
let storage;
|
||||
beforeEach(async () => {
|
||||
storage = await Storage.connect('sqlite::memory:', true);
|
||||
});
|
||||
|
||||
test('should be able to create workspace', async () => {
|
||||
const workspace = await storage.createWorkspace('test-workspace', init);
|
||||
|
||||
assert(workspace.id === 'test-workspace');
|
||||
assert.deepEqual(init, await storage.load(workspace.doc.guid));
|
||||
});
|
||||
|
||||
test('should not create workspace with same id', async () => {
|
||||
await storage.createWorkspace('test-workspace', init);
|
||||
await assert.rejects(
|
||||
storage.createWorkspace('test-workspace', init),
|
||||
/Workspace [\w-]+ already exists/
|
||||
);
|
||||
});
|
||||
|
||||
test('should be able to delete workspace', async () => {
|
||||
const workspace = await storage.createWorkspace('test-workspace', init);
|
||||
|
||||
await storage.deleteWorkspace(workspace.id);
|
||||
|
||||
await assert.rejects(
|
||||
storage.load(workspace.doc.guid),
|
||||
/Doc [\w-]+ not exists/
|
||||
);
|
||||
});
|
||||
|
||||
test('should be able to sync update', async () => {
|
||||
const workspace = await storage.createWorkspace('test-workspace', init);
|
||||
|
||||
const update = await storage.load(workspace.doc.guid);
|
||||
assert(update !== null);
|
||||
|
||||
const doc = new Y.Doc();
|
||||
Y.applyUpdate(doc, update);
|
||||
|
||||
let text = doc.getText('content');
|
||||
assert.equal(text.toJSON(), 'hello world');
|
||||
|
||||
const updates = [];
|
||||
doc.on('update', async (/** @type { UInt8Array } */ update) => {
|
||||
updates.push(Buffer.from(update));
|
||||
});
|
||||
|
||||
text.insert(5, ' my');
|
||||
text.insert(14, '!');
|
||||
|
||||
for (const update of updates) {
|
||||
await storage.sync(workspace.id, workspace.doc.guid, update);
|
||||
}
|
||||
|
||||
const update2 = await storage.load(workspace.doc.guid);
|
||||
const doc2 = new Y.Doc();
|
||||
Y.applyUpdate(doc2, update2);
|
||||
|
||||
text = doc2.getText('content');
|
||||
assert.equal(text.toJSON(), 'hello my world!');
|
||||
});
|
||||
|
||||
test('should be able to sync update with guid encoded', async () => {
|
||||
const workspace = await storage.createWorkspace('test-workspace', init);
|
||||
|
||||
const update = await storage.load(workspace.doc.guid);
|
||||
assert(update !== null);
|
||||
|
||||
const doc = new Y.Doc();
|
||||
Y.applyUpdate(doc, update);
|
||||
|
||||
let text = doc.getText('content');
|
||||
assert.equal(text.toJSON(), 'hello world');
|
||||
|
||||
const updates = [];
|
||||
doc.on('update', async (/** @type { UInt8Array } */ update) => {
|
||||
const prefix = encoding.encode(encoder => {
|
||||
encoding.writeVarString(encoder, workspace.doc.guid);
|
||||
});
|
||||
|
||||
updates.push(Buffer.concat([prefix, update]));
|
||||
});
|
||||
|
||||
text.insert(5, ' my');
|
||||
text.insert(14, '!');
|
||||
|
||||
for (const update of updates) {
|
||||
await storage.syncWithGuid(workspace.id, update);
|
||||
}
|
||||
|
||||
const update2 = await storage.load(workspace.doc.guid);
|
||||
const doc2 = new Y.Doc();
|
||||
Y.applyUpdate(doc2, update2);
|
||||
|
||||
text = doc2.getText('content');
|
||||
assert.equal(text.toJSON(), 'hello my world!');
|
||||
});
|
||||
|
||||
test('should be able to store blob', async () => {
|
||||
let workspace = await storage.createWorkspace('test-workspace', init);
|
||||
const blobId = await storage.uploadBlob(workspace.id, Buffer.from([1]));
|
||||
|
||||
assert(blobId !== null);
|
||||
|
||||
let blob = await storage.blob(workspace.id, blobId);
|
||||
|
||||
assert.deepEqual(blob.data, Buffer.from([1]));
|
||||
assert.strictEqual(blob.size, 1);
|
||||
assert.equal(blob.contentType, 'application/octet-stream');
|
||||
|
||||
await storage.uploadBlob(workspace.id, Buffer.from([1, 2, 3, 4, 5]));
|
||||
|
||||
const spaceTaken = await storage.blobsSize(workspace.id);
|
||||
|
||||
assert.equal(spaceTaken, 6);
|
||||
});
|
||||
});
|
3
packages/storage/build.rs
Normal file
3
packages/storage/build.rs
Normal file
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
napi_build::setup();
|
||||
}
|
52
packages/storage/index.d.ts
vendored
Normal file
52
packages/storage/index.d.ts
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
/* auto-generated by NAPI-RS */
|
||||
/* eslint-disable */
|
||||
|
||||
export class Doc {
|
||||
get guid(): string;
|
||||
}
|
||||
|
||||
export class Storage {
|
||||
/** Create a storage instance and establish connection to persist store. */
|
||||
static connect(
|
||||
database: string,
|
||||
debugOnlyAutoMigrate?: boolean | undefined | null
|
||||
): Promise<Storage>;
|
||||
/** Get a workspace by id */
|
||||
getWorkspace(workspaceId: string): Promise<Workspace | null>;
|
||||
/** Create a new workspace with a init update. */
|
||||
createWorkspace(workspaceId: string, init: Buffer): Promise<Workspace>;
|
||||
/** Delete a workspace. */
|
||||
deleteWorkspace(workspaceId: string): Promise<void>;
|
||||
/** Sync doc updates. */
|
||||
sync(workspaceId: string, guid: string, update: Buffer): Promise<void>;
|
||||
/** Sync doc update with doc guid encoded. */
|
||||
syncWithGuid(workspaceId: string, update: Buffer): Promise<void>;
|
||||
/** Load doc as update buffer. */
|
||||
load(guid: string): Promise<Buffer | null>;
|
||||
/** Fetch a workspace blob. */
|
||||
blob(workspaceId: string, name: string): Promise<Blob | null>;
|
||||
/** Upload a blob into workspace storage. */
|
||||
uploadBlob(workspaceId: string, blob: Buffer): Promise<string>;
|
||||
/** Workspace size taken by blobs. */
|
||||
blobsSize(workspaceId: string): Promise<number>;
|
||||
}
|
||||
|
||||
export class Workspace {
|
||||
get doc(): Doc;
|
||||
isEmpty(): boolean;
|
||||
get id(): string;
|
||||
get clientId(): string;
|
||||
search(query: string): Array<SearchResult>;
|
||||
}
|
||||
|
||||
export interface Blob {
|
||||
contentType: string;
|
||||
lastModified: string;
|
||||
size: number;
|
||||
data: Buffer;
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
blockId: string;
|
||||
score: number;
|
||||
}
|
10
packages/storage/index.js
Normal file
10
packages/storage/index.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
/** @type {import('.')} */
|
||||
const binding = require('./storage.node');
|
||||
|
||||
export const Storage = binding.Storage;
|
||||
export const Workspace = binding.Workspace;
|
||||
export const Document = binding.Doc;
|
43
packages/storage/package.json
Normal file
43
packages/storage/package.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@affine/storage",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": ">= 10.16.0 < 11 || >= 11.8.0"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "./index.js",
|
||||
"module": "./index.js",
|
||||
"types": "index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./storage.node",
|
||||
"import": "./index.js",
|
||||
"types": "./index.d.ts"
|
||||
}
|
||||
},
|
||||
"napi": {
|
||||
"binaryName": "storage",
|
||||
"targets": [
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"aarch64-pc-windows-msvc",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-pc-windows-msvc",
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"universal-apple-darwin"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node --test ./__tests__/**/*.spec.js",
|
||||
"build": "napi build --release --strip",
|
||||
"build:debug": "napi build",
|
||||
"prepublishOnly": "napi prepublish -t npm",
|
||||
"artifacts": "napi artifacts",
|
||||
"version": "napi version"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^3.0.0-alpha.4",
|
||||
"lib0": "^0.2.78",
|
||||
"yjs": "^13.6.6"
|
||||
}
|
||||
}
|
284
packages/storage/src/lib.rs
Normal file
284
packages/storage/src/lib.rs
Normal file
@ -0,0 +1,284 @@
|
||||
#![deny(clippy::all)]
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::{Debug, Display},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use jwst::{BlobStorage, SearchResult as JwstSearchResult, Workspace as JwstWorkspace, DocStorage};
|
||||
use jwst_storage::{JwstStorage, JwstStorageError};
|
||||
use yrs::{Doc as YDoc, ReadTxn, StateVector, Transact};
|
||||
|
||||
use napi::{bindgen_prelude::*, Error, Result, Status};
|
||||
|
||||
#[macro_use]
|
||||
extern crate napi_derive;
|
||||
|
||||
fn map_err_inner<T, E: Display + Debug>(v: std::result::Result<T, E>, status: Status) -> Result<T> {
|
||||
match v {
|
||||
Ok(val) => Ok(val),
|
||||
Err(e) => {
|
||||
dbg!(&e);
|
||||
Err(Error::new(status, e.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! map_err {
|
||||
($val: expr) => {
|
||||
map_err_inner($val, Status::GenericFailure)
|
||||
};
|
||||
($val: expr, $stauts: ident) => {
|
||||
map_err_inner($val, $stauts)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! napi_wrap {
|
||||
($( ($name: ident, $target: ident) ),*) => {
|
||||
$(
|
||||
#[napi]
|
||||
pub struct $name($target);
|
||||
|
||||
impl std::ops::Deref for $name {
|
||||
type Target = $target;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<$target> for $name {
|
||||
fn from(t: $target) -> Self {
|
||||
Self(t)
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
napi_wrap!(
|
||||
(Storage, JwstStorage),
|
||||
(Workspace, JwstWorkspace),
|
||||
(Doc, YDoc)
|
||||
);
|
||||
|
||||
fn to_update_v1(doc: &YDoc) -> Result<Buffer> {
|
||||
let trx = doc.transact();
|
||||
|
||||
map_err!(trx.encode_state_as_update_v1(&StateVector::default())).map(|update| update.into())
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct Blob {
|
||||
pub content_type: String,
|
||||
pub last_modified: String,
|
||||
pub size: i64,
|
||||
pub data: Buffer,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct SearchResult {
|
||||
pub block_id: String,
|
||||
pub score: f64,
|
||||
}
|
||||
|
||||
impl From<JwstSearchResult> for SearchResult {
|
||||
fn from(r: JwstSearchResult) -> Self {
|
||||
Self {
|
||||
block_id: r.block_id,
|
||||
score: r.score as f64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl Storage {
|
||||
/// Create a storage instance and establish connection to persist store.
|
||||
#[napi]
|
||||
pub async fn connect(database: String, debug_only_auto_migrate: Option<bool>) -> Result<Storage> {
|
||||
let inner = match if cfg!(debug_assertions) && debug_only_auto_migrate.unwrap_or(false) {
|
||||
JwstStorage::new_with_migration(&database).await
|
||||
} else {
|
||||
JwstStorage::new(&database).await
|
||||
} {
|
||||
Ok(storage) => storage,
|
||||
Err(JwstStorageError::Db(e)) => {
|
||||
return Err(Error::new(
|
||||
Status::GenericFailure,
|
||||
format!("failed to connect to database: {}", e),
|
||||
))
|
||||
}
|
||||
Err(e) => return Err(Error::new(Status::GenericFailure, e.to_string())),
|
||||
};
|
||||
|
||||
Ok(inner.into())
|
||||
}
|
||||
|
||||
/// Get a workspace by id
|
||||
#[napi]
|
||||
pub async fn get_workspace(&self, workspace_id: String) -> Result<Option<Workspace>> {
|
||||
match self.0.get_workspace(workspace_id).await {
|
||||
Ok(w) => Ok(Some(w.into())),
|
||||
Err(JwstStorageError::WorkspaceNotFound(_)) => Ok(None),
|
||||
Err(e) => Err(Error::new(Status::GenericFailure, e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new workspace with a init update.
|
||||
#[napi]
|
||||
pub async fn create_workspace(&self, workspace_id: String, init: Buffer) -> Result<Workspace> {
|
||||
if map_err!(self.0.docs().detect_workspace(&workspace_id).await)? {
|
||||
return Err(Error::new(
|
||||
Status::GenericFailure,
|
||||
format!("Workspace {} already exists", workspace_id),
|
||||
));
|
||||
}
|
||||
|
||||
let workspace = map_err!(self.0.create_workspace(workspace_id).await)?;
|
||||
|
||||
let init = init.as_ref();
|
||||
let guid = workspace.doc_guid().to_string();
|
||||
map_err!(self.docs().update_doc(workspace.id(), guid, init).await)?;
|
||||
|
||||
Ok(workspace.into())
|
||||
}
|
||||
|
||||
/// Delete a workspace.
|
||||
#[napi]
|
||||
pub async fn delete_workspace(&self, workspace_id: String) -> Result<()> {
|
||||
map_err!(self.docs().delete_workspace(&workspace_id).await)?;
|
||||
map_err!(self.blobs().delete_workspace(workspace_id).await)
|
||||
}
|
||||
|
||||
/// Sync doc updates.
|
||||
#[napi]
|
||||
pub async fn sync(&self, workspace_id: String, guid: String, update: Buffer) -> Result<()> {
|
||||
let update = update.as_ref();
|
||||
map_err!(self.docs().update_doc(workspace_id, guid, update).await)
|
||||
}
|
||||
|
||||
/// Sync doc update with doc guid encoded.
|
||||
#[napi]
|
||||
pub async fn sync_with_guid(&self, workspace_id: String, update: Buffer) -> Result<()> {
|
||||
let update = update.as_ref();
|
||||
map_err!(self.docs().update_doc_with_guid(workspace_id, update).await)
|
||||
}
|
||||
|
||||
/// Load doc as update buffer.
|
||||
#[napi]
|
||||
pub async fn load(&self, guid: String) -> Result<Option<Buffer>> {
|
||||
self.ensure_exists(&guid).await?;
|
||||
|
||||
if let Some(doc) = map_err!(self.docs().get_doc(guid).await)? {
|
||||
Ok(Some(to_update_v1(&doc)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a workspace blob.
|
||||
#[napi]
|
||||
pub async fn blob(&self, workspace_id: String, name: String) -> Result<Option<Blob>> {
|
||||
let (id, params) = {
|
||||
let path = PathBuf::from(name.clone());
|
||||
let ext = path
|
||||
.extension()
|
||||
.and_then(|s| s.to_str().map(|s| s.to_string()));
|
||||
let id = path
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str().map(|s| s.to_string()))
|
||||
.unwrap_or(name);
|
||||
|
||||
(id, ext.map(|ext| HashMap::from([("format".into(), ext)])))
|
||||
};
|
||||
|
||||
let Ok(meta) = self.blobs().get_metadata(Some(workspace_id.clone()), id.clone(), params.clone()).await else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let Ok(file) = self.blobs().get_blob(Some(workspace_id), id, params).await else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(Some(Blob {
|
||||
content_type: meta.content_type,
|
||||
last_modified: format!("{:?}", meta.last_modified),
|
||||
size: meta.size,
|
||||
data: file.into(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Upload a blob into workspace storage.
|
||||
#[napi]
|
||||
pub async fn upload_blob(&self, workspace_id: String, blob: Buffer) -> Result<String> {
|
||||
// TODO: can optimize, avoid copy
|
||||
let blob = blob.as_ref().to_vec();
|
||||
map_err!(self.blobs().put_blob(Some(workspace_id), blob).await)
|
||||
}
|
||||
|
||||
/// Workspace size taken by blobs.
|
||||
#[napi]
|
||||
pub async fn blobs_size(&self, workspace_id: String) -> Result<i64> {
|
||||
map_err!(self.blobs().get_blobs_size(workspace_id).await)
|
||||
}
|
||||
|
||||
async fn ensure_exists(&self, guid: &str) -> Result<()> {
|
||||
if map_err!(self.docs().detect_doc(guid).await)? {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(
|
||||
Status::GenericFailure,
|
||||
format!("Doc {} not exists", guid),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl Workspace {
|
||||
#[napi(getter)]
|
||||
pub fn doc(&self) -> Doc {
|
||||
self.0.doc().into()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
#[inline]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
#[inline]
|
||||
pub fn id(&self) -> String {
|
||||
self.0.id()
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
#[inline]
|
||||
pub fn client_id(&self) -> String {
|
||||
self.0.client_id().to_string()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn search(&self, query: String) -> Result<Vec<SearchResult>> {
|
||||
// TODO: search in all subdocs
|
||||
let result = map_err!(self.0.search(&query))?;
|
||||
|
||||
Ok(
|
||||
result
|
||||
.into_inner()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl Doc {
|
||||
#[napi(getter)]
|
||||
pub fn guid(&self) -> String {
|
||||
self.0.guid().to_string()
|
||||
}
|
||||
}
|
9
packages/storage/tsconfig.json
Normal file
9
packages/storage/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"outDir": "lib",
|
||||
"composite": true
|
||||
},
|
||||
"include": ["index.d.ts"]
|
||||
}
|
@ -79,6 +79,7 @@
|
||||
"@toeverything/plugin-infra/*": ["./packages/plugin-infra/src/*"],
|
||||
"@affine/native": ["./packages/native/index.d.ts"],
|
||||
"@affine/native/*": ["./packages/native/*"],
|
||||
"@affine/storage": ["./packages/storage/index.d.ts"],
|
||||
|
||||
// Development only
|
||||
"@affine/electron/*": ["./apps/electron/src/*"]
|
||||
|
269
yarn.lock
269
yarn.lock
@ -387,6 +387,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/server@workspace:apps/server"
|
||||
dependencies:
|
||||
"@affine/storage": "workspace:*"
|
||||
"@apollo/server": ^4.7.4
|
||||
"@auth/prisma-adapter": ^1.0.0
|
||||
"@aws-sdk/client-s3": ^3.359.0
|
||||
@ -426,6 +427,16 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@affine/storage@workspace:*, @affine/storage@workspace:packages/storage":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/storage@workspace:packages/storage"
|
||||
dependencies:
|
||||
"@napi-rs/cli": ^3.0.0-alpha.4
|
||||
lib0: ^0.2.78
|
||||
yjs: ^13.6.6
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@affine/storybook@workspace:apps/storybook":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/storybook@workspace:apps/storybook"
|
||||
@ -7422,6 +7433,25 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@napi-rs/cli@npm:^3.0.0-alpha.4":
|
||||
version: 3.0.0-alpha.4
|
||||
resolution: "@napi-rs/cli@npm:3.0.0-alpha.4"
|
||||
dependencies:
|
||||
"@octokit/rest": ^19.0.7
|
||||
clipanion: ^3.2.0
|
||||
colorette: ^2.0.19
|
||||
debug: ^4.3.4
|
||||
inquirer: ^9.1.5
|
||||
js-yaml: ^4.1.0
|
||||
lodash-es: ^4.17.21
|
||||
typanion: ^3.12.1
|
||||
bin:
|
||||
napi: dist/cli.js
|
||||
napi-raw: cli.mjs
|
||||
checksum: 961d0f569c73c1dc84e362e2b4baf1fb090ef4fca9a0a53a9b149fd793eeb7e035b66d2cfb79e5c6fc02a1a5ff8732be6d72cca6b2a633b25b7d3cc47b1d983f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@napi-rs/image-android-arm-eabi@npm:1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "@napi-rs/image-android-arm-eabi@npm:1.6.1"
|
||||
@ -8356,6 +8386,151 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/auth-token@npm:^3.0.0":
|
||||
version: 3.0.4
|
||||
resolution: "@octokit/auth-token@npm:3.0.4"
|
||||
checksum: 42f533a873d4192e6df406b3176141c1f95287423ebdc4cf23a38bb77ee00ccbc0e60e3fbd5874234fc2ed2e67bbc6035e3b0561dacc1d078adb5c4ced3579e3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/core@npm:^4.2.1":
|
||||
version: 4.2.1
|
||||
resolution: "@octokit/core@npm:4.2.1"
|
||||
dependencies:
|
||||
"@octokit/auth-token": ^3.0.0
|
||||
"@octokit/graphql": ^5.0.0
|
||||
"@octokit/request": ^6.0.0
|
||||
"@octokit/request-error": ^3.0.0
|
||||
"@octokit/types": ^9.0.0
|
||||
before-after-hook: ^2.2.0
|
||||
universal-user-agent: ^6.0.0
|
||||
checksum: f82d52e937e12da1c7c163341c845b8e27e7fa75678f5e5954e6fa017a94f1833d6e5c4e43f0be796fbfea9dc5e1137087f655dbd5acb3d57879e1b28568e0a9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/endpoint@npm:^7.0.0":
|
||||
version: 7.0.6
|
||||
resolution: "@octokit/endpoint@npm:7.0.6"
|
||||
dependencies:
|
||||
"@octokit/types": ^9.0.0
|
||||
is-plain-object: ^5.0.0
|
||||
universal-user-agent: ^6.0.0
|
||||
checksum: 7caebf30ceec50eb7f253341ed419df355232f03d4638a95c178ee96620400db7e4a5e15d89773fe14db19b8653d4ab4cc81b2e93ca0c760b4e0f7eb7ad80301
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/graphql@npm:^5.0.0":
|
||||
version: 5.0.6
|
||||
resolution: "@octokit/graphql@npm:5.0.6"
|
||||
dependencies:
|
||||
"@octokit/request": ^6.0.0
|
||||
"@octokit/types": ^9.0.0
|
||||
universal-user-agent: ^6.0.0
|
||||
checksum: 7be545d348ef31dcab0a2478dd64d5746419a2f82f61459c774602bcf8a9b577989c18001f50b03f5f61a3d9e34203bdc021a4e4d75ff2d981e8c9c09cf8a65c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/openapi-types@npm:^18.0.0":
|
||||
version: 18.0.0
|
||||
resolution: "@octokit/openapi-types@npm:18.0.0"
|
||||
checksum: d487d6c6c1965e583eee417d567e4fe3357a98953fc49bce1a88487e7908e9b5dbb3e98f60dfa340e23b1792725fbc006295aea071c5667a813b9c098185b56f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/plugin-paginate-rest@npm:^6.1.2":
|
||||
version: 6.1.2
|
||||
resolution: "@octokit/plugin-paginate-rest@npm:6.1.2"
|
||||
dependencies:
|
||||
"@octokit/tsconfig": ^1.0.2
|
||||
"@octokit/types": ^9.2.3
|
||||
peerDependencies:
|
||||
"@octokit/core": ">=4"
|
||||
checksum: a7b3e686c7cbd27ec07871cde6e0b1dc96337afbcef426bbe3067152a17b535abd480db1861ca28c88d93db5f7bfdbcadd0919ead19818c28a69d0e194038065
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/plugin-request-log@npm:^1.0.4":
|
||||
version: 1.0.4
|
||||
resolution: "@octokit/plugin-request-log@npm:1.0.4"
|
||||
peerDependencies:
|
||||
"@octokit/core": ">=3"
|
||||
checksum: 2086db00056aee0f8ebd79797b5b57149ae1014e757ea08985b71eec8c3d85dbb54533f4fd34b6b9ecaa760904ae6a7536be27d71e50a3782ab47809094bfc0c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods@npm:^7.1.2":
|
||||
version: 7.2.3
|
||||
resolution: "@octokit/plugin-rest-endpoint-methods@npm:7.2.3"
|
||||
dependencies:
|
||||
"@octokit/types": ^10.0.0
|
||||
peerDependencies:
|
||||
"@octokit/core": ">=3"
|
||||
checksum: 21dfb98514dbe900c29cddb13b335bbce43d613800c6b17eba3c1fd31d17e69c1960f3067f7bf864bb38fdd5043391f4a23edee42729d8c7fbabd00569a80336
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/request-error@npm:^3.0.0":
|
||||
version: 3.0.3
|
||||
resolution: "@octokit/request-error@npm:3.0.3"
|
||||
dependencies:
|
||||
"@octokit/types": ^9.0.0
|
||||
deprecation: ^2.0.0
|
||||
once: ^1.4.0
|
||||
checksum: 5db0b514732686b627e6ed9ef1ccdbc10501f1b271a9b31f784783f01beee70083d7edcfeb35fbd7e569fa31fdd6762b1ff6b46101700d2d97e7e48e749520d0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/request@npm:^6.0.0":
|
||||
version: 6.2.6
|
||||
resolution: "@octokit/request@npm:6.2.6"
|
||||
dependencies:
|
||||
"@octokit/endpoint": ^7.0.0
|
||||
"@octokit/request-error": ^3.0.0
|
||||
"@octokit/types": ^9.0.0
|
||||
is-plain-object: ^5.0.0
|
||||
node-fetch: ^2.6.7
|
||||
universal-user-agent: ^6.0.0
|
||||
checksum: 0732fb60e20b0348fc61d856c9e234038e34f9ce062b04b968df18c2f106da094055d55c93f653284298b03e6dac2ea5a4b5c5a97686b529ed30447bd37f2773
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/rest@npm:^19.0.7":
|
||||
version: 19.0.11
|
||||
resolution: "@octokit/rest@npm:19.0.11"
|
||||
dependencies:
|
||||
"@octokit/core": ^4.2.1
|
||||
"@octokit/plugin-paginate-rest": ^6.1.2
|
||||
"@octokit/plugin-request-log": ^1.0.4
|
||||
"@octokit/plugin-rest-endpoint-methods": ^7.1.2
|
||||
checksum: 147518ad51d214ead88adc717b5fdc4f33317949d58c124f4069bdf07d2e6b49fa66861036b9e233aed71fcb88ff367a6da0357653484e466175ab4fb7183b3b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/tsconfig@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "@octokit/tsconfig@npm:1.0.2"
|
||||
checksum: 74d56f3e9f326a8dd63700e9a51a7c75487180629c7a68bbafee97c612fbf57af8347369bfa6610b9268a3e8b833c19c1e4beb03f26db9a9dce31f6f7a19b5b1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/types@npm:^10.0.0":
|
||||
version: 10.0.0
|
||||
resolution: "@octokit/types@npm:10.0.0"
|
||||
dependencies:
|
||||
"@octokit/openapi-types": ^18.0.0
|
||||
checksum: 8aafba2ff0cd2435fb70c291bf75ed071c0fa8a865cf6169648732068a35dec7b85a345851f18920ec5f3e94ee0e954988485caac0da09ec3f6781cc44fe153a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/types@npm:^9.0.0, @octokit/types@npm:^9.2.3":
|
||||
version: 9.3.2
|
||||
resolution: "@octokit/types@npm:9.3.2"
|
||||
dependencies:
|
||||
"@octokit/openapi-types": ^18.0.0
|
||||
checksum: f55d096aaed3e04b8308d4422104fb888f355988056ba7b7ef0a4c397b8a3e54290d7827b06774dbe0c9ce55280b00db486286954f9c265aa6b03091026d9da8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@open-draft/until@npm:^1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "@open-draft/until@npm:1.0.3"
|
||||
@ -13232,7 +13407,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.0":
|
||||
"ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.0, ansi-escapes@npm:^4.3.2":
|
||||
version: 4.3.2
|
||||
resolution: "ansi-escapes@npm:4.3.2"
|
||||
dependencies:
|
||||
@ -14102,6 +14277,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"before-after-hook@npm:^2.2.0":
|
||||
version: 2.2.3
|
||||
resolution: "before-after-hook@npm:2.2.3"
|
||||
checksum: a1a2430976d9bdab4cd89cb50d27fa86b19e2b41812bf1315923b0cba03371ebca99449809226425dd3bcef20e010db61abdaff549278e111d6480034bebae87
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"better-opn@npm:^2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "better-opn@npm:2.1.1"
|
||||
@ -15019,6 +15201,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cli-width@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "cli-width@npm:4.0.0"
|
||||
checksum: 1ec12311217cc8b2d018646a58b61424d2348def598fb58ba2c32e28f0bcb59a35cef168110311cefe3340abf00e5171b351de6c3e2c084bd1642e6e2a9e144e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"client-only@npm:0.0.1, client-only@npm:^0.0.1":
|
||||
version: 0.0.1
|
||||
resolution: "client-only@npm:0.0.1"
|
||||
@ -15026,7 +15215,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"clipanion@npm:^3.1.0":
|
||||
"clipanion@npm:^3.1.0, clipanion@npm:^3.2.0":
|
||||
version: 3.2.1
|
||||
resolution: "clipanion@npm:3.2.1"
|
||||
dependencies:
|
||||
@ -16148,6 +16337,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"deprecation@npm:^2.0.0":
|
||||
version: 2.3.1
|
||||
resolution: "deprecation@npm:2.3.1"
|
||||
checksum: f56a05e182c2c195071385455956b0c4106fe14e36245b00c689ceef8e8ab639235176a96977ba7c74afb173317fac2e0ec6ec7a1c6d1e6eaa401c586c714132
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dequal@npm:^2.0.2, dequal@npm:^2.0.3":
|
||||
version: 2.0.3
|
||||
resolution: "dequal@npm:2.0.3"
|
||||
@ -17450,6 +17646,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"escape-string-regexp@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "escape-string-regexp@npm:5.0.0"
|
||||
checksum: 20daabe197f3cb198ec28546deebcf24b3dbb1a5a269184381b3116d12f0532e06007f4bc8da25669d6a7f8efb68db0758df4cd981f57bc5b57f521a3e12c59e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"escodegen@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "escodegen@npm:2.0.0"
|
||||
@ -18360,6 +18563,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"figures@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "figures@npm:5.0.0"
|
||||
dependencies:
|
||||
escape-string-regexp: ^5.0.0
|
||||
is-unicode-supported: ^1.2.0
|
||||
checksum: e6e8b6d1df2f554d4effae4a5ceff5d796f9449f6d4e912d74dab7d5f25916ecda6c305b9084833157d56485a0c78b37164430ddc5675bcee1330e346710669e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"file-entry-cache@npm:^6.0.1":
|
||||
version: 6.0.1
|
||||
resolution: "file-entry-cache@npm:6.0.1"
|
||||
@ -20327,6 +20540,29 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"inquirer@npm:^9.1.5":
|
||||
version: 9.2.7
|
||||
resolution: "inquirer@npm:9.2.7"
|
||||
dependencies:
|
||||
ansi-escapes: ^4.3.2
|
||||
chalk: ^5.2.0
|
||||
cli-cursor: ^3.1.0
|
||||
cli-width: ^4.0.0
|
||||
external-editor: ^3.0.3
|
||||
figures: ^5.0.0
|
||||
lodash: ^4.17.21
|
||||
mute-stream: 1.0.0
|
||||
ora: ^5.4.1
|
||||
run-async: ^3.0.0
|
||||
rxjs: ^7.8.1
|
||||
string-width: ^4.2.3
|
||||
strip-ansi: ^6.0.1
|
||||
through: ^2.3.6
|
||||
wrap-ansi: ^6.0.1
|
||||
checksum: 5522fd4af72aec151d92a8156d24ff55c2e5f68177eac1e704a594004b31b3215ff92c59a3105691b8e237640995efd55df0aa2e3b47053fb27768ded760fbf5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"internal-slot@npm:^1.0.3, internal-slot@npm:^1.0.4, internal-slot@npm:^1.0.5":
|
||||
version: 1.0.5
|
||||
resolution: "internal-slot@npm:1.0.5"
|
||||
@ -20741,7 +20977,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-plain-object@npm:5.0.0":
|
||||
"is-plain-object@npm:5.0.0, is-plain-object@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "is-plain-object@npm:5.0.0"
|
||||
checksum: e32d27061eef62c0847d303125440a38660517e586f2f3db7c9d179ae5b6674ab0f469d519b2e25c147a1a3bc87156d0d5f4d8821e0ce4a9ee7fe1fcf11ce45c
|
||||
@ -20906,7 +21142,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-unicode-supported@npm:*":
|
||||
"is-unicode-supported@npm:*, is-unicode-supported@npm:^1.2.0":
|
||||
version: 1.3.0
|
||||
resolution: "is-unicode-supported@npm:1.3.0"
|
||||
checksum: 20a1fc161afafaf49243551a5ac33b6c4cf0bbcce369fcd8f2951fbdd000c30698ce320de3ee6830497310a8f41880f8066d440aa3eb0a853e2aa4836dd89abc
|
||||
@ -23972,6 +24208,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mute-stream@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "mute-stream@npm:1.0.0"
|
||||
checksum: 36fc968b0e9c9c63029d4f9dc63911950a3bdf55c9a87f58d3a266289b67180201cade911e7699f8b2fa596b34c9db43dad37649e3f7fdd13c3bb9edb0017ee7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mz@npm:^2.7.0":
|
||||
version: 2.7.0
|
||||
resolution: "mz@npm:2.7.0"
|
||||
@ -27558,6 +27801,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"run-async@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "run-async@npm:3.0.0"
|
||||
checksum: 280c03d5a88603f48103fc6fd69f07fb0c392a1e0d319c34ec96a2516030e07ba06f79231a563c78698b882649c2fc1fda601bc84705f57d50efcd1fa506cfc0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"run-parallel@npm:^1.1.9":
|
||||
version: 1.2.0
|
||||
resolution: "run-parallel@npm:1.2.0"
|
||||
@ -29597,7 +29847,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typanion@npm:^3.8.0":
|
||||
"typanion@npm:^3.12.1, typanion@npm:^3.8.0":
|
||||
version: 3.12.1
|
||||
resolution: "typanion@npm:3.12.1"
|
||||
checksum: a2e26fa216f8a1dbd2ffbaacb75b1e2dc042a0356e9702fba05a968cad95d9f661b24e37f6c6d8c3adad2c8582c99fca4826ff26a2d07cd2ae617ea87e6187eb
|
||||
@ -30033,6 +30283,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"universal-user-agent@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "universal-user-agent@npm:6.0.0"
|
||||
checksum: 5092bbc80dd0d583cef0b62c17df0043193b74f425112ea6c1f69bc5eda21eeec7a08d8c4f793a277eb2202ffe9b44bec852fa3faff971234cd209874d1b79ef
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"universalify@npm:^0.1.0":
|
||||
version: 0.1.2
|
||||
resolution: "universalify@npm:0.1.2"
|
||||
@ -31038,7 +31295,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"wrap-ansi@npm:^6.2.0":
|
||||
"wrap-ansi@npm:^6.0.1, wrap-ansi@npm:^6.2.0":
|
||||
version: 6.2.0
|
||||
resolution: "wrap-ansi@npm:6.2.0"
|
||||
dependencies:
|
||||
|
Loading…
Reference in New Issue
Block a user