mirror of
https://github.com/toeverything/AFFiNE.git
synced 2025-01-09 01:09:49 +03:00
feat(storage): binding jwst storage to node (#2808)
This commit is contained in:
parent
86616e152d
commit
2c95bfcc3d
@ -21,7 +21,8 @@
|
|||||||
"native",
|
"native",
|
||||||
"templates",
|
"templates",
|
||||||
"y-indexeddb",
|
"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
|
.cargo-cache
|
||||||
target/${{ inputs.target }}
|
target/${{ inputs.target }}
|
||||||
key: stable-${{ inputs.target }}-cargo-cache
|
key: stable-${{ inputs.target }}-cargo-cache
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
if: ${{ inputs.target != 'x86_64-unknown-linux-gnu' && inputs.target != 'aarch64-unknown-linux-gnu' }}
|
if: ${{ inputs.target != 'x86_64-unknown-linux-gnu' && inputs.target != 'aarch64-unknown-linux-gnu' }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: yarn nx build @affine/native --target ${{ inputs.target }}
|
run: |
|
||||||
|
yarn nx build @affine/native --target ${{ inputs.target }}
|
||||||
env:
|
env:
|
||||||
NX_CLOUD_ACCESS_TOKEN: ${{ inputs.nx_token }}
|
NX_CLOUD_ACCESS_TOKEN: ${{ inputs.nx_token }}
|
||||||
|
|
||||||
@ -41,10 +41,10 @@ runs:
|
|||||||
with:
|
with:
|
||||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
|
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 }}
|
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: |
|
||||||
export CC=x86_64-unknown-linux-gnu-gcc &&
|
export CC=x86_64-unknown-linux-gnu-gcc
|
||||||
export CC_x86_64_unknown_linux_gnu=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 }} &&
|
yarn nx build @affine/native --target ${{ inputs.target }}
|
||||||
chmod -R 777 node_modules/.cache
|
chmod -R 777 node_modules/.cache
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
@ -53,6 +53,6 @@ runs:
|
|||||||
with:
|
with:
|
||||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
|
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 }}
|
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 }}
|
yarn nx build @affine/native --target ${{ inputs.target }}
|
||||||
chmod -R 777 node_modules/.cache
|
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
|
working-directory: apps/server
|
||||||
env:
|
env:
|
||||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
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
|
- name: Run server tests
|
||||||
run: yarn nx test:coverage @affine/server
|
run: yarn nx test:coverage @affine/server
|
||||||
env:
|
env:
|
||||||
|
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
|
||||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||||
- name: Upload server test coverage results
|
- name: Upload server test coverage results
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v3
|
||||||
|
@ -1,3 +1,23 @@
|
|||||||
# Server
|
# 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"
|
"postinstall": "prisma generate"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@affine/storage": "workspace:*",
|
||||||
"@apollo/server": "^4.7.4",
|
"@apollo/server": "^4.7.4",
|
||||||
"@auth/prisma-adapter": "^1.0.0",
|
"@auth/prisma-adapter": "^1.0.0",
|
||||||
"@aws-sdk/client-s3": "^3.359.0",
|
"@aws-sdk/client-s3": "^3.359.0",
|
||||||
|
@ -8,10 +8,13 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Workspace {
|
model Workspace {
|
||||||
id String @id @default(uuid()) @db.VarChar
|
id String @id @default(uuid()) @db.VarChar
|
||||||
public Boolean
|
public Boolean
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
users UserWorkspacePermission[]
|
users UserWorkspacePermission[]
|
||||||
|
blobs Blob[]
|
||||||
|
docs Doc[]
|
||||||
|
optimizedBlobs OptimizedBlob[]
|
||||||
|
|
||||||
@@map("workspaces")
|
@@map("workspaces")
|
||||||
}
|
}
|
||||||
@ -86,3 +89,44 @@ model VerificationToken {
|
|||||||
@@unique([identifier, token])
|
@@unique([identifier, token])
|
||||||
@@map("verificationtokens")
|
@@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 { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { ConfigModule } from './config';
|
import { ConfigModule } from './config';
|
||||||
import { GqlModule } from './graphql.module';
|
import { GqlModule } from './graphql.module';
|
||||||
import { BusinessModules } from './modules';
|
import { BusinessModules } from './modules';
|
||||||
import { PrismaModule } from './prisma';
|
import { PrismaModule } from './prisma';
|
||||||
|
import { StorageModule } from './storage';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
GqlModule,
|
GqlModule,
|
||||||
ConfigModule.forRoot(),
|
ConfigModule.forRoot(),
|
||||||
|
StorageModule.forRoot(),
|
||||||
...BusinessModules,
|
...BusinessModules,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
@ -132,6 +132,13 @@ export interface AFFiNEConfig {
|
|||||||
*/
|
*/
|
||||||
get origin(): string;
|
get origin(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the database config
|
||||||
|
*/
|
||||||
|
db: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the apollo driver config
|
* the apollo driver config
|
||||||
*/
|
*/
|
||||||
|
@ -52,6 +52,9 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => ({
|
|||||||
get baseUrl() {
|
get baseUrl() {
|
||||||
return `${this.origin}${this.path}`;
|
return `${this.origin}${this.path}`;
|
||||||
},
|
},
|
||||||
|
db: {
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
graphql: {
|
graphql: {
|
||||||
buildSchemaOptions: {
|
buildSchemaOptions: {
|
||||||
numberScalarMode: 'integer',
|
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';
|
import { parseEnvValue } from './def';
|
||||||
|
|
||||||
for (const env in AFFiNE.ENV_MAP) {
|
export function registerEnvs() {
|
||||||
const config = AFFiNE.ENV_MAP[env];
|
for (const env in globalThis.AFFiNE.ENV_MAP) {
|
||||||
const [path, value] =
|
const config = globalThis.AFFiNE.ENV_MAP[env];
|
||||||
typeof config === 'string'
|
const [path, value] =
|
||||||
? [config, process.env[env]]
|
typeof config === 'string'
|
||||||
: [config[0], parseEnvValue(process.env[env], config[1])];
|
? [config, process.env[env]]
|
||||||
|
: [config[0], parseEnvValue(process.env[env], config[1])];
|
||||||
|
|
||||||
if (typeof value !== 'undefined') {
|
if (typeof value !== 'undefined') {
|
||||||
set(globalThis.AFFiNE, path, process.env[env]);
|
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 type { DynamicModule, FactoryProvider } from '@nestjs/common';
|
||||||
import { merge } from 'lodash-es';
|
import { merge } from 'lodash-es';
|
||||||
|
|
||||||
import type { DeepPartial } from '../utils/types';
|
import type { DeepPartial } from '../utils/types';
|
||||||
import type { AFFiNEConfig } from './def';
|
import type { AFFiNEConfig } from './def';
|
||||||
|
|
||||||
|
import '../prelude';
|
||||||
|
|
||||||
type ConstructorOf<T> = {
|
type ConstructorOf<T> = {
|
||||||
new (): T;
|
new (): T;
|
||||||
};
|
};
|
||||||
@ -37,11 +40,14 @@ function createConfigProvider(
|
|||||||
provide: Config,
|
provide: Config,
|
||||||
useFactory: () => {
|
useFactory: () => {
|
||||||
const wrapper = new Config();
|
const wrapper = new Config();
|
||||||
const config = merge({}, AFFiNE, override);
|
const config = merge({}, globalThis.AFFiNE, override);
|
||||||
|
|
||||||
const proxy: Config = new Proxy(wrapper, {
|
const proxy: Config = new Proxy(wrapper, {
|
||||||
get: (_target, property: keyof Config) => {
|
get: (_target, property: keyof Config) => {
|
||||||
const desc = Object.getOwnPropertyDescriptor(AFFiNE, property);
|
const desc = Object.getOwnPropertyDescriptor(
|
||||||
|
globalThis.AFFiNE,
|
||||||
|
property
|
||||||
|
);
|
||||||
if (desc?.get) {
|
if (desc?.get) {
|
||||||
return desc.get.call(proxy);
|
return desc.get.call(proxy);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import './prelude';
|
/// <reference types="./global.d.ts" />
|
||||||
|
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import type { NestExpressApplication } from '@nestjs/platform-express';
|
import type { NestExpressApplication } from '@nestjs/platform-express';
|
||||||
import { static as staticMiddleware } from '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 { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { WorkspacesController } from './controller';
|
||||||
import { PermissionService } from './permission';
|
import { PermissionService } from './permission';
|
||||||
import { WorkspaceResolver } from './resolver';
|
import { WorkspaceResolver } from './resolver';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [WorkspaceResolver, PermissionService],
|
providers: [WorkspaceResolver, PermissionService, WorkspacesController],
|
||||||
exports: [PermissionService],
|
exports: [PermissionService],
|
||||||
})
|
})
|
||||||
export class WorkspaceModule {}
|
export class WorkspaceModule {}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { Storage } from '@affine/storage';
|
||||||
import { ForbiddenException, NotFoundException } from '@nestjs/common';
|
import { ForbiddenException, NotFoundException } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
Args,
|
Args,
|
||||||
@ -16,8 +17,11 @@ import {
|
|||||||
Resolver,
|
Resolver,
|
||||||
} from '@nestjs/graphql';
|
} from '@nestjs/graphql';
|
||||||
import type { User, Workspace } from '@prisma/client';
|
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 { PrismaService } from '../../prisma';
|
||||||
|
import type { FileUpload } from '../../types';
|
||||||
import { Auth, CurrentUser } from '../auth';
|
import { Auth, CurrentUser } from '../auth';
|
||||||
import { UserType } from '../users/resolver';
|
import { UserType } from '../users/resolver';
|
||||||
import { PermissionService } from './permission';
|
import { PermissionService } from './permission';
|
||||||
@ -55,7 +59,8 @@ export class UpdateWorkspaceInput extends PickType(
|
|||||||
export class WorkspaceResolver {
|
export class WorkspaceResolver {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly permissionProvider: PermissionService
|
private readonly permissionProvider: PermissionService,
|
||||||
|
private readonly storage: Storage
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ResolveField(() => Permission, {
|
@ResolveField(() => Permission, {
|
||||||
@ -174,8 +179,25 @@ export class WorkspaceResolver {
|
|||||||
@Mutation(() => WorkspaceType, {
|
@Mutation(() => WorkspaceType, {
|
||||||
description: 'Create a new workspace',
|
description: 'Create a new workspace',
|
||||||
})
|
})
|
||||||
async createWorkspace(@CurrentUser() user: User) {
|
async createWorkspace(
|
||||||
return this.prisma.workspace.create({
|
@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: {
|
data: {
|
||||||
public: false,
|
public: false,
|
||||||
users: {
|
users: {
|
||||||
@ -191,6 +213,10 @@ export class WorkspaceResolver {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.storage.createWorkspace(workspace.id, buffer);
|
||||||
|
|
||||||
|
return workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => WorkspaceType, {
|
@Mutation(() => WorkspaceType, {
|
||||||
@ -221,8 +247,15 @@ export class WorkspaceResolver {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.prisma.userWorkspacePermission.deleteMany({
|
||||||
|
where: {
|
||||||
|
workspaceId: id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// delete all related data, like websocket connections, blobs, etc.
|
// delete all related data, like websocket connections, blobs, etc.
|
||||||
|
await this.storage.deleteWorkspace(id);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -283,4 +316,28 @@ export class WorkspaceResolver {
|
|||||||
|
|
||||||
return this.permissionProvider.revoke(workspaceId, user.id);
|
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 'reflect-metadata';
|
||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
|
|
||||||
import { getDefaultAFFiNEConfig } from './config/default';
|
import { getDefaultAFFiNEConfig, registerEnvs } from './config/default';
|
||||||
|
|
||||||
globalThis.AFFiNE = getDefaultAFFiNEConfig();
|
globalThis.AFFiNE = getDefaultAFFiNEConfig();
|
||||||
|
|
||||||
|
globalThis.AFFiNE.ENV_MAP = {
|
||||||
|
DATABASE_URL: 'db.url',
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEnvs();
|
||||||
|
@ -106,7 +106,7 @@ type Mutation {
|
|||||||
"""
|
"""
|
||||||
Create a new workspace
|
Create a new workspace
|
||||||
"""
|
"""
|
||||||
createWorkspace: WorkspaceType!
|
createWorkspace(init: Upload!): WorkspaceType!
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Update workspace
|
Update workspace
|
||||||
@ -121,6 +121,7 @@ type Mutation {
|
|||||||
revoke(workspaceId: String!, userId: String!): Boolean!
|
revoke(workspaceId: String!, userId: String!): Boolean!
|
||||||
acceptInvite(workspaceId: String!): Boolean!
|
acceptInvite(workspaceId: String!): Boolean!
|
||||||
leaveWorkspace(workspaceId: String!): Boolean!
|
leaveWorkspace(workspaceId: String!): Boolean!
|
||||||
|
uploadBlob(workspaceId: String!, blob: Upload!): String!
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Upload user avatar
|
Upload user avatar
|
||||||
@ -128,6 +129,11 @@ type Mutation {
|
|||||||
uploadAvatar(id: String!, avatar: Upload!): UserType!
|
uploadAvatar(id: String!, avatar: Upload!): UserType!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
The `Upload` scalar type represents a file upload.
|
||||||
|
"""
|
||||||
|
scalar Upload
|
||||||
|
|
||||||
input UpdateWorkspaceInput {
|
input UpdateWorkspaceInput {
|
||||||
"""
|
"""
|
||||||
is Public workspace
|
is Public workspace
|
||||||
@ -135,8 +141,3 @@ input UpdateWorkspaceInput {
|
|||||||
public: Boolean
|
public: Boolean
|
||||||
id: ID!
|
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 request from 'supertest';
|
||||||
|
|
||||||
import { AppModule } from '../app';
|
import { AppModule } from '../app';
|
||||||
import { getDefaultAFFiNEConfig } from '../config/default';
|
|
||||||
|
|
||||||
const gql = '/graphql';
|
const gql = '/graphql';
|
||||||
|
|
||||||
globalThis.AFFiNE = getDefaultAFFiNEConfig();
|
|
||||||
|
|
||||||
describe('AppModule', () => {
|
describe('AppModule', () => {
|
||||||
let app: INestApplication;
|
let app: INestApplication;
|
||||||
|
|
||||||
@ -76,33 +73,14 @@ describe('AppModule', () => {
|
|||||||
.auth(token, { type: 'bearer' })
|
.auth(token, { type: 'bearer' })
|
||||||
.send({
|
.send({
|
||||||
query: `
|
query: `
|
||||||
mutation {
|
query {
|
||||||
createWorkspace {
|
__typename
|
||||||
id
|
|
||||||
public
|
|
||||||
createdAt
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect(res => {
|
.expect(res => {
|
||||||
ok(
|
ok(res.body.data.__typename === 'Query');
|
||||||
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'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
/// <reference types="../global.d.ts" />
|
||||||
import { ok } from 'node:assert';
|
import { ok } from 'node:assert';
|
||||||
import { beforeEach, test } from 'node:test';
|
import { beforeEach, test } from 'node:test';
|
||||||
|
|
||||||
@ -5,14 +6,11 @@ import { Test } from '@nestjs/testing';
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
import { ConfigModule } from '../config';
|
import { ConfigModule } from '../config';
|
||||||
import { getDefaultAFFiNEConfig } from '../config/default';
|
|
||||||
import { GqlModule } from '../graphql.module';
|
import { GqlModule } from '../graphql.module';
|
||||||
import { AuthModule } from '../modules/auth';
|
import { AuthModule } from '../modules/auth';
|
||||||
import { AuthService } from '../modules/auth/service';
|
import { AuthService } from '../modules/auth/service';
|
||||||
import { PrismaModule } from '../prisma';
|
import { PrismaModule } from '../prisma';
|
||||||
|
|
||||||
globalThis.AFFiNE = getDefaultAFFiNEConfig();
|
|
||||||
|
|
||||||
let auth: AuthService;
|
let auth: AuthService;
|
||||||
|
|
||||||
// cleanup database before each test
|
// cleanup database before each test
|
||||||
|
@ -4,9 +4,6 @@ import { beforeEach, test } from 'node:test';
|
|||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
|
|
||||||
import { Config, ConfigModule } from '../config';
|
import { Config, ConfigModule } from '../config';
|
||||||
import { getDefaultAFFiNEConfig } from '../config/default';
|
|
||||||
|
|
||||||
globalThis.AFFiNE = getDefaultAFFiNEConfig();
|
|
||||||
|
|
||||||
let config: Config;
|
let config: Config;
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
@ -1,22 +1,21 @@
|
|||||||
import { ok } from 'node:assert';
|
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 type { INestApplication } from '@nestjs/common';
|
||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
import { PrismaClient } from '@prisma/client';
|
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 request from 'supertest';
|
||||||
|
|
||||||
import { AppModule } from '../app';
|
import { AppModule } from '../app';
|
||||||
import { getDefaultAFFiNEConfig } from '../config/default';
|
|
||||||
import type { TokenType } from '../modules/auth';
|
import type { TokenType } from '../modules/auth';
|
||||||
import type { UserType } from '../modules/users';
|
import type { UserType } from '../modules/users';
|
||||||
import type { WorkspaceType } from '../modules/workspaces';
|
import type { WorkspaceType } from '../modules/workspaces';
|
||||||
|
|
||||||
const gql = '/graphql';
|
const gql = '/graphql';
|
||||||
|
|
||||||
globalThis.AFFiNE = getDefaultAFFiNEConfig();
|
describe('Workspace Module', () => {
|
||||||
|
|
||||||
describe('AppModule', () => {
|
|
||||||
let app: INestApplication;
|
let app: INestApplication;
|
||||||
|
|
||||||
// cleanup database before each test
|
// cleanup database before each test
|
||||||
@ -32,6 +31,12 @@ describe('AppModule', () => {
|
|||||||
imports: [AppModule],
|
imports: [AppModule],
|
||||||
}).compile();
|
}).compile();
|
||||||
app = module.createNestApplication();
|
app = module.createNestApplication();
|
||||||
|
app.use(
|
||||||
|
graphqlUploadExpress({
|
||||||
|
maxFileSize: 10 * 1024 * 1024,
|
||||||
|
maxFiles: 5,
|
||||||
|
})
|
||||||
|
);
|
||||||
await app.init();
|
await app.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -63,15 +68,20 @@ describe('AppModule', () => {
|
|||||||
const res = await request(app.getHttpServer())
|
const res = await request(app.getHttpServer())
|
||||||
.post(gql)
|
.post(gql)
|
||||||
.auth(token, { type: 'bearer' })
|
.auth(token, { type: 'bearer' })
|
||||||
.send({
|
.field(
|
||||||
query: `
|
'operations',
|
||||||
mutation {
|
JSON.stringify({
|
||||||
createWorkspace {
|
name: 'createWorkspace',
|
||||||
|
query: `mutation createWorkspace($init: Upload!) {
|
||||||
|
createWorkspace(init: $init) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}`,
|
||||||
`,
|
variables: { init: null },
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
.field('map', JSON.stringify({ '0': ['variables.init'] }))
|
||||||
|
.attach('0', Buffer.from([0, 0]), 'init.data')
|
||||||
.expect(200);
|
.expect(200);
|
||||||
return res.body.data.createWorkspace;
|
return res.body.data.createWorkspace;
|
||||||
}
|
}
|
||||||
@ -151,21 +161,21 @@ describe('AppModule', () => {
|
|||||||
return res.body.data.revoke;
|
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');
|
const user = await registerUser('u1', 'u1@affine.pro', '123456');
|
||||||
ok(typeof user.id === 'string', 'user.id is not a string');
|
ok(typeof user.id === 'string', 'user.id is not a string');
|
||||||
ok(user.name === 'u1', 'user.name is not valid');
|
ok(user.name === 'u1', 'user.name is not valid');
|
||||||
ok(user.email === 'u1@affine.pro', 'user.email 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 user = await registerUser('u1', 'u1@affine.pro', '1');
|
||||||
|
|
||||||
const workspace = await createWorkspace(user.token.token);
|
const workspace = await createWorkspace(user.token.token);
|
||||||
ok(typeof workspace.id === 'string', 'workspace.id is not a string');
|
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 u1 = await registerUser('u1', 'u1@affine.pro', '1');
|
||||||
const u2 = await registerUser('u2', 'u2@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');
|
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 u1 = await registerUser('u1', 'u1@affine.pro', '1');
|
||||||
const u2 = await registerUser('u2', 'u2@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');
|
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 u1 = await registerUser('u1', 'u1@affine.pro', '1');
|
||||||
const u2 = await registerUser('u2', 'u2@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');
|
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 u1 = await registerUser('u1', 'u1@affine.pro', '1');
|
||||||
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
|
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
|
||||||
|
|
||||||
|
@ -20,6 +20,9 @@
|
|||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "./tsconfig.node.json"
|
"path": "./tsconfig.node.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../packages/storage/tsconfig.json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ts-node": {
|
"ts-node": {
|
||||||
|
1
nx.json
1
nx.json
@ -35,6 +35,7 @@
|
|||||||
"{workspaceRoot}/apps/storybook/storybook-static",
|
"{workspaceRoot}/apps/storybook/storybook-static",
|
||||||
"{workspaceRoot}/packages/i18n/src/i18n-generated.ts",
|
"{workspaceRoot}/packages/i18n/src/i18n-generated.ts",
|
||||||
"{workspaceRoot}/packages/native/affine.*.node",
|
"{workspaceRoot}/packages/native/affine.*.node",
|
||||||
|
"{workspaceRoot}/packages/storage/*.node",
|
||||||
"{workspaceRoot}/affine.db",
|
"{workspaceRoot}/affine.db",
|
||||||
"{workspaceRoot/apps/electron/dist"
|
"{workspaceRoot/apps/electron/dist"
|
||||||
],
|
],
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
mutation createWorkspace {
|
mutation createWorkspace($init: Upload!) {
|
||||||
createWorkspace {
|
createWorkspace(init: $init) {
|
||||||
id
|
id
|
||||||
public
|
public
|
||||||
createdAt
|
createdAt
|
||||||
|
@ -11,10 +11,10 @@ export const createWorkspaceMutation = {
|
|||||||
id: 'createWorkspaceMutation' as const,
|
id: 'createWorkspaceMutation' as const,
|
||||||
operationName: 'createWorkspace',
|
operationName: 'createWorkspace',
|
||||||
definitionName: 'createWorkspace',
|
definitionName: 'createWorkspace',
|
||||||
containsFile: false,
|
containsFile: true,
|
||||||
query: `
|
query: `
|
||||||
mutation createWorkspace {
|
mutation createWorkspace($init: Upload!) {
|
||||||
createWorkspace {
|
createWorkspace(init: $init) {
|
||||||
id
|
id
|
||||||
public
|
public
|
||||||
createdAt
|
createdAt
|
||||||
|
@ -46,7 +46,9 @@ export interface UpdateWorkspaceInput {
|
|||||||
public: InputMaybe<Scalars['Boolean']['input']>;
|
public: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreateWorkspaceMutationVariables = Exact<{ [key: string]: never }>;
|
export type CreateWorkspaceMutationVariables = Exact<{
|
||||||
|
init: Scalars['Upload']['input'];
|
||||||
|
}>;
|
||||||
|
|
||||||
export type CreateWorkspaceMutation = {
|
export type CreateWorkspaceMutation = {
|
||||||
__typename?: 'Mutation';
|
__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/*"],
|
"@toeverything/plugin-infra/*": ["./packages/plugin-infra/src/*"],
|
||||||
"@affine/native": ["./packages/native/index.d.ts"],
|
"@affine/native": ["./packages/native/index.d.ts"],
|
||||||
"@affine/native/*": ["./packages/native/*"],
|
"@affine/native/*": ["./packages/native/*"],
|
||||||
|
"@affine/storage": ["./packages/storage/index.d.ts"],
|
||||||
|
|
||||||
// Development only
|
// Development only
|
||||||
"@affine/electron/*": ["./apps/electron/src/*"]
|
"@affine/electron/*": ["./apps/electron/src/*"]
|
||||||
|
269
yarn.lock
269
yarn.lock
@ -387,6 +387,7 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@affine/server@workspace:apps/server"
|
resolution: "@affine/server@workspace:apps/server"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@affine/storage": "workspace:*"
|
||||||
"@apollo/server": ^4.7.4
|
"@apollo/server": ^4.7.4
|
||||||
"@auth/prisma-adapter": ^1.0.0
|
"@auth/prisma-adapter": ^1.0.0
|
||||||
"@aws-sdk/client-s3": ^3.359.0
|
"@aws-sdk/client-s3": ^3.359.0
|
||||||
@ -426,6 +427,16 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
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":
|
"@affine/storybook@workspace:apps/storybook":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@affine/storybook@workspace:apps/storybook"
|
resolution: "@affine/storybook@workspace:apps/storybook"
|
||||||
@ -7422,6 +7433,25 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@napi-rs/image-android-arm-eabi@npm:1.6.1":
|
||||||
version: 1.6.1
|
version: 1.6.1
|
||||||
resolution: "@napi-rs/image-android-arm-eabi@npm:1.6.1"
|
resolution: "@napi-rs/image-android-arm-eabi@npm:1.6.1"
|
||||||
@ -8356,6 +8386,151 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@open-draft/until@npm:^1.0.3":
|
||||||
version: 1.0.3
|
version: 1.0.3
|
||||||
resolution: "@open-draft/until@npm:1.0.3"
|
resolution: "@open-draft/until@npm:1.0.3"
|
||||||
@ -13232,7 +13407,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 4.3.2
|
||||||
resolution: "ansi-escapes@npm:4.3.2"
|
resolution: "ansi-escapes@npm:4.3.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -14102,6 +14277,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"better-opn@npm:^2.1.1":
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
resolution: "better-opn@npm:2.1.1"
|
resolution: "better-opn@npm:2.1.1"
|
||||||
@ -15019,6 +15201,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"client-only@npm:0.0.1, client-only@npm:^0.0.1":
|
||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
resolution: "client-only@npm:0.0.1"
|
resolution: "client-only@npm:0.0.1"
|
||||||
@ -15026,7 +15215,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"clipanion@npm:^3.1.0":
|
"clipanion@npm:^3.1.0, clipanion@npm:^3.2.0":
|
||||||
version: 3.2.1
|
version: 3.2.1
|
||||||
resolution: "clipanion@npm:3.2.1"
|
resolution: "clipanion@npm:3.2.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -16148,6 +16337,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"dequal@npm:^2.0.2, dequal@npm:^2.0.3":
|
||||||
version: 2.0.3
|
version: 2.0.3
|
||||||
resolution: "dequal@npm:2.0.3"
|
resolution: "dequal@npm:2.0.3"
|
||||||
@ -17450,6 +17646,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"escodegen@npm:^2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "escodegen@npm:2.0.0"
|
resolution: "escodegen@npm:2.0.0"
|
||||||
@ -18360,6 +18563,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"file-entry-cache@npm:^6.0.1":
|
||||||
version: 6.0.1
|
version: 6.0.1
|
||||||
resolution: "file-entry-cache@npm:6.0.1"
|
resolution: "file-entry-cache@npm:6.0.1"
|
||||||
@ -20327,6 +20540,29 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"internal-slot@npm:^1.0.3, internal-slot@npm:^1.0.4, internal-slot@npm:^1.0.5":
|
||||||
version: 1.0.5
|
version: 1.0.5
|
||||||
resolution: "internal-slot@npm:1.0.5"
|
resolution: "internal-slot@npm:1.0.5"
|
||||||
@ -20741,7 +20977,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 5.0.0
|
||||||
resolution: "is-plain-object@npm:5.0.0"
|
resolution: "is-plain-object@npm:5.0.0"
|
||||||
checksum: e32d27061eef62c0847d303125440a38660517e586f2f3db7c9d179ae5b6674ab0f469d519b2e25c147a1a3bc87156d0d5f4d8821e0ce4a9ee7fe1fcf11ce45c
|
checksum: e32d27061eef62c0847d303125440a38660517e586f2f3db7c9d179ae5b6674ab0f469d519b2e25c147a1a3bc87156d0d5f4d8821e0ce4a9ee7fe1fcf11ce45c
|
||||||
@ -20906,7 +21142,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"is-unicode-supported@npm:*":
|
"is-unicode-supported@npm:*, is-unicode-supported@npm:^1.2.0":
|
||||||
version: 1.3.0
|
version: 1.3.0
|
||||||
resolution: "is-unicode-supported@npm:1.3.0"
|
resolution: "is-unicode-supported@npm:1.3.0"
|
||||||
checksum: 20a1fc161afafaf49243551a5ac33b6c4cf0bbcce369fcd8f2951fbdd000c30698ce320de3ee6830497310a8f41880f8066d440aa3eb0a853e2aa4836dd89abc
|
checksum: 20a1fc161afafaf49243551a5ac33b6c4cf0bbcce369fcd8f2951fbdd000c30698ce320de3ee6830497310a8f41880f8066d440aa3eb0a853e2aa4836dd89abc
|
||||||
@ -23972,6 +24208,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"mz@npm:^2.7.0":
|
||||||
version: 2.7.0
|
version: 2.7.0
|
||||||
resolution: "mz@npm:2.7.0"
|
resolution: "mz@npm:2.7.0"
|
||||||
@ -27558,6 +27801,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"run-parallel@npm:^1.1.9":
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
resolution: "run-parallel@npm:1.2.0"
|
resolution: "run-parallel@npm:1.2.0"
|
||||||
@ -29597,7 +29847,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"typanion@npm:^3.8.0":
|
"typanion@npm:^3.12.1, typanion@npm:^3.8.0":
|
||||||
version: 3.12.1
|
version: 3.12.1
|
||||||
resolution: "typanion@npm:3.12.1"
|
resolution: "typanion@npm:3.12.1"
|
||||||
checksum: a2e26fa216f8a1dbd2ffbaacb75b1e2dc042a0356e9702fba05a968cad95d9f661b24e37f6c6d8c3adad2c8582c99fca4826ff26a2d07cd2ae617ea87e6187eb
|
checksum: a2e26fa216f8a1dbd2ffbaacb75b1e2dc042a0356e9702fba05a968cad95d9f661b24e37f6c6d8c3adad2c8582c99fca4826ff26a2d07cd2ae617ea87e6187eb
|
||||||
@ -30033,6 +30283,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"universalify@npm:^0.1.0":
|
||||||
version: 0.1.2
|
version: 0.1.2
|
||||||
resolution: "universalify@npm:0.1.2"
|
resolution: "universalify@npm:0.1.2"
|
||||||
@ -31038,7 +31295,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"wrap-ansi@npm:^6.2.0":
|
"wrap-ansi@npm:^6.0.1, wrap-ansi@npm:^6.2.0":
|
||||||
version: 6.2.0
|
version: 6.2.0
|
||||||
resolution: "wrap-ansi@npm:6.2.0"
|
resolution: "wrap-ansi@npm:6.2.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
Loading…
Reference in New Issue
Block a user