Compare commits
1 Commits
feat/coll-
...
revamp/hea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dcfc684ef |
@@ -66,7 +66,6 @@ services:
|
|||||||
# The service that spins up all 3 services at once in one container
|
# The service that spins up all 3 services at once in one container
|
||||||
hoppscotch-aio:
|
hoppscotch-aio:
|
||||||
container_name: hoppscotch-aio
|
container_name: hoppscotch-aio
|
||||||
restart: unless-stopped
|
|
||||||
build:
|
build:
|
||||||
dockerfile: prod.Dockerfile
|
dockerfile: prod.Dockerfile
|
||||||
context: .
|
context: .
|
||||||
|
|||||||
@@ -32,9 +32,6 @@
|
|||||||
"lint-staged": "12.4.0"
|
"lint-staged": "12.4.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
|
||||||
"vue": "3.3.9"
|
|
||||||
},
|
|
||||||
"packageExtensions": {
|
"packageExtensions": {
|
||||||
"httpsnippet@^3.0.1": {
|
"httpsnippet@^3.0.1": {
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
24
packages/dioc/.gitignore
vendored
Normal file
24
packages/dioc/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
141
packages/dioc/README.md
Normal file
141
packages/dioc/README.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# dioc
|
||||||
|
|
||||||
|
A small and lightweight dependency injection / inversion of control system.
|
||||||
|
|
||||||
|
### About
|
||||||
|
|
||||||
|
`dioc` is a really simple **DI/IOC** system where you write services (which are singletons per container) that can depend on each other and emit events that can be listened upon.
|
||||||
|
|
||||||
|
### Demo
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Service, Container } from "dioc"
|
||||||
|
|
||||||
|
// Here is a simple service, which you can define by extending the Service class
|
||||||
|
// and providing an ID static field (of type string)
|
||||||
|
export class PersistenceService extends Service {
|
||||||
|
// This should be unique for each container
|
||||||
|
public static ID = "PERSISTENCE_SERVICE"
|
||||||
|
|
||||||
|
public read(key: string): string | undefined {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
public write(key: string, value: string) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TodoServiceEvent =
|
||||||
|
| { type: "TODO_CREATED"; index: number }
|
||||||
|
| { type: "TODO_DELETED"; index: number }
|
||||||
|
|
||||||
|
// Services have a built in event system
|
||||||
|
// Define the generic argument to say what are the possible emitted values
|
||||||
|
export class TodoService extends Service<TodoServiceEvent> {
|
||||||
|
public static ID = "TODO_SERVICE"
|
||||||
|
|
||||||
|
// Inject persistence service into this service
|
||||||
|
private readonly persistence = this.bind(PersistenceService)
|
||||||
|
|
||||||
|
public todos = []
|
||||||
|
|
||||||
|
// Service constructors cannot have arguments
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.todos = JSON.parse(this.persistence.read("todos") ?? "[]")
|
||||||
|
}
|
||||||
|
|
||||||
|
public addTodo(text: string) {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// You can access services via the bound fields
|
||||||
|
this.persistence.write("todos", JSON.stringify(this.todos))
|
||||||
|
|
||||||
|
// This is how you emit an event
|
||||||
|
this.emit({
|
||||||
|
type: "TODO_CREATED",
|
||||||
|
index,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeTodo(index: number) {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
this.emit({
|
||||||
|
type: "TODO_DELETED",
|
||||||
|
index,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Services need a container to run in
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
// You can initialize and get services using Container#bind
|
||||||
|
// It will automatically initialize the service (and its dependencies)
|
||||||
|
const todoService = container.bind(TodoService) // Returns an instance of TodoService
|
||||||
|
```
|
||||||
|
|
||||||
|
### Demo (Unit Test)
|
||||||
|
|
||||||
|
`dioc/testing` contains `TestContainer` which lets you bind mocked services to the container.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { TestContainer } from "dioc/testing"
|
||||||
|
import { TodoService, PersistenceService } from "./demo.ts" // The above demo code snippet
|
||||||
|
import { describe, it, expect, vi } from "vitest"
|
||||||
|
|
||||||
|
describe("TodoService", () => {
|
||||||
|
it("addTodo writes to persistence", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const writeFn = vi.fn()
|
||||||
|
|
||||||
|
// The first parameter is the service to mock and the second parameter
|
||||||
|
// is the mocked service fields and functions
|
||||||
|
container.bindMock(PersistenceService, {
|
||||||
|
read: () => undefined, // Not really important for this test
|
||||||
|
write: writeFn,
|
||||||
|
})
|
||||||
|
|
||||||
|
// the peristence service bind in TodoService will now use the
|
||||||
|
// above defined mocked implementation
|
||||||
|
const todoService = container.bind(TodoService)
|
||||||
|
|
||||||
|
todoService.addTodo("sup")
|
||||||
|
|
||||||
|
expect(writeFn).toHaveBeenCalledOnce()
|
||||||
|
expect(writeFn).toHaveBeenCalledWith("todos", JSON.stringify(["sup"]))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Demo (Vue)
|
||||||
|
|
||||||
|
`dioc/vue` contains a Vue Plugin and a `useService` composable that allows Vue components to use the defined services.
|
||||||
|
|
||||||
|
In the app entry point:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createApp } from "vue"
|
||||||
|
import { diocPlugin } from "dioc/vue"
|
||||||
|
|
||||||
|
const app = createApp()
|
||||||
|
|
||||||
|
app.use(diocPlugin, {
|
||||||
|
container: new Container(), // You can pass in the container you want to provide to the components here
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
In your Vue components:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { TodoService } from "./demo.ts" // The above demo
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
|
||||||
|
const todoService = useService(TodoService) // Returns an instance of the TodoService class
|
||||||
|
</script>
|
||||||
|
```
|
||||||
2
packages/dioc/index.d.ts
vendored
Normal file
2
packages/dioc/index.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from "./dist/main.d.ts"
|
||||||
|
export * from "./dist/main.d.ts"
|
||||||
147
packages/dioc/lib/container.ts
Normal file
147
packages/dioc/lib/container.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { Service } from "./service"
|
||||||
|
import { Observable, Subject } from 'rxjs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the current container instance in the current operating context.
|
||||||
|
*
|
||||||
|
* NOTE: This should not be used outside of dioc library code
|
||||||
|
*/
|
||||||
|
export let currentContainer: Container | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The events emitted by the container
|
||||||
|
*
|
||||||
|
* `SERVICE_BIND` - emitted when a service is bound to the container directly or as a dependency to another service
|
||||||
|
* `SERVICE_INIT` - emitted when a service is initialized
|
||||||
|
*/
|
||||||
|
export type ContainerEvent =
|
||||||
|
| {
|
||||||
|
type: 'SERVICE_BIND';
|
||||||
|
|
||||||
|
/** The Service ID of the service being bounded (the dependency) */
|
||||||
|
boundeeID: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Service ID of the bounder that is binding the boundee (the dependent)
|
||||||
|
*
|
||||||
|
* NOTE: This will be undefined if the service is bound directly to the container
|
||||||
|
*/
|
||||||
|
bounderID: string | undefined
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'SERVICE_INIT';
|
||||||
|
|
||||||
|
/** The Service ID of the service being initialized */
|
||||||
|
serviceID: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dependency injection container, allows for services to be initialized and maintains the dependency trees.
|
||||||
|
*/
|
||||||
|
export class Container {
|
||||||
|
/** Used during the `bind` operation to detect circular dependencies */
|
||||||
|
private bindStack: string[] = []
|
||||||
|
|
||||||
|
/** The map of bound services to their IDs */
|
||||||
|
protected boundMap = new Map<string, Service<unknown>>()
|
||||||
|
|
||||||
|
/** The RxJS observable representing the event stream */
|
||||||
|
protected event$ = new Subject<ContainerEvent>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether a container has the given service bound
|
||||||
|
* @param service The service to check for
|
||||||
|
*/
|
||||||
|
public hasBound<
|
||||||
|
T extends typeof Service<any> & { ID: string }
|
||||||
|
>(service: T): boolean {
|
||||||
|
return this.boundMap.has(service.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the service bound to the container with the given ID or if not found, undefined.
|
||||||
|
*
|
||||||
|
* NOTE: This is an advanced method and should not be used as much as possible.
|
||||||
|
*
|
||||||
|
* @param serviceID The ID of the service to get
|
||||||
|
*/
|
||||||
|
public getBoundServiceWithID(serviceID: string): Service<unknown> | undefined {
|
||||||
|
return this.boundMap.get(serviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds a service to the container. This is equivalent to marking a service as a dependency.
|
||||||
|
* @param service The class reference of a service to bind
|
||||||
|
* @param bounder The class reference of the service that is binding the service (if bound directly to the container, this should be undefined)
|
||||||
|
*/
|
||||||
|
public bind<T extends typeof Service<any> & { ID: string }>(
|
||||||
|
service: T,
|
||||||
|
bounder: ((typeof Service<T>) & { ID: string }) | undefined = undefined
|
||||||
|
): InstanceType<T> {
|
||||||
|
// We need to store the current container in a variable so that we can restore it after the bind operation
|
||||||
|
const oldCurrentContainer = currentContainer;
|
||||||
|
currentContainer = this;
|
||||||
|
|
||||||
|
// If the service is already bound, return the existing instance
|
||||||
|
if (this.hasBound(service)) {
|
||||||
|
this.event$.next({
|
||||||
|
type: 'SERVICE_BIND',
|
||||||
|
boundeeID: service.ID,
|
||||||
|
bounderID: bounder?.ID // Return the bounder ID if it is defined, else assume its the container
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.boundMap.get(service.ID) as InstanceType<T> // Casted as InstanceType<T> because service IDs and types are expected to match
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect circular dependency and throw error
|
||||||
|
if (this.bindStack.findIndex((serviceID) => serviceID === service.ID) !== -1) {
|
||||||
|
const circularServices = `${this.bindStack.join(' -> ')} -> ${service.ID}`
|
||||||
|
|
||||||
|
throw new Error(`Circular dependency detected.\nChain: ${circularServices}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push the service ID onto the bind stack to detect circular dependencies
|
||||||
|
this.bindStack.push(service.ID)
|
||||||
|
|
||||||
|
// Initialize the service and emit events
|
||||||
|
|
||||||
|
// NOTE: We need to cast the service to any as TypeScript thinks that the service is abstract
|
||||||
|
const instance: Service<any> = new (service as any)()
|
||||||
|
|
||||||
|
this.boundMap.set(service.ID, instance)
|
||||||
|
|
||||||
|
this.bindStack.pop()
|
||||||
|
|
||||||
|
this.event$.next({
|
||||||
|
type: 'SERVICE_INIT',
|
||||||
|
serviceID: service.ID,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.event$.next({
|
||||||
|
type: 'SERVICE_BIND',
|
||||||
|
boundeeID: service.ID,
|
||||||
|
bounderID: bounder?.ID
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Restore the current container
|
||||||
|
currentContainer = oldCurrentContainer;
|
||||||
|
|
||||||
|
// We expect the return type to match the service definition
|
||||||
|
return instance as InstanceType<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an iterator of the currently bound service IDs and their instances
|
||||||
|
*/
|
||||||
|
public getBoundServices(): IterableIterator<[string, Service<any>]> {
|
||||||
|
return this.boundMap.entries()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the public container event stream
|
||||||
|
*/
|
||||||
|
public getEventStream(): Observable<ContainerEvent> {
|
||||||
|
return this.event$.asObservable()
|
||||||
|
}
|
||||||
|
}
|
||||||
2
packages/dioc/lib/main.ts
Normal file
2
packages/dioc/lib/main.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./container"
|
||||||
|
export * from "./service"
|
||||||
65
packages/dioc/lib/service.ts
Normal file
65
packages/dioc/lib/service.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { Observable, Subject } from 'rxjs'
|
||||||
|
import { Container, currentContainer } from './container'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Dioc service that can bound to a container and can bind dependency services.
|
||||||
|
*
|
||||||
|
* NOTE: Services cannot have a constructor that takes arguments.
|
||||||
|
*
|
||||||
|
* @template EventDef The type of events that can be emitted by the service. These will be accessible by event streams
|
||||||
|
*/
|
||||||
|
export abstract class Service<EventDef = {}> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The internal event stream of the service
|
||||||
|
*/
|
||||||
|
private event$ = new Subject<EventDef>()
|
||||||
|
|
||||||
|
/** The container the service is bound to */
|
||||||
|
#container: Container
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
if (!currentContainer) {
|
||||||
|
throw new Error(
|
||||||
|
`Tried to initialize service with no container (ID: ${ (this.constructor as any).ID })`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#container = currentContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds a dependency service into this service.
|
||||||
|
* @param service The class reference of the service to bind
|
||||||
|
*/
|
||||||
|
protected bind<T extends typeof Service<any> & { ID: string }>(service: T): InstanceType<T> {
|
||||||
|
if (!currentContainer) {
|
||||||
|
throw new Error('No currentContainer defined.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentContainer.bind(service, this.constructor as typeof Service<any> & { ID: string })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the container the service is bound to
|
||||||
|
*/
|
||||||
|
protected getContainer(): Container {
|
||||||
|
return this.#container
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits an event on the service's event stream
|
||||||
|
* @param event The event to emit
|
||||||
|
*/
|
||||||
|
protected emit(event: EventDef) {
|
||||||
|
this.event$.next(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the event stream of the service
|
||||||
|
*/
|
||||||
|
public getEventStream(): Observable<EventDef> {
|
||||||
|
|
||||||
|
return this.event$.asObservable()
|
||||||
|
}
|
||||||
|
}
|
||||||
33
packages/dioc/lib/testing.ts
Normal file
33
packages/dioc/lib/testing.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Container, Service } from "./main";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A container that can be used for writing tests, contains additional methods
|
||||||
|
* for binding suitable for writing tests. (see `bindMock`).
|
||||||
|
*/
|
||||||
|
export class TestContainer extends Container {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds a mock service to the container.
|
||||||
|
*
|
||||||
|
* @param service
|
||||||
|
* @param mock
|
||||||
|
*/
|
||||||
|
public bindMock<
|
||||||
|
T extends typeof Service<any> & { ID: string },
|
||||||
|
U extends Partial<InstanceType<T>>
|
||||||
|
>(service: T, mock: U): U {
|
||||||
|
if (this.boundMap.has(service.ID)) {
|
||||||
|
throw new Error(`Service '${service.ID}' already bound to container. Did you already call bindMock on this ?`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.boundMap.set(service.ID, mock as any)
|
||||||
|
|
||||||
|
this.event$.next({
|
||||||
|
type: "SERVICE_BIND",
|
||||||
|
boundeeID: service.ID,
|
||||||
|
bounderID: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
}
|
||||||
34
packages/dioc/lib/vue.ts
Normal file
34
packages/dioc/lib/vue.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Plugin, inject } from "vue"
|
||||||
|
import { Container } from "./container"
|
||||||
|
import { Service } from "./service"
|
||||||
|
|
||||||
|
const VUE_CONTAINER_KEY = Symbol()
|
||||||
|
|
||||||
|
// TODO: Some Vue version issue with plugin generics is breaking type checking
|
||||||
|
/**
|
||||||
|
* The Vue Dioc Plugin, this allows the composables to work and access the container
|
||||||
|
*
|
||||||
|
* NOTE: Make sure you add `vue` as dependency to be able to use this plugin (duh)
|
||||||
|
*/
|
||||||
|
export const diocPlugin: Plugin = {
|
||||||
|
install(app, { container }) {
|
||||||
|
app.provide(VUE_CONTAINER_KEY, container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A composable that binds a service to a Vue Component
|
||||||
|
*
|
||||||
|
* @param service The class reference of the service to bind
|
||||||
|
*/
|
||||||
|
export function useService<
|
||||||
|
T extends typeof Service<any> & { ID: string }
|
||||||
|
>(service: T): InstanceType<T> {
|
||||||
|
const container = inject(VUE_CONTAINER_KEY) as Container | undefined | null
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
throw new Error("Container not found, did you forget to install the dioc plugin?")
|
||||||
|
}
|
||||||
|
|
||||||
|
return container.bind(service)
|
||||||
|
}
|
||||||
54
packages/dioc/package.json
Normal file
54
packages/dioc/package.json
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"name": "dioc",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"index.d.ts"
|
||||||
|
],
|
||||||
|
"main": "./dist/counter.umd.cjs",
|
||||||
|
"module": "./dist/counter.js",
|
||||||
|
"types": "./index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/main.d.ts",
|
||||||
|
"require": "./dist/index.cjs",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./vue": {
|
||||||
|
"types": "./dist/vue.d.ts",
|
||||||
|
"require": "./dist/vue.cjs",
|
||||||
|
"import": "./dist/vue.js"
|
||||||
|
},
|
||||||
|
"./testing": {
|
||||||
|
"types": "./dist/testing.d.ts",
|
||||||
|
"require": "./dist/testing.cjs",
|
||||||
|
"import": "./dist/testing.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build && tsc --emitDeclarationOnly",
|
||||||
|
"prepare": "pnpm run build",
|
||||||
|
"test": "vitest run",
|
||||||
|
"do-test": "pnpm run test",
|
||||||
|
"test:watch": "vitest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^4.9.4",
|
||||||
|
"vite": "^4.0.4",
|
||||||
|
"vitest": "^0.29.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"rxjs": "^7.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.2.25"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"vue": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
262
packages/dioc/test/container.spec.ts
Normal file
262
packages/dioc/test/container.spec.ts
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
import { it, expect, describe, vi } from "vitest"
|
||||||
|
import { Service } from "../lib/service"
|
||||||
|
import { Container, currentContainer, ContainerEvent } from "../lib/container"
|
||||||
|
|
||||||
|
class TestServiceA extends Service {
|
||||||
|
public static ID = "TestServiceA"
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestServiceB extends Service {
|
||||||
|
public static ID = "TestServiceB"
|
||||||
|
|
||||||
|
// Marked public to allow for testing
|
||||||
|
public readonly serviceA = this.bind(TestServiceA)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Container", () => {
|
||||||
|
describe("getBoundServiceWithID", () => {
|
||||||
|
it("returns the service instance if it is bound to the container", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const service = container.bind(TestServiceA)
|
||||||
|
|
||||||
|
expect(container.getBoundServiceWithID(TestServiceA.ID)).toBe(service)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns undefined if the service is not bound to the container", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
expect(container.getBoundServiceWithID(TestServiceA.ID)).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("bind", () => {
|
||||||
|
it("correctly binds the service to it", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const service = container.bind(TestServiceA)
|
||||||
|
|
||||||
|
// @ts-expect-error getContainer is defined as a protected property, but we are leveraging it here to check
|
||||||
|
expect(service.getContainer()).toBe(container)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("after bind, the current container is set back to its previous value", () => {
|
||||||
|
const originalValue = currentContainer
|
||||||
|
|
||||||
|
const container = new Container()
|
||||||
|
container.bind(TestServiceA)
|
||||||
|
|
||||||
|
expect(currentContainer).toBe(originalValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("dependent services are registered in the same container", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceB = container.bind(TestServiceB)
|
||||||
|
|
||||||
|
// @ts-expect-error getContainer is defined as a protected property, but we are leveraging it here to check
|
||||||
|
expect(serviceB.serviceA.getContainer()).toBe(container)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("binding an already initialized service returns the initialized instance (services are singletons)", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceA = container.bind(TestServiceA)
|
||||||
|
const serviceA2 = container.bind(TestServiceA)
|
||||||
|
|
||||||
|
expect(serviceA).toBe(serviceA2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("binding a service which is a dependency of another service returns the same instance created from the dependency resolution (services are singletons)", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceB = container.bind(TestServiceB)
|
||||||
|
const serviceA = container.bind(TestServiceA)
|
||||||
|
|
||||||
|
expect(serviceB.serviceA).toBe(serviceA)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("binding an initialized service as a dependency returns the same instance", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceA = container.bind(TestServiceA)
|
||||||
|
const serviceB = container.bind(TestServiceB)
|
||||||
|
|
||||||
|
expect(serviceB.serviceA).toBe(serviceA)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("container emits an init event when an uninitialized service is initialized via bind and event only called once", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceFunc = vi.fn<
|
||||||
|
[ContainerEvent & { type: "SERVICE_INIT" }],
|
||||||
|
void
|
||||||
|
>()
|
||||||
|
|
||||||
|
container.getEventStream().subscribe((ev) => {
|
||||||
|
if (ev.type === "SERVICE_INIT") {
|
||||||
|
serviceFunc(ev)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const instance = container.bind(TestServiceA)
|
||||||
|
|
||||||
|
expect(serviceFunc).toHaveBeenCalledOnce()
|
||||||
|
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
|
||||||
|
type: "SERVICE_INIT",
|
||||||
|
serviceID: TestServiceA.ID,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("the bind event emitted has an undefined bounderID when the service is bound directly to the container", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceFunc = vi.fn<
|
||||||
|
[ContainerEvent & { type: "SERVICE_BIND" }],
|
||||||
|
void
|
||||||
|
>()
|
||||||
|
|
||||||
|
container.getEventStream().subscribe((ev) => {
|
||||||
|
if (ev.type === "SERVICE_BIND") {
|
||||||
|
serviceFunc(ev)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
container.bind(TestServiceA)
|
||||||
|
|
||||||
|
expect(serviceFunc).toHaveBeenCalledOnce()
|
||||||
|
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
|
||||||
|
type: "SERVICE_BIND",
|
||||||
|
boundeeID: TestServiceA.ID,
|
||||||
|
bounderID: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("the bind event emitted has the correct bounderID when the service is bound to another service", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceFunc = vi.fn<
|
||||||
|
[ContainerEvent & { type: "SERVICE_BIND" }],
|
||||||
|
void
|
||||||
|
>()
|
||||||
|
|
||||||
|
container.getEventStream().subscribe((ev) => {
|
||||||
|
// We only care about the bind event of TestServiceA
|
||||||
|
if (ev.type === "SERVICE_BIND" && ev.boundeeID === TestServiceA.ID) {
|
||||||
|
serviceFunc(ev)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
container.bind(TestServiceB)
|
||||||
|
|
||||||
|
expect(serviceFunc).toHaveBeenCalledOnce()
|
||||||
|
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
|
||||||
|
type: "SERVICE_BIND",
|
||||||
|
boundeeID: TestServiceA.ID,
|
||||||
|
bounderID: TestServiceB.ID,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("hasBound", () => {
|
||||||
|
it("returns true if the given service is bound to the container", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
container.bind(TestServiceA)
|
||||||
|
|
||||||
|
expect(container.hasBound(TestServiceA)).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns false if the given service is not bound to the container", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
expect(container.hasBound(TestServiceA)).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns true when the service is bound because it is a dependency of another service", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
container.bind(TestServiceB)
|
||||||
|
|
||||||
|
expect(container.hasBound(TestServiceA)).toEqual(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getEventStream", () => {
|
||||||
|
it("returns an observable which emits events correctly when services are initialized", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceFunc = vi.fn<
|
||||||
|
[ContainerEvent & { type: "SERVICE_INIT" }],
|
||||||
|
void
|
||||||
|
>()
|
||||||
|
|
||||||
|
container.getEventStream().subscribe((ev) => {
|
||||||
|
if (ev.type === "SERVICE_INIT") {
|
||||||
|
serviceFunc(ev)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
container.bind(TestServiceB)
|
||||||
|
|
||||||
|
expect(serviceFunc).toHaveBeenCalledTimes(2)
|
||||||
|
expect(serviceFunc).toHaveBeenNthCalledWith(1, <ContainerEvent>{
|
||||||
|
type: "SERVICE_INIT",
|
||||||
|
serviceID: TestServiceA.ID,
|
||||||
|
})
|
||||||
|
expect(serviceFunc).toHaveBeenNthCalledWith(2, <ContainerEvent>{
|
||||||
|
type: "SERVICE_INIT",
|
||||||
|
serviceID: TestServiceB.ID,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns an observable which emits events correctly when services are bound", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceFunc = vi.fn<
|
||||||
|
[ContainerEvent & { type: "SERVICE_BIND" }],
|
||||||
|
void
|
||||||
|
>()
|
||||||
|
|
||||||
|
container.getEventStream().subscribe((ev) => {
|
||||||
|
if (ev.type === "SERVICE_BIND") {
|
||||||
|
serviceFunc(ev)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
container.bind(TestServiceB)
|
||||||
|
|
||||||
|
expect(serviceFunc).toHaveBeenCalledTimes(2)
|
||||||
|
expect(serviceFunc).toHaveBeenNthCalledWith(1, <ContainerEvent>{
|
||||||
|
type: "SERVICE_BIND",
|
||||||
|
boundeeID: TestServiceA.ID,
|
||||||
|
bounderID: TestServiceB.ID,
|
||||||
|
})
|
||||||
|
expect(serviceFunc).toHaveBeenNthCalledWith(2, <ContainerEvent>{
|
||||||
|
type: "SERVICE_BIND",
|
||||||
|
boundeeID: TestServiceB.ID,
|
||||||
|
bounderID: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getBoundServices", () => {
|
||||||
|
it("returns an iterator over all services bound to the container in the format [service id, service instance]", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const instanceB = container.bind(TestServiceB)
|
||||||
|
const instanceA = instanceB.serviceA
|
||||||
|
|
||||||
|
expect(Array.from(container.getBoundServices())).toEqual([
|
||||||
|
[TestServiceA.ID, instanceA],
|
||||||
|
[TestServiceB.ID, instanceB],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns an empty iterator if no services are bound", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
expect(Array.from(container.getBoundServices())).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
66
packages/dioc/test/service.spec.ts
Normal file
66
packages/dioc/test/service.spec.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest"
|
||||||
|
import { Service, Container } from "../lib/main"
|
||||||
|
|
||||||
|
class TestServiceA extends Service {
|
||||||
|
public static ID = "TestServiceA"
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestServiceB extends Service<"test"> {
|
||||||
|
public static ID = "TestServiceB"
|
||||||
|
|
||||||
|
// Marked public to allow for testing
|
||||||
|
public readonly serviceA = this.bind(TestServiceA)
|
||||||
|
|
||||||
|
public emitTestEvent() {
|
||||||
|
this.emit("test")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Service", () => {
|
||||||
|
describe("constructor", () => {
|
||||||
|
it("throws an error if the service is initialized without a container", () => {
|
||||||
|
expect(() => new TestServiceA()).toThrowError(
|
||||||
|
"Tried to initialize service with no container (ID: TestServiceA)"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("bind", () => {
|
||||||
|
it("correctly binds the dependency service using the container", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceA = container.bind(TestServiceA)
|
||||||
|
|
||||||
|
const serviceB = container.bind(TestServiceB)
|
||||||
|
expect(serviceB.serviceA).toBe(serviceA)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getContainer", () => {
|
||||||
|
it("returns the container the service is bound to", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceA = container.bind(TestServiceA)
|
||||||
|
|
||||||
|
// @ts-expect-error getContainer is a protected member, we are just using it to help with testing
|
||||||
|
expect(serviceA.getContainer()).toBe(container)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getEventStream", () => {
|
||||||
|
it("returns the valid event stream of the service", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceB = container.bind(TestServiceB)
|
||||||
|
|
||||||
|
const serviceFunc = vi.fn()
|
||||||
|
|
||||||
|
serviceB.getEventStream().subscribe(serviceFunc)
|
||||||
|
|
||||||
|
serviceB.emitTestEvent()
|
||||||
|
|
||||||
|
expect(serviceFunc).toHaveBeenCalledOnce()
|
||||||
|
expect(serviceFunc).toHaveBeenCalledWith("test")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
92
packages/dioc/test/test-container.spec.ts
Normal file
92
packages/dioc/test/test-container.spec.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest"
|
||||||
|
import { TestContainer } from "../lib/testing"
|
||||||
|
import { Service } from "../lib/service"
|
||||||
|
import { ContainerEvent } from "../lib/container"
|
||||||
|
|
||||||
|
class TestServiceA extends Service {
|
||||||
|
public static ID = "TestServiceA"
|
||||||
|
|
||||||
|
public test() {
|
||||||
|
return "real"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestServiceB extends Service {
|
||||||
|
public static ID = "TestServiceB"
|
||||||
|
|
||||||
|
// declared public to help with testing
|
||||||
|
public readonly serviceA = this.bind(TestServiceA)
|
||||||
|
|
||||||
|
public test() {
|
||||||
|
return this.serviceA.test()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("TestContainer", () => {
|
||||||
|
describe("bindMock", () => {
|
||||||
|
it("returns the fake service defined", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const fakeService = {
|
||||||
|
test: () => "fake",
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = container.bindMock(TestServiceA, fakeService)
|
||||||
|
|
||||||
|
expect(result).toBe(fakeService)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("new services bound to the container get the mock service", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const fakeServiceA = {
|
||||||
|
test: () => "fake",
|
||||||
|
}
|
||||||
|
|
||||||
|
container.bindMock(TestServiceA, fakeServiceA)
|
||||||
|
|
||||||
|
const serviceB = container.bind(TestServiceB)
|
||||||
|
|
||||||
|
expect(serviceB.serviceA).toBe(fakeServiceA)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("container emits SERVICE_BIND event", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const fakeServiceA = {
|
||||||
|
test: () => "fake",
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceFunc = vi.fn<[ContainerEvent, void]>()
|
||||||
|
|
||||||
|
container.getEventStream().subscribe((ev) => {
|
||||||
|
serviceFunc(ev)
|
||||||
|
})
|
||||||
|
|
||||||
|
container.bindMock(TestServiceA, fakeServiceA)
|
||||||
|
|
||||||
|
expect(serviceFunc).toHaveBeenCalledOnce()
|
||||||
|
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
|
||||||
|
type: "SERVICE_BIND",
|
||||||
|
boundeeID: TestServiceA.ID,
|
||||||
|
bounderID: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws if service already bound", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const fakeServiceA = {
|
||||||
|
test: () => "fake",
|
||||||
|
}
|
||||||
|
|
||||||
|
container.bindMock(TestServiceA, fakeServiceA)
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
container.bindMock(TestServiceA, fakeServiceA)
|
||||||
|
}).toThrowError(
|
||||||
|
"Service 'TestServiceA' already bound to container. Did you already call bindMock on this ?"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
2
packages/dioc/testing.d.ts
vendored
Normal file
2
packages/dioc/testing.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from "./dist/testing.d.ts"
|
||||||
|
export * from "./dist/testing.d.ts"
|
||||||
21
packages/dioc/tsconfig.json
Normal file
21
packages/dioc/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["lib"]
|
||||||
|
}
|
||||||
16
packages/dioc/vite.config.ts
Normal file
16
packages/dioc/vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: {
|
||||||
|
index: './lib/main.ts',
|
||||||
|
vue: './lib/vue.ts',
|
||||||
|
testing: './lib/testing.ts',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['vue'],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
7
packages/dioc/vitest.config.ts
Normal file
7
packages/dioc/vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from "vitest/config"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
2
packages/dioc/vue.d.ts
vendored
Normal file
2
packages/dioc/vue.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from "./dist/vue.d.ts"
|
||||||
|
export * from "./dist/vue.d.ts"
|
||||||
@@ -28,7 +28,6 @@
|
|||||||
"@nestjs-modules/mailer": "^1.9.1",
|
"@nestjs-modules/mailer": "^1.9.1",
|
||||||
"@nestjs/apollo": "^12.0.9",
|
"@nestjs/apollo": "^12.0.9",
|
||||||
"@nestjs/common": "^10.2.6",
|
"@nestjs/common": "^10.2.6",
|
||||||
"@nestjs/config": "^3.1.1",
|
|
||||||
"@nestjs/core": "^10.2.6",
|
"@nestjs/core": "^10.2.6",
|
||||||
"@nestjs/graphql": "^12.0.9",
|
"@nestjs/graphql": "^12.0.9",
|
||||||
"@nestjs/jwt": "^10.1.1",
|
"@nestjs/jwt": "^10.1.1",
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
-- CreateTable
|
|
||||||
CREATE TABLE "InfraConfig" (
|
|
||||||
"id" TEXT NOT NULL,
|
|
||||||
"name" TEXT NOT NULL,
|
|
||||||
"value" TEXT,
|
|
||||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
|
||||||
"createdOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedOn" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "InfraConfig_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "InfraConfig_name_key" ON "InfraConfig"("name");
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "TeamCollection" ADD COLUMN "data" JSONB;
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "UserCollection" ADD COLUMN "data" JSONB;
|
|
||||||
@@ -43,7 +43,6 @@ model TeamInvitation {
|
|||||||
model TeamCollection {
|
model TeamCollection {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
parentID String?
|
parentID String?
|
||||||
data Json?
|
|
||||||
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
|
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
|
||||||
children TeamCollection[] @relation("TeamCollectionChildParent")
|
children TeamCollection[] @relation("TeamCollectionChildParent")
|
||||||
requests TeamRequest[]
|
requests TeamRequest[]
|
||||||
@@ -75,8 +74,7 @@ model Shortcode {
|
|||||||
creatorUid String?
|
creatorUid String?
|
||||||
User User? @relation(fields: [creatorUid], references: [uid])
|
User User? @relation(fields: [creatorUid], references: [uid])
|
||||||
createdOn DateTime @default(now())
|
createdOn DateTime @default(now())
|
||||||
updatedOn DateTime @default(now()) @updatedAt
|
updatedOn DateTime @updatedAt @default(now())
|
||||||
|
|
||||||
@@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique")
|
@@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +195,6 @@ model UserCollection {
|
|||||||
userUid String
|
userUid String
|
||||||
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
|
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
|
||||||
title String
|
title String
|
||||||
data Json?
|
|
||||||
orderIndex Int
|
orderIndex Int
|
||||||
type ReqType
|
type ReqType
|
||||||
createdOn DateTime @default(now()) @db.Timestamp(3)
|
createdOn DateTime @default(now()) @db.Timestamp(3)
|
||||||
@@ -209,12 +206,3 @@ enum TeamMemberRole {
|
|||||||
VIEWER
|
VIEWER
|
||||||
EDITOR
|
EDITOR
|
||||||
}
|
}
|
||||||
|
|
||||||
model InfraConfig {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
name String @unique
|
|
||||||
value String?
|
|
||||||
active Boolean @default(true) // Use case: Let's say, Admin wants to disable Google SSO, but doesn't want to delete the config
|
|
||||||
createdOn DateTime @default(now()) @db.Timestamp(3)
|
|
||||||
updatedOn DateTime @updatedAt @db.Timestamp(3)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { AdminService } from './admin.service';
|
|||||||
import { PrismaModule } from '../prisma/prisma.module';
|
import { PrismaModule } from '../prisma/prisma.module';
|
||||||
import { PubSubModule } from '../pubsub/pubsub.module';
|
import { PubSubModule } from '../pubsub/pubsub.module';
|
||||||
import { UserModule } from '../user/user.module';
|
import { UserModule } from '../user/user.module';
|
||||||
|
import { MailerModule } from '../mailer/mailer.module';
|
||||||
import { TeamModule } from '../team/team.module';
|
import { TeamModule } from '../team/team.module';
|
||||||
import { TeamInvitationModule } from '../team-invitation/team-invitation.module';
|
import { TeamInvitationModule } from '../team-invitation/team-invitation.module';
|
||||||
import { TeamEnvironmentsModule } from '../team-environments/team-environments.module';
|
import { TeamEnvironmentsModule } from '../team-environments/team-environments.module';
|
||||||
@@ -11,20 +12,19 @@ import { TeamCollectionModule } from '../team-collection/team-collection.module'
|
|||||||
import { TeamRequestModule } from '../team-request/team-request.module';
|
import { TeamRequestModule } from '../team-request/team-request.module';
|
||||||
import { InfraResolver } from './infra.resolver';
|
import { InfraResolver } from './infra.resolver';
|
||||||
import { ShortcodeModule } from 'src/shortcode/shortcode.module';
|
import { ShortcodeModule } from 'src/shortcode/shortcode.module';
|
||||||
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
PubSubModule,
|
PubSubModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
|
MailerModule,
|
||||||
TeamModule,
|
TeamModule,
|
||||||
TeamInvitationModule,
|
TeamInvitationModule,
|
||||||
TeamEnvironmentsModule,
|
TeamEnvironmentsModule,
|
||||||
TeamCollectionModule,
|
TeamCollectionModule,
|
||||||
TeamRequestModule,
|
TeamRequestModule,
|
||||||
ShortcodeModule,
|
ShortcodeModule,
|
||||||
InfraConfigModule,
|
|
||||||
],
|
],
|
||||||
providers: [InfraResolver, AdminResolver, AdminService],
|
providers: [InfraResolver, AdminResolver, AdminService],
|
||||||
exports: [AdminService],
|
exports: [AdminService],
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
USER_ALREADY_INVITED,
|
USER_ALREADY_INVITED,
|
||||||
} from '../errors';
|
} from '../errors';
|
||||||
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockPubSub = mockDeep<PubSubService>();
|
const mockPubSub = mockDeep<PubSubService>();
|
||||||
@@ -28,7 +27,6 @@ const mockTeamInvitationService = mockDeep<TeamInvitationService>();
|
|||||||
const mockTeamCollectionService = mockDeep<TeamCollectionService>();
|
const mockTeamCollectionService = mockDeep<TeamCollectionService>();
|
||||||
const mockMailerService = mockDeep<MailerService>();
|
const mockMailerService = mockDeep<MailerService>();
|
||||||
const mockShortcodeService = mockDeep<ShortcodeService>();
|
const mockShortcodeService = mockDeep<ShortcodeService>();
|
||||||
const mockConfigService = mockDeep<ConfigService>();
|
|
||||||
|
|
||||||
const adminService = new AdminService(
|
const adminService = new AdminService(
|
||||||
mockUserService,
|
mockUserService,
|
||||||
@@ -41,7 +39,6 @@ const adminService = new AdminService(
|
|||||||
mockPrisma as any,
|
mockPrisma as any,
|
||||||
mockMailerService,
|
mockMailerService,
|
||||||
mockShortcodeService,
|
mockShortcodeService,
|
||||||
mockConfigService,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const invitedUsers: InvitedUsers[] = [
|
const invitedUsers: InvitedUsers[] = [
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import { TeamEnvironmentsService } from '../team-environments/team-environments.
|
|||||||
import { TeamInvitationService } from '../team-invitation/team-invitation.service';
|
import { TeamInvitationService } from '../team-invitation/team-invitation.service';
|
||||||
import { TeamMemberRole } from '../team/team.model';
|
import { TeamMemberRole } from '../team/team.model';
|
||||||
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminService {
|
export class AdminService {
|
||||||
@@ -40,7 +39,6 @@ export class AdminService {
|
|||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly mailerService: MailerService,
|
private readonly mailerService: MailerService,
|
||||||
private readonly shortcodeService: ShortcodeService,
|
private readonly shortcodeService: ShortcodeService,
|
||||||
private readonly configService: ConfigService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,7 +79,7 @@ export class AdminService {
|
|||||||
template: 'user-invitation',
|
template: 'user-invitation',
|
||||||
variables: {
|
variables: {
|
||||||
inviteeEmail: inviteeEmail,
|
inviteeEmail: inviteeEmail,
|
||||||
magicLink: `${this.configService.get('VITE_BASE_URL')}`,
|
magicLink: `${process.env.VITE_BASE_URL}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
import { UseGuards } from '@nestjs/common';
|
import { UseGuards } from '@nestjs/common';
|
||||||
import {
|
import { Args, ID, Query, ResolveField, Resolver } from '@nestjs/graphql';
|
||||||
Args,
|
|
||||||
ID,
|
|
||||||
Mutation,
|
|
||||||
Query,
|
|
||||||
ResolveField,
|
|
||||||
Resolver,
|
|
||||||
} from '@nestjs/graphql';
|
|
||||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||||
import { Infra } from './infra.model';
|
import { Infra } from './infra.model';
|
||||||
import { AdminService } from './admin.service';
|
import { AdminService } from './admin.service';
|
||||||
@@ -23,21 +16,11 @@ import { Team } from 'src/team/team.model';
|
|||||||
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
|
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
|
||||||
import { GqlAdmin } from './decorators/gql-admin.decorator';
|
import { GqlAdmin } from './decorators/gql-admin.decorator';
|
||||||
import { ShortcodeWithUserEmail } from 'src/shortcode/shortcode.model';
|
import { ShortcodeWithUserEmail } from 'src/shortcode/shortcode.model';
|
||||||
import { InfraConfig } from 'src/infra-config/infra-config.model';
|
|
||||||
import { InfraConfigService } from 'src/infra-config/infra-config.service';
|
|
||||||
import {
|
|
||||||
EnableAndDisableSSOArgs,
|
|
||||||
InfraConfigArgs,
|
|
||||||
} from 'src/infra-config/input-args';
|
|
||||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
|
||||||
|
|
||||||
@UseGuards(GqlThrottlerGuard)
|
@UseGuards(GqlThrottlerGuard)
|
||||||
@Resolver(() => Infra)
|
@Resolver(() => Infra)
|
||||||
export class InfraResolver {
|
export class InfraResolver {
|
||||||
constructor(
|
constructor(private adminService: AdminService) {}
|
||||||
private adminService: AdminService,
|
|
||||||
private infraConfigService: InfraConfigService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Query(() => Infra, {
|
@Query(() => Infra, {
|
||||||
description: 'Fetch details of the Infrastructure',
|
description: 'Fetch details of the Infrastructure',
|
||||||
@@ -239,76 +222,4 @@ export class InfraResolver {
|
|||||||
userEmail,
|
userEmail,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query(() => [InfraConfig], {
|
|
||||||
description: 'Retrieve configuration details for the instance',
|
|
||||||
})
|
|
||||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
|
||||||
async infraConfigs(
|
|
||||||
@Args({
|
|
||||||
name: 'configNames',
|
|
||||||
type: () => [InfraConfigEnumForClient],
|
|
||||||
description: 'Configs to fetch',
|
|
||||||
})
|
|
||||||
names: InfraConfigEnumForClient[],
|
|
||||||
) {
|
|
||||||
const infraConfigs = await this.infraConfigService.getMany(names);
|
|
||||||
if (E.isLeft(infraConfigs)) throwErr(infraConfigs.left);
|
|
||||||
return infraConfigs.right;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Query(() => [String], {
|
|
||||||
description: 'Allowed Auth Provider list',
|
|
||||||
})
|
|
||||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
|
||||||
allowedAuthProviders() {
|
|
||||||
return this.infraConfigService.getAllowedAuthProviders();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mutations */
|
|
||||||
|
|
||||||
@Mutation(() => [InfraConfig], {
|
|
||||||
description: 'Update Infra Configs',
|
|
||||||
})
|
|
||||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
|
||||||
async updateInfraConfigs(
|
|
||||||
@Args({
|
|
||||||
name: 'infraConfigs',
|
|
||||||
type: () => [InfraConfigArgs],
|
|
||||||
description: 'InfraConfigs to update',
|
|
||||||
})
|
|
||||||
infraConfigs: InfraConfigArgs[],
|
|
||||||
) {
|
|
||||||
const updatedRes = await this.infraConfigService.updateMany(infraConfigs);
|
|
||||||
if (E.isLeft(updatedRes)) throwErr(updatedRes.left);
|
|
||||||
return updatedRes.right;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
|
||||||
description: 'Reset Infra Configs with default values (.env)',
|
|
||||||
})
|
|
||||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
|
||||||
async resetInfraConfigs() {
|
|
||||||
const resetRes = await this.infraConfigService.reset();
|
|
||||||
if (E.isLeft(resetRes)) throwErr(resetRes.left);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
|
||||||
description: 'Enable or Disable SSO for login/signup',
|
|
||||||
})
|
|
||||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
|
||||||
async enableAndDisableSSO(
|
|
||||||
@Args({
|
|
||||||
name: 'providerInfo',
|
|
||||||
type: () => [EnableAndDisableSSOArgs],
|
|
||||||
description: 'SSO provider and status',
|
|
||||||
})
|
|
||||||
providerInfo: EnableAndDisableSSOArgs[],
|
|
||||||
) {
|
|
||||||
const isUpdated = await this.infraConfigService.enableAndDisableSSO(providerInfo);
|
|
||||||
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,27 +20,14 @@ import { ShortcodeModule } from './shortcode/shortcode.module';
|
|||||||
import { COOKIES_NOT_FOUND } from './errors';
|
import { COOKIES_NOT_FOUND } from './errors';
|
||||||
import { ThrottlerModule } from '@nestjs/throttler';
|
import { ThrottlerModule } from '@nestjs/throttler';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
||||||
import { InfraConfigModule } from './infra-config/infra-config.module';
|
|
||||||
import { loadInfraConfiguration } from './infra-config/helper';
|
|
||||||
import { MailerModule } from './mailer/mailer.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({
|
GraphQLModule.forRoot<ApolloDriverConfig>({
|
||||||
isGlobal: true,
|
|
||||||
load: [async () => loadInfraConfiguration()],
|
|
||||||
}),
|
|
||||||
GraphQLModule.forRootAsync<ApolloDriverConfig>({
|
|
||||||
driver: ApolloDriver,
|
|
||||||
imports: [ConfigModule],
|
|
||||||
inject: [ConfigService],
|
|
||||||
useFactory: async (configService: ConfigService) => {
|
|
||||||
return {
|
|
||||||
buildSchemaOptions: {
|
buildSchemaOptions: {
|
||||||
numberScalarMode: 'integer',
|
numberScalarMode: 'integer',
|
||||||
},
|
},
|
||||||
playground: configService.get('PRODUCTION') !== 'true',
|
playground: process.env.PRODUCTION !== 'true',
|
||||||
autoSchemaFile: true,
|
autoSchemaFile: true,
|
||||||
installSubscriptionHandlers: true,
|
installSubscriptionHandlers: true,
|
||||||
subscriptions: {
|
subscriptions: {
|
||||||
@@ -51,6 +38,7 @@ import { MailerModule } from './mailer/mailer.module';
|
|||||||
const cookies = subscriptionContextCookieParser(
|
const cookies = subscriptionContextCookieParser(
|
||||||
websocket.upgradeReq.headers.cookie,
|
websocket.upgradeReq.headers.cookie,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
headers: { ...websocket?.upgradeReq?.headers, cookies },
|
headers: { ...websocket?.upgradeReq?.headers, cookies },
|
||||||
};
|
};
|
||||||
@@ -67,22 +55,16 @@ import { MailerModule } from './mailer/mailer.module';
|
|||||||
res,
|
res,
|
||||||
connection,
|
connection,
|
||||||
}),
|
}),
|
||||||
};
|
driver: ApolloDriver,
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
ThrottlerModule.forRootAsync({
|
ThrottlerModule.forRoot([
|
||||||
imports: [ConfigModule],
|
|
||||||
inject: [ConfigService],
|
|
||||||
useFactory: async (configService: ConfigService) => [
|
|
||||||
{
|
{
|
||||||
ttl: +configService.get('RATE_LIMIT_TTL'),
|
ttl: +process.env.RATE_LIMIT_TTL,
|
||||||
limit: +configService.get('RATE_LIMIT_MAX'),
|
limit: +process.env.RATE_LIMIT_MAX,
|
||||||
},
|
},
|
||||||
],
|
]),
|
||||||
}),
|
|
||||||
MailerModule.register(),
|
|
||||||
UserModule,
|
UserModule,
|
||||||
AuthModule.register(),
|
AuthModule,
|
||||||
AdminModule,
|
AdminModule,
|
||||||
UserSettingsModule,
|
UserSettingsModule,
|
||||||
UserEnvironmentsModule,
|
UserEnvironmentsModule,
|
||||||
@@ -95,7 +77,6 @@ import { MailerModule } from './mailer/mailer.module';
|
|||||||
TeamInvitationModule,
|
TeamInvitationModule,
|
||||||
UserCollectionModule,
|
UserCollectionModule,
|
||||||
ShortcodeModule,
|
ShortcodeModule,
|
||||||
InfraConfigModule,
|
|
||||||
],
|
],
|
||||||
providers: [GQLComplexityPlugin],
|
providers: [GQLComplexityPlugin],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
|
InternalServerErrorException,
|
||||||
Post,
|
Post,
|
||||||
Query,
|
Query,
|
||||||
Request,
|
Request,
|
||||||
@@ -30,21 +31,11 @@ import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
|
|||||||
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
||||||
import { SkipThrottle } from '@nestjs/throttler';
|
import { SkipThrottle } from '@nestjs/throttler';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@UseGuards(ThrottlerBehindProxyGuard)
|
@UseGuards(ThrottlerBehindProxyGuard)
|
||||||
@Controller({ path: 'auth', version: '1' })
|
@Controller({ path: 'auth', version: '1' })
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(
|
constructor(private authService: AuthService) {}
|
||||||
private authService: AuthService,
|
|
||||||
private configService: ConfigService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Get('providers')
|
|
||||||
async getAuthProviders() {
|
|
||||||
const providers = await this.authService.getAuthProviders();
|
|
||||||
return { providers };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
** Route to initiate magic-link auth for a users email
|
** Route to initiate magic-link auth for a users email
|
||||||
@@ -54,14 +45,8 @@ export class AuthController {
|
|||||||
@Body() authData: SignInMagicDto,
|
@Body() authData: SignInMagicDto,
|
||||||
@Query('origin') origin: string,
|
@Query('origin') origin: string,
|
||||||
) {
|
) {
|
||||||
if (
|
if (!authProviderCheck(AuthProvider.EMAIL))
|
||||||
!authProviderCheck(
|
|
||||||
AuthProvider.EMAIL,
|
|
||||||
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
||||||
}
|
|
||||||
|
|
||||||
const deviceIdToken = await this.authService.signInMagicLink(
|
const deviceIdToken = await this.authService.signInMagicLink(
|
||||||
authData.email,
|
authData.email,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
|||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { AuthController } from './auth.controller';
|
import { AuthController } from './auth.controller';
|
||||||
import { UserModule } from 'src/user/user.module';
|
import { UserModule } from 'src/user/user.module';
|
||||||
|
import { MailerModule } from 'src/mailer/mailer.module';
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||||
import { PassportModule } from '@nestjs/passport';
|
import { PassportModule } from '@nestjs/passport';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
@@ -11,47 +12,25 @@ import { GoogleStrategy } from './strategies/google.strategy';
|
|||||||
import { GithubStrategy } from './strategies/github.strategy';
|
import { GithubStrategy } from './strategies/github.strategy';
|
||||||
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
||||||
import { AuthProvider, authProviderCheck } from './helper';
|
import { AuthProvider, authProviderCheck } from './helper';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
||||||
import { loadInfraConfiguration } from 'src/infra-config/helper';
|
|
||||||
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
|
MailerModule,
|
||||||
PassportModule,
|
PassportModule,
|
||||||
JwtModule.registerAsync({
|
JwtModule.register({
|
||||||
imports: [ConfigModule],
|
secret: process.env.JWT_SECRET,
|
||||||
inject: [ConfigService],
|
|
||||||
useFactory: async (configService: ConfigService) => ({
|
|
||||||
secret: configService.get('JWT_SECRET'),
|
|
||||||
}),
|
}),
|
||||||
}),
|
|
||||||
InfraConfigModule,
|
|
||||||
],
|
],
|
||||||
providers: [AuthService, JwtStrategy, RTJwtStrategy],
|
providers: [
|
||||||
|
AuthService,
|
||||||
|
JwtStrategy,
|
||||||
|
RTJwtStrategy,
|
||||||
|
...(authProviderCheck(AuthProvider.GOOGLE) ? [GoogleStrategy] : []),
|
||||||
|
...(authProviderCheck(AuthProvider.GITHUB) ? [GithubStrategy] : []),
|
||||||
|
...(authProviderCheck(AuthProvider.MICROSOFT) ? [MicrosoftStrategy] : []),
|
||||||
|
],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
})
|
})
|
||||||
export class AuthModule {
|
export class AuthModule {}
|
||||||
static async register() {
|
|
||||||
const env = await loadInfraConfiguration();
|
|
||||||
const allowedAuthProviders = env.INFRA.VITE_ALLOWED_AUTH_PROVIDERS;
|
|
||||||
|
|
||||||
const providers = [
|
|
||||||
...(authProviderCheck(AuthProvider.GOOGLE, allowedAuthProviders)
|
|
||||||
? [GoogleStrategy]
|
|
||||||
: []),
|
|
||||||
...(authProviderCheck(AuthProvider.GITHUB, allowedAuthProviders)
|
|
||||||
? [GithubStrategy]
|
|
||||||
: []),
|
|
||||||
...(authProviderCheck(AuthProvider.MICROSOFT, allowedAuthProviders)
|
|
||||||
? [MicrosoftStrategy]
|
|
||||||
: []),
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
|
||||||
module: AuthModule,
|
|
||||||
providers,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,26 +21,15 @@ import { VerifyMagicDto } from './dto/verify-magic.dto';
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import * as argon2 from 'argon2';
|
import * as argon2 from 'argon2';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { InfraConfigService } from 'src/infra-config/infra-config.service';
|
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockUser = mockDeep<UserService>();
|
const mockUser = mockDeep<UserService>();
|
||||||
const mockJWT = mockDeep<JwtService>();
|
const mockJWT = mockDeep<JwtService>();
|
||||||
const mockMailer = mockDeep<MailerService>();
|
const mockMailer = mockDeep<MailerService>();
|
||||||
const mockConfigService = mockDeep<ConfigService>();
|
|
||||||
const mockInfraConfigService = mockDeep<InfraConfigService>();
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const authService = new AuthService(
|
const authService = new AuthService(mockUser, mockPrisma, mockJWT, mockMailer);
|
||||||
mockUser,
|
|
||||||
mockPrisma,
|
|
||||||
mockJWT,
|
|
||||||
mockMailer,
|
|
||||||
mockConfigService,
|
|
||||||
mockInfraConfigService,
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentTime = new Date();
|
const currentTime = new Date();
|
||||||
|
|
||||||
@@ -102,8 +91,6 @@ describe('signInMagicLink', () => {
|
|||||||
mockUser.createUserViaMagicLink.mockResolvedValue(user);
|
mockUser.createUserViaMagicLink.mockResolvedValue(user);
|
||||||
// create new entry in VerificationToken table
|
// create new entry in VerificationToken table
|
||||||
mockPrisma.verificationToken.create.mockResolvedValueOnce(passwordlessData);
|
mockPrisma.verificationToken.create.mockResolvedValueOnce(passwordlessData);
|
||||||
// Read env variable 'MAGIC_LINK_TOKEN_VALIDITY' from config service
|
|
||||||
mockConfigService.get.mockReturnValue('3');
|
|
||||||
|
|
||||||
const result = await authService.signInMagicLink(
|
const result = await authService.signInMagicLink(
|
||||||
'dwight@dundermifflin.com',
|
'dwight@dundermifflin.com',
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ import { AuthError } from 'src/types/AuthError';
|
|||||||
import { AuthUser, IsAdmin } from 'src/types/AuthUser';
|
import { AuthUser, IsAdmin } from 'src/types/AuthUser';
|
||||||
import { VerificationToken } from '@prisma/client';
|
import { VerificationToken } from '@prisma/client';
|
||||||
import { Origin } from './helper';
|
import { Origin } from './helper';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { InfraConfigService } from 'src/infra-config/infra-config.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@@ -38,8 +36,6 @@ export class AuthService {
|
|||||||
private prismaService: PrismaService,
|
private prismaService: PrismaService,
|
||||||
private jwtService: JwtService,
|
private jwtService: JwtService,
|
||||||
private readonly mailerService: MailerService,
|
private readonly mailerService: MailerService,
|
||||||
private readonly configService: ConfigService,
|
|
||||||
private infraConfigService: InfraConfigService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,12 +46,10 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
private async generateMagicLinkTokens(user: AuthUser) {
|
private async generateMagicLinkTokens(user: AuthUser) {
|
||||||
const salt = await bcrypt.genSalt(
|
const salt = await bcrypt.genSalt(
|
||||||
parseInt(this.configService.get('TOKEN_SALT_COMPLEXITY')),
|
parseInt(process.env.TOKEN_SALT_COMPLEXITY),
|
||||||
);
|
);
|
||||||
const expiresOn = DateTime.now()
|
const expiresOn = DateTime.now()
|
||||||
.plus({
|
.plus({ hours: parseInt(process.env.MAGIC_LINK_TOKEN_VALIDITY) })
|
||||||
hours: parseInt(this.configService.get('MAGIC_LINK_TOKEN_VALIDITY')),
|
|
||||||
})
|
|
||||||
.toISO()
|
.toISO()
|
||||||
.toString();
|
.toString();
|
||||||
|
|
||||||
@@ -101,13 +95,13 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
private async generateRefreshToken(userUid: string) {
|
private async generateRefreshToken(userUid: string) {
|
||||||
const refreshTokenPayload: RefreshTokenPayload = {
|
const refreshTokenPayload: RefreshTokenPayload = {
|
||||||
iss: this.configService.get('VITE_BASE_URL'),
|
iss: process.env.VITE_BASE_URL,
|
||||||
sub: userUid,
|
sub: userUid,
|
||||||
aud: [this.configService.get('VITE_BASE_URL')],
|
aud: [process.env.VITE_BASE_URL],
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshToken = await this.jwtService.sign(refreshTokenPayload, {
|
const refreshToken = await this.jwtService.sign(refreshTokenPayload, {
|
||||||
expiresIn: this.configService.get('REFRESH_TOKEN_VALIDITY'), //7 Days
|
expiresIn: process.env.REFRESH_TOKEN_VALIDITY, //7 Days
|
||||||
});
|
});
|
||||||
|
|
||||||
const refreshTokenHash = await argon2.hash(refreshToken);
|
const refreshTokenHash = await argon2.hash(refreshToken);
|
||||||
@@ -133,9 +127,9 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
async generateAuthTokens(userUid: string) {
|
async generateAuthTokens(userUid: string) {
|
||||||
const accessTokenPayload: AccessTokenPayload = {
|
const accessTokenPayload: AccessTokenPayload = {
|
||||||
iss: this.configService.get('VITE_BASE_URL'),
|
iss: process.env.VITE_BASE_URL,
|
||||||
sub: userUid,
|
sub: userUid,
|
||||||
aud: [this.configService.get('VITE_BASE_URL')],
|
aud: [process.env.VITE_BASE_URL],
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshToken = await this.generateRefreshToken(userUid);
|
const refreshToken = await this.generateRefreshToken(userUid);
|
||||||
@@ -143,7 +137,7 @@ export class AuthService {
|
|||||||
|
|
||||||
return E.right(<AuthTokens>{
|
return E.right(<AuthTokens>{
|
||||||
access_token: await this.jwtService.sign(accessTokenPayload, {
|
access_token: await this.jwtService.sign(accessTokenPayload, {
|
||||||
expiresIn: this.configService.get('ACCESS_TOKEN_VALIDITY'), //1 Day
|
expiresIn: process.env.ACCESS_TOKEN_VALIDITY, //1 Day
|
||||||
}),
|
}),
|
||||||
refresh_token: refreshToken.right,
|
refresh_token: refreshToken.right,
|
||||||
});
|
});
|
||||||
@@ -224,14 +218,14 @@ export class AuthService {
|
|||||||
let url: string;
|
let url: string;
|
||||||
switch (origin) {
|
switch (origin) {
|
||||||
case Origin.ADMIN:
|
case Origin.ADMIN:
|
||||||
url = this.configService.get('VITE_ADMIN_URL');
|
url = process.env.VITE_ADMIN_URL;
|
||||||
break;
|
break;
|
||||||
case Origin.APP:
|
case Origin.APP:
|
||||||
url = this.configService.get('VITE_BASE_URL');
|
url = process.env.VITE_BASE_URL;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// if origin is invalid by default set URL to Hoppscotch-App
|
// if origin is invalid by default set URL to Hoppscotch-App
|
||||||
url = this.configService.get('VITE_BASE_URL');
|
url = process.env.VITE_BASE_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.mailerService.sendEmail(email, {
|
await this.mailerService.sendEmail(email, {
|
||||||
@@ -383,8 +377,4 @@ export class AuthService {
|
|||||||
|
|
||||||
return E.right(<IsAdmin>{ isAdmin: false });
|
return E.right(<IsAdmin>{ isAdmin: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthProviders() {
|
|
||||||
return this.infraConfigService.getAllowedAuthProviders();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,25 +3,14 @@ import { AuthGuard } from '@nestjs/passport';
|
|||||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
|
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
|
||||||
constructor(private readonly configService: ConfigService) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
canActivate(
|
canActivate(
|
||||||
context: ExecutionContext,
|
context: ExecutionContext,
|
||||||
): boolean | Promise<boolean> | Observable<boolean> {
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
if (
|
if (!authProviderCheck(AuthProvider.GITHUB))
|
||||||
!authProviderCheck(
|
|
||||||
AuthProvider.GITHUB,
|
|
||||||
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
||||||
}
|
|
||||||
|
|
||||||
return super.canActivate(context);
|
return super.canActivate(context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,25 +3,14 @@ import { AuthGuard } from '@nestjs/passport';
|
|||||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
|
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
|
||||||
constructor(private readonly configService: ConfigService) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
canActivate(
|
canActivate(
|
||||||
context: ExecutionContext,
|
context: ExecutionContext,
|
||||||
): boolean | Promise<boolean> | Observable<boolean> {
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
if (
|
if (!authProviderCheck(AuthProvider.GOOGLE))
|
||||||
!authProviderCheck(
|
|
||||||
AuthProvider.GOOGLE,
|
|
||||||
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
||||||
}
|
|
||||||
|
|
||||||
return super.canActivate(context);
|
return super.canActivate(context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,31 +3,20 @@ import { AuthGuard } from '@nestjs/passport';
|
|||||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MicrosoftSSOGuard
|
export class MicrosoftSSOGuard
|
||||||
extends AuthGuard('microsoft')
|
extends AuthGuard('microsoft')
|
||||||
implements CanActivate
|
implements CanActivate
|
||||||
{
|
{
|
||||||
constructor(private readonly configService: ConfigService) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
canActivate(
|
canActivate(
|
||||||
context: ExecutionContext,
|
context: ExecutionContext,
|
||||||
): boolean | Promise<boolean> | Observable<boolean> {
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
if (
|
if (!authProviderCheck(AuthProvider.MICROSOFT))
|
||||||
!authProviderCheck(
|
|
||||||
AuthProvider.MICROSOFT,
|
|
||||||
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throwHTTPErr({
|
throwHTTPErr({
|
||||||
message: AUTH_PROVIDER_NOT_SPECIFIED,
|
message: AUTH_PROVIDER_NOT_SPECIFIED,
|
||||||
statusCode: 404,
|
statusCode: 404,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return super.canActivate(context);
|
return super.canActivate(context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { Response } from 'express';
|
|||||||
import * as cookie from 'cookie';
|
import * as cookie from 'cookie';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED, COOKIES_NOT_FOUND } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED, COOKIES_NOT_FOUND } from 'src/errors';
|
||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
enum AuthTokenType {
|
enum AuthTokenType {
|
||||||
ACCESS_TOKEN = 'access_token',
|
ACCESS_TOKEN = 'access_token',
|
||||||
@@ -46,17 +45,15 @@ export const authCookieHandler = (
|
|||||||
redirect: boolean,
|
redirect: boolean,
|
||||||
redirectUrl: string | null,
|
redirectUrl: string | null,
|
||||||
) => {
|
) => {
|
||||||
const configService = new ConfigService();
|
|
||||||
|
|
||||||
const currentTime = DateTime.now();
|
const currentTime = DateTime.now();
|
||||||
const accessTokenValidity = currentTime
|
const accessTokenValidity = currentTime
|
||||||
.plus({
|
.plus({
|
||||||
milliseconds: parseInt(configService.get('ACCESS_TOKEN_VALIDITY')),
|
milliseconds: parseInt(process.env.ACCESS_TOKEN_VALIDITY),
|
||||||
})
|
})
|
||||||
.toMillis();
|
.toMillis();
|
||||||
const refreshTokenValidity = currentTime
|
const refreshTokenValidity = currentTime
|
||||||
.plus({
|
.plus({
|
||||||
milliseconds: parseInt(configService.get('REFRESH_TOKEN_VALIDITY')),
|
milliseconds: parseInt(process.env.REFRESH_TOKEN_VALIDITY),
|
||||||
})
|
})
|
||||||
.toMillis();
|
.toMillis();
|
||||||
|
|
||||||
@@ -78,12 +75,10 @@ export const authCookieHandler = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check to see if redirectUrl is a whitelisted url
|
// check to see if redirectUrl is a whitelisted url
|
||||||
const whitelistedOrigins = configService
|
const whitelistedOrigins = process.env.WHITELISTED_ORIGINS.split(',');
|
||||||
.get('WHITELISTED_ORIGINS')
|
|
||||||
.split(',');
|
|
||||||
if (!whitelistedOrigins.includes(redirectUrl))
|
if (!whitelistedOrigins.includes(redirectUrl))
|
||||||
// if it is not redirect by default to REDIRECT_URL
|
// if it is not redirect by default to REDIRECT_URL
|
||||||
redirectUrl = configService.get('REDIRECT_URL');
|
redirectUrl = process.env.REDIRECT_URL;
|
||||||
|
|
||||||
return res.status(HttpStatus.OK).redirect(redirectUrl);
|
return res.status(HttpStatus.OK).redirect(redirectUrl);
|
||||||
};
|
};
|
||||||
@@ -117,16 +112,13 @@ export const subscriptionContextCookieParser = (rawCookies: string) => {
|
|||||||
* @param provider Provider we want to check the presence of
|
* @param provider Provider we want to check the presence of
|
||||||
* @returns Boolean if provider specified is present or not
|
* @returns Boolean if provider specified is present or not
|
||||||
*/
|
*/
|
||||||
export function authProviderCheck(
|
export function authProviderCheck(provider: string) {
|
||||||
provider: string,
|
|
||||||
VITE_ALLOWED_AUTH_PROVIDERS: string,
|
|
||||||
) {
|
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
|
throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
|
||||||
}
|
}
|
||||||
|
|
||||||
const envVariables = VITE_ALLOWED_AUTH_PROVIDERS
|
const envVariables = process.env.VITE_ALLOWED_AUTH_PROVIDERS
|
||||||
? VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
|
? process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
|
||||||
provider.trim().toUpperCase(),
|
provider.trim().toUpperCase(),
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|||||||
@@ -5,20 +5,18 @@ import { AuthService } from '../auth.service';
|
|||||||
import { UserService } from 'src/user/user.service';
|
import { UserService } from 'src/user/user.service';
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GithubStrategy extends PassportStrategy(Strategy) {
|
export class GithubStrategy extends PassportStrategy(Strategy) {
|
||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private usersService: UserService,
|
private usersService: UserService,
|
||||||
private configService: ConfigService,
|
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
clientID: configService.get('INFRA.GITHUB_CLIENT_ID'),
|
clientID: process.env.GITHUB_CLIENT_ID,
|
||||||
clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'),
|
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
||||||
callbackURL: configService.get('GITHUB_CALLBACK_URL'),
|
callbackURL: process.env.GITHUB_CALLBACK_URL,
|
||||||
scope: [configService.get('GITHUB_SCOPE')],
|
scope: [process.env.GITHUB_SCOPE],
|
||||||
store: true,
|
store: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,18 @@ import { UserService } from 'src/user/user.service';
|
|||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import { AuthService } from '../auth.service';
|
import { AuthService } from '../auth.service';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GoogleStrategy extends PassportStrategy(Strategy) {
|
export class GoogleStrategy extends PassportStrategy(Strategy) {
|
||||||
constructor(
|
constructor(
|
||||||
private usersService: UserService,
|
private usersService: UserService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private configService: ConfigService,
|
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
clientID: configService.get('INFRA.GOOGLE_CLIENT_ID'),
|
clientID: process.env.GOOGLE_CLIENT_ID,
|
||||||
clientSecret: configService.get('INFRA.GOOGLE_CLIENT_SECRET'),
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||||
callbackURL: configService.get('GOOGLE_CALLBACK_URL'),
|
callbackURL: process.env.GOOGLE_CALLBACK_URL,
|
||||||
scope: configService.get('GOOGLE_SCOPE').split(','),
|
scope: process.env.GOOGLE_SCOPE.split(','),
|
||||||
passReqToCallback: true,
|
passReqToCallback: true,
|
||||||
store: true,
|
store: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,14 +15,10 @@ import {
|
|||||||
INVALID_ACCESS_TOKEN,
|
INVALID_ACCESS_TOKEN,
|
||||||
USER_NOT_FOUND,
|
USER_NOT_FOUND,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||||
constructor(
|
constructor(private usersService: UserService) {
|
||||||
private usersService: UserService,
|
|
||||||
private configService: ConfigService,
|
|
||||||
) {
|
|
||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromExtractors([
|
jwtFromRequest: ExtractJwt.fromExtractors([
|
||||||
(request: Request) => {
|
(request: Request) => {
|
||||||
@@ -33,7 +29,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
|||||||
return ATCookie;
|
return ATCookie;
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
secretOrKey: configService.get('JWT_SECRET'),
|
secretOrKey: process.env.JWT_SECRET,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,21 +5,19 @@ import { AuthService } from '../auth.service';
|
|||||||
import { UserService } from 'src/user/user.service';
|
import { UserService } from 'src/user/user.service';
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MicrosoftStrategy extends PassportStrategy(Strategy) {
|
export class MicrosoftStrategy extends PassportStrategy(Strategy) {
|
||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private usersService: UserService,
|
private usersService: UserService,
|
||||||
private configService: ConfigService,
|
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
clientID: configService.get('INFRA.MICROSOFT_CLIENT_ID'),
|
clientID: process.env.MICROSOFT_CLIENT_ID,
|
||||||
clientSecret: configService.get('INFRA.MICROSOFT_CLIENT_SECRET'),
|
clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
|
||||||
callbackURL: configService.get('MICROSOFT_CALLBACK_URL'),
|
callbackURL: process.env.MICROSOFT_CALLBACK_URL,
|
||||||
scope: [configService.get('MICROSOFT_SCOPE')],
|
scope: [process.env.MICROSOFT_SCOPE],
|
||||||
tenant: configService.get('MICROSOFT_TENANT'),
|
tenant: process.env.MICROSOFT_TENANT,
|
||||||
store: true,
|
store: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,14 +14,10 @@ import {
|
|||||||
USER_NOT_FOUND,
|
USER_NOT_FOUND,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
|
export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
|
||||||
constructor(
|
constructor(private usersService: UserService) {
|
||||||
private usersService: UserService,
|
|
||||||
private configService: ConfigService,
|
|
||||||
) {
|
|
||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromExtractors([
|
jwtFromRequest: ExtractJwt.fromExtractors([
|
||||||
(request: Request) => {
|
(request: Request) => {
|
||||||
@@ -32,7 +28,7 @@ export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
|
|||||||
return RTCookie;
|
return RTCookie;
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
secretOrKey: configService.get('JWT_SECRET'),
|
secretOrKey: process.env.JWT_SECRET,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -254,13 +254,6 @@ export const TEAM_COLL_INVALID_JSON = 'team_coll/invalid_json';
|
|||||||
*/
|
*/
|
||||||
export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const;
|
export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const;
|
||||||
|
|
||||||
/**
|
|
||||||
* The Team Collection data is not valid
|
|
||||||
* (TeamCollectionService)
|
|
||||||
*/
|
|
||||||
export const TEAM_COLL_DATA_INVALID =
|
|
||||||
'team_coll/team_coll_data_invalid' as const;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tried to perform an action on a request that doesn't accept their member role level
|
* Tried to perform an action on a request that doesn't accept their member role level
|
||||||
* (GqlRequestTeamMemberGuard)
|
* (GqlRequestTeamMemberGuard)
|
||||||
@@ -592,13 +585,6 @@ export const USER_COLL_REORDERING_FAILED =
|
|||||||
export const USER_COLL_SAME_NEXT_COLL =
|
export const USER_COLL_SAME_NEXT_COLL =
|
||||||
'user_coll/user_collection_and_next_user_collection_are_same' as const;
|
'user_coll/user_collection_and_next_user_collection_are_same' as const;
|
||||||
|
|
||||||
/**
|
|
||||||
* The User Collection data is not valid
|
|
||||||
* (UserCollectionService)
|
|
||||||
*/
|
|
||||||
export const USER_COLL_DATA_INVALID =
|
|
||||||
'user_coll/user_coll_data_invalid' as const;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The User Collection does not belong to the logged-in user
|
* The User Collection does not belong to the logged-in user
|
||||||
* (UserCollectionService)
|
* (UserCollectionService)
|
||||||
@@ -644,41 +630,3 @@ export const SHORTCODE_INVALID_PROPERTIES_JSON =
|
|||||||
*/
|
*/
|
||||||
export const SHORTCODE_PROPERTIES_NOT_FOUND =
|
export const SHORTCODE_PROPERTIES_NOT_FOUND =
|
||||||
'shortcode/properties_not_found' as const;
|
'shortcode/properties_not_found' as const;
|
||||||
|
|
||||||
/**
|
|
||||||
* Infra Config not found
|
|
||||||
* (InfraConfigService)
|
|
||||||
*/
|
|
||||||
export const INFRA_CONFIG_NOT_FOUND = 'infra_config/not_found' as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Infra Config update failed
|
|
||||||
* (InfraConfigService)
|
|
||||||
*/
|
|
||||||
export const INFRA_CONFIG_UPDATE_FAILED = 'infra_config/update_failed' as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Infra Config not listed for onModuleInit creation
|
|
||||||
* (InfraConfigService)
|
|
||||||
*/
|
|
||||||
export const INFRA_CONFIG_NOT_LISTED =
|
|
||||||
'infra_config/properly_not_listed' as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Infra Config reset failed
|
|
||||||
* (InfraConfigService)
|
|
||||||
*/
|
|
||||||
export const INFRA_CONFIG_RESET_FAILED = 'infra_config/reset_failed' as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Infra Config invalid input for Config variable
|
|
||||||
* (InfraConfigService)
|
|
||||||
*/
|
|
||||||
export const INFRA_CONFIG_INVALID_INPUT = 'infra_config/invalid_input' as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error message for when the database table does not exist
|
|
||||||
* (InfraConfigService)
|
|
||||||
*/
|
|
||||||
export const DATABASE_TABLE_NOT_EXIST =
|
|
||||||
'Database migration not performed. Please check the FAQ for assistance: https://docs.hoppscotch.io/support/faq';
|
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
|
||||||
|
|
||||||
export enum ServiceStatus {
|
|
||||||
ENABLE = 'ENABLE',
|
|
||||||
DISABLE = 'DISABLE',
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load environment variables from the database and set them in the process
|
|
||||||
*
|
|
||||||
* @Description Fetch the 'infra_config' table from the database and return it as an object
|
|
||||||
* (ConfigModule will set the environment variables in the process)
|
|
||||||
*/
|
|
||||||
export async function loadInfraConfiguration() {
|
|
||||||
try {
|
|
||||||
const prisma = new PrismaService();
|
|
||||||
|
|
||||||
const infraConfigs = await prisma.infraConfig.findMany();
|
|
||||||
|
|
||||||
let environmentObject: Record<string, any> = {};
|
|
||||||
infraConfigs.forEach((infraConfig) => {
|
|
||||||
environmentObject[infraConfig.name] = infraConfig.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return { INFRA: environmentObject };
|
|
||||||
} catch (error) {
|
|
||||||
// Prisma throw error if 'Can't reach at database server' OR 'Table does not exist'
|
|
||||||
// Reason for not throwing error is, we want successful build during 'postinstall' and generate dist files
|
|
||||||
return { INFRA: {} };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop the app after 5 seconds
|
|
||||||
* (Docker will re-start the app)
|
|
||||||
*/
|
|
||||||
export function stopApp() {
|
|
||||||
console.log('Stopping app in 5 seconds...');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('Stopping app now...');
|
|
||||||
process.kill(process.pid, 'SIGTERM');
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
|
||||||
import { AuthProvider } from 'src/auth/helper';
|
|
||||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
|
||||||
import { ServiceStatus } from './helper';
|
|
||||||
|
|
||||||
@ObjectType()
|
|
||||||
export class InfraConfig {
|
|
||||||
@Field({
|
|
||||||
description: 'Infra Config Name',
|
|
||||||
})
|
|
||||||
name: InfraConfigEnumForClient;
|
|
||||||
|
|
||||||
@Field({
|
|
||||||
description: 'Infra Config Value',
|
|
||||||
})
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
registerEnumType(InfraConfigEnumForClient, {
|
|
||||||
name: 'InfraConfigEnum',
|
|
||||||
});
|
|
||||||
|
|
||||||
registerEnumType(AuthProvider, {
|
|
||||||
name: 'AuthProvider',
|
|
||||||
});
|
|
||||||
|
|
||||||
registerEnumType(ServiceStatus, {
|
|
||||||
name: 'ServiceStatus',
|
|
||||||
});
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { InfraConfigService } from './infra-config.service';
|
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [PrismaModule],
|
|
||||||
providers: [InfraConfigService],
|
|
||||||
exports: [InfraConfigService],
|
|
||||||
})
|
|
||||||
export class InfraConfigModule {}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
|
||||||
import { InfraConfigService } from './infra-config.service';
|
|
||||||
import {
|
|
||||||
InfraConfigEnum,
|
|
||||||
InfraConfigEnumForClient,
|
|
||||||
} from 'src/types/InfraConfig';
|
|
||||||
import { INFRA_CONFIG_NOT_FOUND, INFRA_CONFIG_UPDATE_FAILED } from 'src/errors';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import * as helper from './helper';
|
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
|
||||||
const mockConfigService = mockDeep<ConfigService>();
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
const infraConfigService = new InfraConfigService(
|
|
||||||
mockPrisma,
|
|
||||||
mockConfigService,
|
|
||||||
);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockReset(mockPrisma);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('InfraConfigService', () => {
|
|
||||||
describe('update', () => {
|
|
||||||
it('should update the infra config', async () => {
|
|
||||||
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
|
||||||
const value = 'true';
|
|
||||||
|
|
||||||
mockPrisma.infraConfig.update.mockResolvedValueOnce({
|
|
||||||
id: '',
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
active: true,
|
|
||||||
createdOn: new Date(),
|
|
||||||
updatedOn: new Date(),
|
|
||||||
});
|
|
||||||
jest.spyOn(helper, 'stopApp').mockReturnValueOnce();
|
|
||||||
|
|
||||||
const result = await infraConfigService.update(name, value);
|
|
||||||
expect(result).toEqualRight({ name, value });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass correct params to prisma update', async () => {
|
|
||||||
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
|
||||||
const value = 'true';
|
|
||||||
|
|
||||||
jest.spyOn(helper, 'stopApp').mockReturnValueOnce();
|
|
||||||
|
|
||||||
await infraConfigService.update(name, value);
|
|
||||||
|
|
||||||
expect(mockPrisma.infraConfig.update).toHaveBeenCalledWith({
|
|
||||||
where: { name },
|
|
||||||
data: { value },
|
|
||||||
});
|
|
||||||
expect(mockPrisma.infraConfig.update).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error if the infra config update failed', async () => {
|
|
||||||
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
|
||||||
const value = 'true';
|
|
||||||
|
|
||||||
mockPrisma.infraConfig.update.mockRejectedValueOnce('null');
|
|
||||||
|
|
||||||
const result = await infraConfigService.update(name, value);
|
|
||||||
expect(result).toEqualLeft(INFRA_CONFIG_UPDATE_FAILED);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('get', () => {
|
|
||||||
it('should get the infra config', async () => {
|
|
||||||
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
|
|
||||||
const value = 'true';
|
|
||||||
|
|
||||||
mockPrisma.infraConfig.findUniqueOrThrow.mockResolvedValueOnce({
|
|
||||||
id: '',
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
active: true,
|
|
||||||
createdOn: new Date(),
|
|
||||||
updatedOn: new Date(),
|
|
||||||
});
|
|
||||||
const result = await infraConfigService.get(name);
|
|
||||||
expect(result).toEqualRight({ name, value });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass correct params to prisma findUnique', async () => {
|
|
||||||
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
|
|
||||||
|
|
||||||
await infraConfigService.get(name);
|
|
||||||
|
|
||||||
expect(mockPrisma.infraConfig.findUniqueOrThrow).toHaveBeenCalledWith({
|
|
||||||
where: { name },
|
|
||||||
});
|
|
||||||
expect(mockPrisma.infraConfig.findUniqueOrThrow).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error if the infra config does not exist', async () => {
|
|
||||||
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
|
|
||||||
|
|
||||||
mockPrisma.infraConfig.findUniqueOrThrow.mockRejectedValueOnce('null');
|
|
||||||
|
|
||||||
const result = await infraConfigService.get(name);
|
|
||||||
expect(result).toEqualLeft(INFRA_CONFIG_NOT_FOUND);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
|
||||||
import { InfraConfig } from './infra-config.model';
|
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
|
||||||
import { InfraConfig as DBInfraConfig } from '@prisma/client';
|
|
||||||
import * as E from 'fp-ts/Either';
|
|
||||||
import {
|
|
||||||
InfraConfigEnum,
|
|
||||||
InfraConfigEnumForClient,
|
|
||||||
} from 'src/types/InfraConfig';
|
|
||||||
import {
|
|
||||||
AUTH_PROVIDER_NOT_SPECIFIED,
|
|
||||||
DATABASE_TABLE_NOT_EXIST,
|
|
||||||
INFRA_CONFIG_INVALID_INPUT,
|
|
||||||
INFRA_CONFIG_NOT_FOUND,
|
|
||||||
INFRA_CONFIG_NOT_LISTED,
|
|
||||||
INFRA_CONFIG_RESET_FAILED,
|
|
||||||
INFRA_CONFIG_UPDATE_FAILED,
|
|
||||||
} from 'src/errors';
|
|
||||||
import { throwErr, validateEmail, validateSMTPUrl } from 'src/utils';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { ServiceStatus, stopApp } from './helper';
|
|
||||||
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class InfraConfigService implements OnModuleInit {
|
|
||||||
constructor(
|
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
private readonly configService: ConfigService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async onModuleInit() {
|
|
||||||
await this.initializeInfraConfigTable();
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaultInfraConfigs(): { name: InfraConfigEnum; value: string }[] {
|
|
||||||
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
|
|
||||||
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MAILER_SMTP_URL,
|
|
||||||
value: process.env.MAILER_SMTP_URL,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MAILER_ADDRESS_FROM,
|
|
||||||
value: process.env.MAILER_ADDRESS_FROM,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
|
|
||||||
value: process.env.GOOGLE_CLIENT_ID,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
|
||||||
value: process.env.GOOGLE_CLIENT_SECRET,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GITHUB_CLIENT_ID,
|
|
||||||
value: process.env.GITHUB_CLIENT_ID,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
|
||||||
value: process.env.GITHUB_CLIENT_SECRET,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
|
||||||
value: process.env.MICROSOFT_CLIENT_ID,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
|
||||||
value: process.env.MICROSOFT_CLIENT_SECRET,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
|
||||||
value: process.env.VITE_ALLOWED_AUTH_PROVIDERS.toLocaleUpperCase(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return infraConfigDefaultObjs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the 'infra_config' table with values from .env
|
|
||||||
* @description This function create rows 'infra_config' in very first time (only once)
|
|
||||||
*/
|
|
||||||
async initializeInfraConfigTable() {
|
|
||||||
try {
|
|
||||||
// Get all the 'names' of the properties to be saved in the 'infra_config' table
|
|
||||||
const enumValues = Object.values(InfraConfigEnum);
|
|
||||||
|
|
||||||
// Fetch the default values (value in .env) for configs to be saved in 'infra_config' table
|
|
||||||
const infraConfigDefaultObjs = this.getDefaultInfraConfigs();
|
|
||||||
|
|
||||||
// Check if all the 'names' are listed in the default values
|
|
||||||
if (enumValues.length !== infraConfigDefaultObjs.length) {
|
|
||||||
throw new Error(INFRA_CONFIG_NOT_LISTED);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eliminate the rows (from 'infraConfigDefaultObjs') that are already present in the database table
|
|
||||||
const dbInfraConfigs = await this.prisma.infraConfig.findMany();
|
|
||||||
const propsToInsert = infraConfigDefaultObjs.filter(
|
|
||||||
(p) => !dbInfraConfigs.find((e) => e.name === p.name),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (propsToInsert.length > 0) {
|
|
||||||
await this.prisma.infraConfig.createMany({ data: propsToInsert });
|
|
||||||
stopApp();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === 'P1001') {
|
|
||||||
// Prisma error code for 'Can't reach at database server'
|
|
||||||
// We're not throwing error here because we want to allow the app to run 'pnpm install'
|
|
||||||
} else if (error.code === 'P2021') {
|
|
||||||
// Prisma error code for 'Table does not exist'
|
|
||||||
throwErr(DATABASE_TABLE_NOT_EXIST);
|
|
||||||
} else {
|
|
||||||
throwErr(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Typecast a database InfraConfig to a InfraConfig model
|
|
||||||
* @param dbInfraConfig database InfraConfig
|
|
||||||
* @returns InfraConfig model
|
|
||||||
*/
|
|
||||||
cast(dbInfraConfig: DBInfraConfig) {
|
|
||||||
return <InfraConfig>{
|
|
||||||
name: dbInfraConfig.name,
|
|
||||||
value: dbInfraConfig.value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update InfraConfig by name
|
|
||||||
* @param name Name of the InfraConfig
|
|
||||||
* @param value Value of the InfraConfig
|
|
||||||
* @returns InfraConfig model
|
|
||||||
*/
|
|
||||||
async update(
|
|
||||||
name: InfraConfigEnumForClient | InfraConfigEnum,
|
|
||||||
value: string,
|
|
||||||
) {
|
|
||||||
const isValidate = this.validateEnvValues([{ name, value }]);
|
|
||||||
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const infraConfig = await this.prisma.infraConfig.update({
|
|
||||||
where: { name },
|
|
||||||
data: { value },
|
|
||||||
});
|
|
||||||
|
|
||||||
stopApp();
|
|
||||||
|
|
||||||
return E.right(this.cast(infraConfig));
|
|
||||||
} catch (e) {
|
|
||||||
return E.left(INFRA_CONFIG_UPDATE_FAILED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update InfraConfigs by name
|
|
||||||
* @param infraConfigs InfraConfigs to update
|
|
||||||
* @returns InfraConfig model
|
|
||||||
*/
|
|
||||||
async updateMany(infraConfigs: InfraConfigArgs[]) {
|
|
||||||
const isValidate = this.validateEnvValues(infraConfigs);
|
|
||||||
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.prisma.$transaction(async (tx) => {
|
|
||||||
for (let i = 0; i < infraConfigs.length; i++) {
|
|
||||||
await tx.infraConfig.update({
|
|
||||||
where: { name: infraConfigs[i].name },
|
|
||||||
data: { value: infraConfigs[i].value },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
stopApp();
|
|
||||||
|
|
||||||
return E.right(infraConfigs);
|
|
||||||
} catch (e) {
|
|
||||||
return E.left(INFRA_CONFIG_UPDATE_FAILED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable or Disable SSO for login/signup
|
|
||||||
* @param provider Auth Provider to enable or disable
|
|
||||||
* @param status Status to enable or disable
|
|
||||||
* @returns Either true or an error
|
|
||||||
*/
|
|
||||||
async enableAndDisableSSO(providerInfo: EnableAndDisableSSOArgs[]) {
|
|
||||||
const allowedAuthProviders = this.configService
|
|
||||||
.get<string>('INFRA.VITE_ALLOWED_AUTH_PROVIDERS')
|
|
||||||
.split(',');
|
|
||||||
|
|
||||||
let updatedAuthProviders = allowedAuthProviders;
|
|
||||||
|
|
||||||
providerInfo.forEach(({ provider, status }) => {
|
|
||||||
if (status === ServiceStatus.ENABLE) {
|
|
||||||
updatedAuthProviders.push(provider);
|
|
||||||
} else if (status === ServiceStatus.DISABLE) {
|
|
||||||
updatedAuthProviders = updatedAuthProviders.filter(
|
|
||||||
(p) => p !== provider,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
updatedAuthProviders = [...new Set(updatedAuthProviders)];
|
|
||||||
|
|
||||||
if (updatedAuthProviders.length === 0) {
|
|
||||||
return E.left(AUTH_PROVIDER_NOT_SPECIFIED);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUpdated = await this.update(
|
|
||||||
InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
|
||||||
updatedAuthProviders.join(','),
|
|
||||||
);
|
|
||||||
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
|
|
||||||
|
|
||||||
return E.right(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get InfraConfig by name
|
|
||||||
* @param name Name of the InfraConfig
|
|
||||||
* @returns InfraConfig model
|
|
||||||
*/
|
|
||||||
async get(name: InfraConfigEnumForClient) {
|
|
||||||
try {
|
|
||||||
const infraConfig = await this.prisma.infraConfig.findUniqueOrThrow({
|
|
||||||
where: { name },
|
|
||||||
});
|
|
||||||
|
|
||||||
return E.right(this.cast(infraConfig));
|
|
||||||
} catch (e) {
|
|
||||||
return E.left(INFRA_CONFIG_NOT_FOUND);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get InfraConfigs by names
|
|
||||||
* @param names Names of the InfraConfigs
|
|
||||||
* @returns InfraConfig model
|
|
||||||
*/
|
|
||||||
async getMany(names: InfraConfigEnumForClient[]) {
|
|
||||||
try {
|
|
||||||
const infraConfigs = await this.prisma.infraConfig.findMany({
|
|
||||||
where: { name: { in: names } },
|
|
||||||
});
|
|
||||||
|
|
||||||
return E.right(infraConfigs.map((p) => this.cast(p)));
|
|
||||||
} catch (e) {
|
|
||||||
return E.left(INFRA_CONFIG_NOT_FOUND);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get allowed auth providers for login/signup
|
|
||||||
* @returns string[]
|
|
||||||
*/
|
|
||||||
getAllowedAuthProviders() {
|
|
||||||
return this.configService
|
|
||||||
.get<string>('INFRA.VITE_ALLOWED_AUTH_PROVIDERS')
|
|
||||||
.split(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset all the InfraConfigs to their default values (from .env)
|
|
||||||
*/
|
|
||||||
async reset() {
|
|
||||||
try {
|
|
||||||
const infraConfigDefaultObjs = this.getDefaultInfraConfigs();
|
|
||||||
|
|
||||||
await this.prisma.infraConfig.deleteMany({
|
|
||||||
where: { name: { in: infraConfigDefaultObjs.map((p) => p.name) } },
|
|
||||||
});
|
|
||||||
await this.prisma.infraConfig.createMany({
|
|
||||||
data: infraConfigDefaultObjs,
|
|
||||||
});
|
|
||||||
|
|
||||||
stopApp();
|
|
||||||
|
|
||||||
return E.right(true);
|
|
||||||
} catch (e) {
|
|
||||||
return E.left(INFRA_CONFIG_RESET_FAILED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validateEnvValues(
|
|
||||||
infraConfigs: {
|
|
||||||
name: InfraConfigEnumForClient | InfraConfigEnum;
|
|
||||||
value: string;
|
|
||||||
}[],
|
|
||||||
) {
|
|
||||||
for (let i = 0; i < infraConfigs.length; i++) {
|
|
||||||
switch (infraConfigs[i].name) {
|
|
||||||
case InfraConfigEnumForClient.MAILER_SMTP_URL:
|
|
||||||
const isValidUrl = validateSMTPUrl(infraConfigs[i].value);
|
|
||||||
if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnumForClient.MAILER_ADDRESS_FROM:
|
|
||||||
const isValidEmail = validateEmail(infraConfigs[i].value);
|
|
||||||
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return E.right(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { Field, InputType } from '@nestjs/graphql';
|
|
||||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
|
||||||
import { ServiceStatus } from './helper';
|
|
||||||
import { AuthProvider } from 'src/auth/helper';
|
|
||||||
|
|
||||||
@InputType()
|
|
||||||
export class InfraConfigArgs {
|
|
||||||
@Field(() => InfraConfigEnumForClient, {
|
|
||||||
description: 'Infra Config Name',
|
|
||||||
})
|
|
||||||
name: InfraConfigEnumForClient;
|
|
||||||
|
|
||||||
@Field({
|
|
||||||
description: 'Infra Config Value',
|
|
||||||
})
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@InputType()
|
|
||||||
export class EnableAndDisableSSOArgs {
|
|
||||||
@Field(() => AuthProvider, {
|
|
||||||
description: 'Auth Provider',
|
|
||||||
})
|
|
||||||
provider: AuthProvider;
|
|
||||||
|
|
||||||
@Field(() => ServiceStatus, {
|
|
||||||
description: 'Auth Provider Status',
|
|
||||||
})
|
|
||||||
status: ServiceStatus;
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Global, Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { MailerModule as NestMailerModule } from '@nestjs-modules/mailer';
|
import { MailerModule as NestMailerModule } from '@nestjs-modules/mailer';
|
||||||
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
|
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
|
||||||
import { MailerService } from './mailer.service';
|
import { MailerService } from './mailer.service';
|
||||||
@@ -7,35 +7,16 @@ import {
|
|||||||
MAILER_FROM_ADDRESS_UNDEFINED,
|
MAILER_FROM_ADDRESS_UNDEFINED,
|
||||||
MAILER_SMTP_URL_UNDEFINED,
|
MAILER_SMTP_URL_UNDEFINED,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { loadInfraConfiguration } from 'src/infra-config/helper';
|
|
||||||
|
|
||||||
@Global()
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
|
||||||
providers: [MailerService],
|
|
||||||
exports: [MailerService],
|
|
||||||
})
|
|
||||||
export class MailerModule {
|
|
||||||
static async register() {
|
|
||||||
const env = await loadInfraConfiguration();
|
|
||||||
|
|
||||||
let mailerSmtpUrl = env.INFRA.MAILER_SMTP_URL;
|
|
||||||
let mailerAddressFrom = env.INFRA.MAILER_ADDRESS_FROM;
|
|
||||||
|
|
||||||
if (!env.INFRA.MAILER_SMTP_URL || !env.INFRA.MAILER_ADDRESS_FROM) {
|
|
||||||
const config = new ConfigService();
|
|
||||||
mailerSmtpUrl = config.get('MAILER_SMTP_URL');
|
|
||||||
mailerAddressFrom = config.get('MAILER_ADDRESS_FROM');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
module: MailerModule,
|
|
||||||
imports: [
|
imports: [
|
||||||
NestMailerModule.forRoot({
|
NestMailerModule.forRoot({
|
||||||
transport: mailerSmtpUrl ?? throwErr(MAILER_SMTP_URL_UNDEFINED),
|
transport:
|
||||||
|
process.env.MAILER_SMTP_URL ?? throwErr(MAILER_SMTP_URL_UNDEFINED),
|
||||||
defaults: {
|
defaults: {
|
||||||
from: mailerAddressFrom ?? throwErr(MAILER_FROM_ADDRESS_UNDEFINED),
|
from:
|
||||||
|
process.env.MAILER_ADDRESS_FROM ??
|
||||||
|
throwErr(MAILER_FROM_ADDRESS_UNDEFINED),
|
||||||
},
|
},
|
||||||
template: {
|
template: {
|
||||||
dir: __dirname + '/templates',
|
dir: __dirname + '/templates',
|
||||||
@@ -43,6 +24,7 @@ export class MailerModule {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
};
|
providers: [MailerService],
|
||||||
}
|
exports: [MailerService],
|
||||||
}
|
})
|
||||||
|
export class MailerModule {}
|
||||||
|
|||||||
@@ -6,23 +6,18 @@ import { VersioningType } from '@nestjs/common';
|
|||||||
import * as session from 'express-session';
|
import * as session from 'express-session';
|
||||||
import { emitGQLSchemaFile } from './gql-schema';
|
import { emitGQLSchemaFile } from './gql-schema';
|
||||||
import { checkEnvironmentAuthProvider } from './utils';
|
import { checkEnvironmentAuthProvider } from './utils';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
|
console.log(`Running in production: ${process.env.PRODUCTION}`);
|
||||||
|
console.log(`Port: ${process.env.PORT}`);
|
||||||
|
|
||||||
|
checkEnvironmentAuthProvider();
|
||||||
|
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
const configService = app.get(ConfigService);
|
|
||||||
|
|
||||||
console.log(`Running in production: ${configService.get('PRODUCTION')}`);
|
|
||||||
console.log(`Port: ${configService.get('PORT')}`);
|
|
||||||
|
|
||||||
checkEnvironmentAuthProvider(
|
|
||||||
configService.get('VITE_ALLOWED_AUTH_PROVIDERS'),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
session({
|
session({
|
||||||
secret: configService.get('SESSION_SECRET'),
|
secret: process.env.SESSION_SECRET,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -33,18 +28,18 @@ async function bootstrap() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (configService.get('PRODUCTION') === 'false') {
|
if (process.env.PRODUCTION === 'false') {
|
||||||
console.log('Enabling CORS with development settings');
|
console.log('Enabling CORS with development settings');
|
||||||
|
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: configService.get('WHITELISTED_ORIGINS').split(','),
|
origin: process.env.WHITELISTED_ORIGINS.split(','),
|
||||||
credentials: true,
|
credentials: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log('Enabling CORS with production settings');
|
console.log('Enabling CORS with production settings');
|
||||||
|
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: configService.get('WHITELISTED_ORIGINS').split(','),
|
origin: process.env.WHITELISTED_ORIGINS.split(','),
|
||||||
credentials: true,
|
credentials: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -52,13 +47,7 @@ async function bootstrap() {
|
|||||||
type: VersioningType.URI,
|
type: VersioningType.URI,
|
||||||
});
|
});
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
await app.listen(configService.get('PORT') || 3170);
|
await app.listen(process.env.PORT || 3170);
|
||||||
|
|
||||||
// Graceful shutdown
|
|
||||||
process.on('SIGTERM', async () => {
|
|
||||||
console.info('SIGTERM signal received');
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!process.env.GENERATE_GQL_SCHEMA) {
|
if (!process.env.GENERATE_GQL_SCHEMA) {
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import {
|
|||||||
} from 'src/team-request/team-request.model';
|
} from 'src/team-request/team-request.model';
|
||||||
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
|
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
|
||||||
import { InvitedUser } from '../admin/invited-user.model';
|
import { InvitedUser } from '../admin/invited-user.model';
|
||||||
|
import { UserCollection } from '@prisma/client';
|
||||||
import {
|
import {
|
||||||
UserCollection,
|
|
||||||
UserCollectionRemovedData,
|
UserCollectionRemovedData,
|
||||||
UserCollectionReorderData,
|
UserCollectionReorderData,
|
||||||
} from 'src/user-collection/user-collections.model';
|
} from 'src/user-collection/user-collections.model';
|
||||||
|
|||||||
@@ -504,24 +504,20 @@ describe('ShortcodeService', () => {
|
|||||||
);
|
);
|
||||||
expect(result).toEqual(<ShortcodeWithUserEmail[]>[
|
expect(result).toEqual(<ShortcodeWithUserEmail[]>[
|
||||||
{
|
{
|
||||||
id: shortcodesWithUserEmail[0].id,
|
id: shortcodes[0].id,
|
||||||
request: JSON.stringify(shortcodesWithUserEmail[0].request),
|
request: JSON.stringify(shortcodes[0].request),
|
||||||
properties: JSON.stringify(
|
properties: JSON.stringify(shortcodes[0].embedProperties),
|
||||||
shortcodesWithUserEmail[0].embedProperties,
|
createdOn: shortcodes[0].createdOn,
|
||||||
),
|
|
||||||
createdOn: shortcodesWithUserEmail[0].createdOn,
|
|
||||||
creator: {
|
creator: {
|
||||||
uid: user.uid,
|
uid: user.uid,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: shortcodesWithUserEmail[1].id,
|
id: shortcodes[1].id,
|
||||||
request: JSON.stringify(shortcodesWithUserEmail[1].request),
|
request: JSON.stringify(shortcodes[1].request),
|
||||||
properties: JSON.stringify(
|
properties: JSON.stringify(shortcodes[1].embedProperties),
|
||||||
shortcodesWithUserEmail[1].embedProperties,
|
createdOn: shortcodes[1].createdOn,
|
||||||
),
|
|
||||||
createdOn: shortcodesWithUserEmail[1].createdOn,
|
|
||||||
creator: {
|
creator: {
|
||||||
uid: user.uid,
|
uid: user.uid,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
|||||||
@@ -14,13 +14,6 @@ export class CreateRootTeamCollectionArgs {
|
|||||||
|
|
||||||
@Field({ name: 'title', description: 'Title of the new collection' })
|
@Field({ name: 'title', description: 'Title of the new collection' })
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
@Field({
|
|
||||||
name: 'data',
|
|
||||||
description: 'JSON string representing the collection data',
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
data: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
@@ -33,13 +26,6 @@ export class CreateChildTeamCollectionArgs {
|
|||||||
|
|
||||||
@Field({ name: 'childTitle', description: 'Title of the new collection' })
|
@Field({ name: 'childTitle', description: 'Title of the new collection' })
|
||||||
childTitle: string;
|
childTitle: string;
|
||||||
|
|
||||||
@Field({
|
|
||||||
name: 'data',
|
|
||||||
description: 'JSON string representing the collection data',
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
data: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
@@ -47,14 +33,12 @@ export class RenameTeamCollectionArgs {
|
|||||||
@Field(() => ID, {
|
@Field(() => ID, {
|
||||||
name: 'collectionID',
|
name: 'collectionID',
|
||||||
description: 'ID of the collection',
|
description: 'ID of the collection',
|
||||||
deprecationReason: 'Switch to updateTeamCollection mutation instead',
|
|
||||||
})
|
})
|
||||||
collectionID: string;
|
collectionID: string;
|
||||||
|
|
||||||
@Field({
|
@Field({
|
||||||
name: 'newTitle',
|
name: 'newTitle',
|
||||||
description: 'The updated title of the collection',
|
description: 'The updated title of the collection',
|
||||||
deprecationReason: 'Switch to updateTeamCollection mutation instead',
|
|
||||||
})
|
})
|
||||||
newTitle: string;
|
newTitle: string;
|
||||||
}
|
}
|
||||||
@@ -114,26 +98,3 @@ export class ReplaceTeamCollectionArgs {
|
|||||||
})
|
})
|
||||||
parentCollectionID?: string;
|
parentCollectionID?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ArgsType()
|
|
||||||
export class UpdateTeamCollectionArgs {
|
|
||||||
@Field(() => ID, {
|
|
||||||
name: 'collectionID',
|
|
||||||
description: 'ID of the collection',
|
|
||||||
})
|
|
||||||
collectionID: string;
|
|
||||||
|
|
||||||
@Field({
|
|
||||||
name: 'newTitle',
|
|
||||||
description: 'The updated title of the collection',
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
newTitle: string;
|
|
||||||
|
|
||||||
@Field({
|
|
||||||
name: 'data',
|
|
||||||
description: 'JSON string representing the collection data',
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
data: string;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,17 +12,12 @@ export class TeamCollection {
|
|||||||
})
|
})
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
@Field({
|
|
||||||
description: 'JSON string representing the collection data',
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
data: string;
|
|
||||||
|
|
||||||
@Field(() => ID, {
|
@Field(() => ID, {
|
||||||
description: 'ID of the collection',
|
description: 'ID of the collection',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
parentID: string;
|
parentID: string;
|
||||||
|
teamID: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
MoveTeamCollectionArgs,
|
MoveTeamCollectionArgs,
|
||||||
RenameTeamCollectionArgs,
|
RenameTeamCollectionArgs,
|
||||||
ReplaceTeamCollectionArgs,
|
ReplaceTeamCollectionArgs,
|
||||||
UpdateTeamCollectionArgs,
|
|
||||||
UpdateTeamCollectionOrderArgs,
|
UpdateTeamCollectionOrderArgs,
|
||||||
} from './input-type.args';
|
} from './input-type.args';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
@@ -142,14 +141,7 @@ export class TeamCollectionResolver {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (E.isLeft(teamCollections)) throwErr(teamCollections.left);
|
if (E.isLeft(teamCollections)) throwErr(teamCollections.left);
|
||||||
return <TeamCollection>{
|
return teamCollections.right;
|
||||||
id: teamCollections.right.id,
|
|
||||||
title: teamCollections.right.title,
|
|
||||||
parentID: teamCollections.right.parentID,
|
|
||||||
data: !teamCollections.right.data
|
|
||||||
? null
|
|
||||||
: JSON.stringify(teamCollections.right.data),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mutations
|
// Mutations
|
||||||
@@ -163,7 +155,6 @@ export class TeamCollectionResolver {
|
|||||||
const teamCollection = await this.teamCollectionService.createCollection(
|
const teamCollection = await this.teamCollectionService.createCollection(
|
||||||
args.teamID,
|
args.teamID,
|
||||||
args.title,
|
args.title,
|
||||||
args.data,
|
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -239,7 +230,6 @@ export class TeamCollectionResolver {
|
|||||||
const teamCollection = await this.teamCollectionService.createCollection(
|
const teamCollection = await this.teamCollectionService.createCollection(
|
||||||
team.right.id,
|
team.right.id,
|
||||||
args.childTitle,
|
args.childTitle,
|
||||||
args.data,
|
|
||||||
args.collectionID,
|
args.collectionID,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -249,7 +239,6 @@ export class TeamCollectionResolver {
|
|||||||
|
|
||||||
@Mutation(() => TeamCollection, {
|
@Mutation(() => TeamCollection, {
|
||||||
description: 'Rename a collection',
|
description: 'Rename a collection',
|
||||||
deprecationReason: 'Switch to updateTeamCollection mutation instead',
|
|
||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard)
|
@UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||||
@@ -314,23 +303,6 @@ export class TeamCollectionResolver {
|
|||||||
return request.right;
|
return request.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => TeamCollection, {
|
|
||||||
description: 'Update Team Collection details',
|
|
||||||
})
|
|
||||||
@UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard)
|
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
|
||||||
async updateTeamCollection(@Args() args: UpdateTeamCollectionArgs) {
|
|
||||||
const updatedTeamCollection =
|
|
||||||
await this.teamCollectionService.updateTeamCollection(
|
|
||||||
args.collectionID,
|
|
||||||
args.data,
|
|
||||||
args.newTitle,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (E.isLeft(updatedTeamCollection)) throwErr(updatedTeamCollection.left);
|
|
||||||
return updatedTeamCollection.right;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscriptions
|
// Subscriptions
|
||||||
|
|
||||||
@Subscription(() => TeamCollection, {
|
@Subscription(() => TeamCollection, {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Team, TeamCollection as DBTeamCollection } from '@prisma/client';
|
import { Team, TeamCollection as DBTeamCollection } from '@prisma/client';
|
||||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||||
import {
|
import {
|
||||||
TEAM_COLL_DATA_INVALID,
|
|
||||||
TEAM_COLL_DEST_SAME,
|
TEAM_COLL_DEST_SAME,
|
||||||
TEAM_COLL_INVALID_JSON,
|
TEAM_COLL_INVALID_JSON,
|
||||||
TEAM_COLL_IS_PARENT_COLL,
|
TEAM_COLL_IS_PARENT_COLL,
|
||||||
@@ -18,7 +17,6 @@ import { PrismaService } from 'src/prisma/prisma.service';
|
|||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
import { TeamCollectionService } from './team-collection.service';
|
import { TeamCollectionService } from './team-collection.service';
|
||||||
import { TeamCollection } from './team-collection.model';
|
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockPubSub = mockDeep<PubSubService>();
|
const mockPubSub = mockDeep<PubSubService>();
|
||||||
@@ -53,60 +51,35 @@ const rootTeamCollection: DBTeamCollection = {
|
|||||||
id: '123',
|
id: '123',
|
||||||
orderIndex: 1,
|
orderIndex: 1,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
data: {},
|
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
const rootTeamCollectionsCasted: TeamCollection = {
|
|
||||||
id: rootTeamCollection.id,
|
|
||||||
title: rootTeamCollection.title,
|
|
||||||
parentID: rootTeamCollection.parentID,
|
|
||||||
data: JSON.stringify(rootTeamCollection.data),
|
|
||||||
};
|
|
||||||
|
|
||||||
const rootTeamCollection_2: DBTeamCollection = {
|
const rootTeamCollection_2: DBTeamCollection = {
|
||||||
id: 'erv',
|
id: 'erv',
|
||||||
orderIndex: 2,
|
orderIndex: 2,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
data: {},
|
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
const rootTeamCollection_2Casted: TeamCollection = {
|
|
||||||
id: 'erv',
|
|
||||||
parentID: null,
|
|
||||||
data: JSON.stringify(rootTeamCollection_2.data),
|
|
||||||
title: 'Root Collection 1',
|
|
||||||
};
|
|
||||||
|
|
||||||
const childTeamCollection: DBTeamCollection = {
|
const childTeamCollection: DBTeamCollection = {
|
||||||
id: 'rfe',
|
id: 'rfe',
|
||||||
orderIndex: 1,
|
orderIndex: 1,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
data: {},
|
|
||||||
title: 'Child Collection 1',
|
title: 'Child Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
const childTeamCollectionCasted: TeamCollection = {
|
|
||||||
id: 'rfe',
|
|
||||||
parentID: rootTeamCollection.id,
|
|
||||||
data: JSON.stringify(childTeamCollection.data),
|
|
||||||
title: 'Child Collection 1',
|
|
||||||
};
|
|
||||||
|
|
||||||
const childTeamCollection_2: DBTeamCollection = {
|
const childTeamCollection_2: DBTeamCollection = {
|
||||||
id: 'bgdz',
|
id: 'bgdz',
|
||||||
orderIndex: 1,
|
orderIndex: 1,
|
||||||
data: {},
|
|
||||||
parentID: rootTeamCollection_2.id,
|
parentID: rootTeamCollection_2.id,
|
||||||
title: 'Child Collection 1',
|
title: 'Child Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
@@ -114,20 +87,11 @@ const childTeamCollection_2: DBTeamCollection = {
|
|||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
const childTeamCollection_2Casted: TeamCollection = {
|
|
||||||
id: 'bgdz',
|
|
||||||
data: JSON.stringify(childTeamCollection_2.data),
|
|
||||||
parentID: rootTeamCollection_2.id,
|
|
||||||
title: 'Child Collection 1',
|
|
||||||
};
|
|
||||||
|
|
||||||
const rootTeamCollectionList: DBTeamCollection[] = [
|
const rootTeamCollectionList: DBTeamCollection[] = [
|
||||||
{
|
{
|
||||||
id: 'fdv',
|
id: 'fdv',
|
||||||
orderIndex: 1,
|
orderIndex: 1,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
data: {},
|
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
@@ -138,8 +102,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
|||||||
orderIndex: 2,
|
orderIndex: 2,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
data: {},
|
|
||||||
|
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
@@ -149,8 +111,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
|||||||
orderIndex: 3,
|
orderIndex: 3,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
data: {},
|
|
||||||
|
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
@@ -159,8 +119,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
|||||||
id: 'bre3',
|
id: 'bre3',
|
||||||
orderIndex: 4,
|
orderIndex: 4,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
data: {},
|
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
@@ -171,8 +129,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
|||||||
orderIndex: 5,
|
orderIndex: 5,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
data: {},
|
|
||||||
|
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
@@ -183,8 +139,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
|||||||
parentID: null,
|
parentID: null,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
data: {},
|
|
||||||
|
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
},
|
},
|
||||||
@@ -194,8 +148,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
|||||||
parentID: null,
|
parentID: null,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
data: {},
|
|
||||||
|
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
},
|
},
|
||||||
@@ -204,7 +156,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
|||||||
orderIndex: 8,
|
orderIndex: 8,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
data: {},
|
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
@@ -214,7 +165,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
|||||||
orderIndex: 9,
|
orderIndex: 9,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
data: {},
|
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
@@ -225,83 +175,17 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
|||||||
parentID: null,
|
parentID: null,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
data: {},
|
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rootTeamCollectionListCasted: TeamCollection[] = [
|
|
||||||
{
|
|
||||||
id: 'fdv',
|
|
||||||
parentID: null,
|
|
||||||
title: 'Root Collection 1',
|
|
||||||
data: JSON.stringify(rootTeamCollection.data),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'fbbg',
|
|
||||||
parentID: null,
|
|
||||||
title: 'Root Collection 1',
|
|
||||||
data: JSON.stringify(rootTeamCollection.data),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'fgbfg',
|
|
||||||
parentID: null,
|
|
||||||
title: 'Root Collection 1',
|
|
||||||
data: JSON.stringify(rootTeamCollection.data),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'bre3',
|
|
||||||
parentID: null,
|
|
||||||
data: JSON.stringify(rootTeamCollection.data),
|
|
||||||
title: 'Root Collection 1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'hghgf',
|
|
||||||
parentID: null,
|
|
||||||
title: 'Root Collection 1',
|
|
||||||
data: JSON.stringify(rootTeamCollection.data),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '123',
|
|
||||||
parentID: null,
|
|
||||||
title: 'Root Collection 1',
|
|
||||||
data: JSON.stringify(rootTeamCollection.data),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '54tyh',
|
|
||||||
parentID: null,
|
|
||||||
title: 'Root Collection 1',
|
|
||||||
data: JSON.stringify(rootTeamCollection.data),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '234re',
|
|
||||||
parentID: null,
|
|
||||||
title: 'Root Collection 1',
|
|
||||||
data: JSON.stringify(rootTeamCollection.data),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '34rtg',
|
|
||||||
parentID: null,
|
|
||||||
title: 'Root Collection 1',
|
|
||||||
data: JSON.stringify(rootTeamCollection.data),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '45tgh',
|
|
||||||
parentID: null,
|
|
||||||
title: 'Root Collection 1',
|
|
||||||
data: JSON.stringify(rootTeamCollection.data),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const childTeamCollectionList: DBTeamCollection[] = [
|
const childTeamCollectionList: DBTeamCollection[] = [
|
||||||
{
|
{
|
||||||
id: '123',
|
id: '123',
|
||||||
orderIndex: 1,
|
orderIndex: 1,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
data: {},
|
|
||||||
|
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
@@ -311,8 +195,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
orderIndex: 2,
|
orderIndex: 2,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
data: {},
|
|
||||||
|
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
@@ -322,8 +204,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
orderIndex: 3,
|
orderIndex: 3,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
data: {},
|
|
||||||
|
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
@@ -332,8 +212,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
id: '567',
|
id: '567',
|
||||||
orderIndex: 4,
|
orderIndex: 4,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
data: {},
|
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
@@ -343,8 +221,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
id: '123',
|
id: '123',
|
||||||
orderIndex: 5,
|
orderIndex: 5,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
data: {},
|
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
@@ -354,8 +230,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
id: '678',
|
id: '678',
|
||||||
orderIndex: 6,
|
orderIndex: 6,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
data: {},
|
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
@@ -365,8 +239,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
id: '789',
|
id: '789',
|
||||||
orderIndex: 7,
|
orderIndex: 7,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
data: {},
|
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
@@ -376,8 +248,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
id: '890',
|
id: '890',
|
||||||
orderIndex: 8,
|
orderIndex: 8,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
data: {},
|
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
@@ -387,7 +257,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
id: '012',
|
id: '012',
|
||||||
orderIndex: 9,
|
orderIndex: 9,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
data: {},
|
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
@@ -397,8 +266,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
id: '0bhu',
|
id: '0bhu',
|
||||||
orderIndex: 10,
|
orderIndex: 10,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
data: {},
|
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
@@ -406,75 +273,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const childTeamCollectionListCasted: TeamCollection[] = [
|
|
||||||
{
|
|
||||||
id: '123',
|
|
||||||
parentID: rootTeamCollection.id,
|
|
||||||
title: 'Root Collection 1',
|
|
||||||
data: JSON.stringify({}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '345',
|
|
||||||
parentID: rootTeamCollection.id,
|
|
||||||
title: 'Root Collection 1',
|
|
||||||
data: JSON.stringify({}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '456',
|
|
||||||
parentID: rootTeamCollection.id,
|
|
||||||
title: 'Root Collection 1',
|
|
||||||
data: JSON.stringify({}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '567',
|
|
||||||
parentID: rootTeamCollection.id,
|
|
||||||
data: JSON.stringify({}),
|
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '123',
|
|
||||||
parentID: rootTeamCollection.id,
|
|
||||||
data: JSON.stringify({}),
|
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '678',
|
|
||||||
parentID: rootTeamCollection.id,
|
|
||||||
data: JSON.stringify({}),
|
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '789',
|
|
||||||
parentID: rootTeamCollection.id,
|
|
||||||
data: JSON.stringify({}),
|
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '890',
|
|
||||||
parentID: rootTeamCollection.id,
|
|
||||||
data: JSON.stringify({}),
|
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '012',
|
|
||||||
parentID: rootTeamCollection.id,
|
|
||||||
data: JSON.stringify({}),
|
|
||||||
title: 'Root Collection 1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '0bhu',
|
|
||||||
parentID: rootTeamCollection.id,
|
|
||||||
data: JSON.stringify({}),
|
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockReset(mockPrisma);
|
mockReset(mockPrisma);
|
||||||
mockPubSub.publish.mockClear();
|
mockPubSub.publish.mockClear();
|
||||||
@@ -513,7 +311,7 @@ describe('getParentOfCollection', () => {
|
|||||||
const result = await teamCollectionService.getParentOfCollection(
|
const result = await teamCollectionService.getParentOfCollection(
|
||||||
childTeamCollection.id,
|
childTeamCollection.id,
|
||||||
);
|
);
|
||||||
expect(result).toEqual(rootTeamCollectionsCasted);
|
expect(result).toEqual(rootTeamCollection);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return null successfully for a root collection with valid collectionID', async () => {
|
test('should return null successfully for a root collection with valid collectionID', async () => {
|
||||||
@@ -549,7 +347,7 @@ describe('getChildrenOfCollection', () => {
|
|||||||
null,
|
null,
|
||||||
10,
|
10,
|
||||||
);
|
);
|
||||||
expect(result).toEqual(childTeamCollectionListCasted);
|
expect(result).toEqual(childTeamCollectionList);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return a list of 3 child collections successfully with cursor being equal to the 7th item in the list', async () => {
|
test('should return a list of 3 child collections successfully with cursor being equal to the 7th item in the list', async () => {
|
||||||
@@ -565,9 +363,9 @@ describe('getChildrenOfCollection', () => {
|
|||||||
10,
|
10,
|
||||||
);
|
);
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{ ...childTeamCollectionListCasted[7] },
|
{ ...childTeamCollectionList[7] },
|
||||||
{ ...childTeamCollectionListCasted[8] },
|
{ ...childTeamCollectionList[8] },
|
||||||
{ ...childTeamCollectionListCasted[9] },
|
{ ...childTeamCollectionList[9] },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -594,7 +392,7 @@ describe('getTeamRootCollections', () => {
|
|||||||
null,
|
null,
|
||||||
10,
|
10,
|
||||||
);
|
);
|
||||||
expect(result).toEqual(rootTeamCollectionListCasted);
|
expect(result).toEqual(rootTeamCollectionList);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return a list of 3 root collections successfully with cursor being equal to the 7th item in the list', async () => {
|
test('should return a list of 3 root collections successfully with cursor being equal to the 7th item in the list', async () => {
|
||||||
@@ -610,9 +408,9 @@ describe('getTeamRootCollections', () => {
|
|||||||
10,
|
10,
|
||||||
);
|
);
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{ ...rootTeamCollectionListCasted[7] },
|
{ ...rootTeamCollectionList[7] },
|
||||||
{ ...rootTeamCollectionListCasted[8] },
|
{ ...rootTeamCollectionList[8] },
|
||||||
{ ...rootTeamCollectionListCasted[9] },
|
{ ...rootTeamCollectionList[9] },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -666,7 +464,6 @@ describe('createCollection', () => {
|
|||||||
const result = await teamCollectionService.createCollection(
|
const result = await teamCollectionService.createCollection(
|
||||||
rootTeamCollection.teamID,
|
rootTeamCollection.teamID,
|
||||||
'ab',
|
'ab',
|
||||||
JSON.stringify(rootTeamCollection.data),
|
|
||||||
rootTeamCollection.id,
|
rootTeamCollection.id,
|
||||||
);
|
);
|
||||||
expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE);
|
expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE);
|
||||||
@@ -681,27 +478,11 @@ describe('createCollection', () => {
|
|||||||
const result = await teamCollectionService.createCollection(
|
const result = await teamCollectionService.createCollection(
|
||||||
rootTeamCollection.teamID,
|
rootTeamCollection.teamID,
|
||||||
'abcd',
|
'abcd',
|
||||||
JSON.stringify(rootTeamCollection.data),
|
|
||||||
rootTeamCollection.id,
|
rootTeamCollection.id,
|
||||||
);
|
);
|
||||||
expect(result).toEqualLeft(TEAM_NOT_OWNER);
|
expect(result).toEqualLeft(TEAM_NOT_OWNER);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw TEAM_COLL_DATA_INVALID when parent TeamCollection does not belong to the team', async () => {
|
|
||||||
// isOwnerCheck
|
|
||||||
mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce(
|
|
||||||
rootTeamCollection,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await teamCollectionService.createCollection(
|
|
||||||
rootTeamCollection.teamID,
|
|
||||||
'abcd',
|
|
||||||
'{',
|
|
||||||
rootTeamCollection.id,
|
|
||||||
);
|
|
||||||
expect(result).toEqualLeft(TEAM_COLL_DATA_INVALID);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should successfully create a new root TeamCollection with valid inputs', async () => {
|
test('should successfully create a new root TeamCollection with valid inputs', async () => {
|
||||||
// isOwnerCheck
|
// isOwnerCheck
|
||||||
mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce(
|
mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce(
|
||||||
@@ -715,10 +496,9 @@ describe('createCollection', () => {
|
|||||||
const result = await teamCollectionService.createCollection(
|
const result = await teamCollectionService.createCollection(
|
||||||
rootTeamCollection.teamID,
|
rootTeamCollection.teamID,
|
||||||
'abcdefg',
|
'abcdefg',
|
||||||
JSON.stringify(rootTeamCollection.data),
|
|
||||||
rootTeamCollection.id,
|
rootTeamCollection.id,
|
||||||
);
|
);
|
||||||
expect(result).toEqualRight(rootTeamCollectionsCasted);
|
expect(result).toEqualRight(rootTeamCollection);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should successfully create a new child TeamCollection with valid inputs', async () => {
|
test('should successfully create a new child TeamCollection with valid inputs', async () => {
|
||||||
@@ -734,10 +514,9 @@ describe('createCollection', () => {
|
|||||||
const result = await teamCollectionService.createCollection(
|
const result = await teamCollectionService.createCollection(
|
||||||
childTeamCollection.teamID,
|
childTeamCollection.teamID,
|
||||||
childTeamCollection.title,
|
childTeamCollection.title,
|
||||||
JSON.stringify(rootTeamCollection.data),
|
|
||||||
rootTeamCollection.id,
|
rootTeamCollection.id,
|
||||||
);
|
);
|
||||||
expect(result).toEqualRight(childTeamCollectionCasted);
|
expect(result).toEqualRight(childTeamCollection);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should send pubsub message to "team_coll/<teamID>/coll_added" if child TeamCollection is created successfully', async () => {
|
test('should send pubsub message to "team_coll/<teamID>/coll_added" if child TeamCollection is created successfully', async () => {
|
||||||
@@ -753,13 +532,11 @@ describe('createCollection', () => {
|
|||||||
const result = await teamCollectionService.createCollection(
|
const result = await teamCollectionService.createCollection(
|
||||||
childTeamCollection.teamID,
|
childTeamCollection.teamID,
|
||||||
childTeamCollection.title,
|
childTeamCollection.title,
|
||||||
JSON.stringify(rootTeamCollection.data),
|
|
||||||
|
|
||||||
rootTeamCollection.id,
|
rootTeamCollection.id,
|
||||||
);
|
);
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_coll/${childTeamCollection.teamID}/coll_added`,
|
`team_coll/${childTeamCollection.teamID}/coll_added`,
|
||||||
childTeamCollectionCasted,
|
childTeamCollection,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -776,13 +553,11 @@ describe('createCollection', () => {
|
|||||||
const result = await teamCollectionService.createCollection(
|
const result = await teamCollectionService.createCollection(
|
||||||
rootTeamCollection.teamID,
|
rootTeamCollection.teamID,
|
||||||
'abcdefg',
|
'abcdefg',
|
||||||
JSON.stringify(rootTeamCollection.data),
|
|
||||||
|
|
||||||
rootTeamCollection.id,
|
rootTeamCollection.id,
|
||||||
);
|
);
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_coll/${rootTeamCollection.teamID}/coll_added`,
|
`team_coll/${rootTeamCollection.teamID}/coll_added`,
|
||||||
rootTeamCollectionsCasted,
|
rootTeamCollection,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -812,7 +587,7 @@ describe('renameCollection', () => {
|
|||||||
'NewTitle',
|
'NewTitle',
|
||||||
);
|
);
|
||||||
expect(result).toEqualRight({
|
expect(result).toEqualRight({
|
||||||
...rootTeamCollectionsCasted,
|
...rootTeamCollection,
|
||||||
title: 'NewTitle',
|
title: 'NewTitle',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -850,7 +625,7 @@ describe('renameCollection', () => {
|
|||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_coll/${rootTeamCollection.teamID}/coll_updated`,
|
`team_coll/${rootTeamCollection.teamID}/coll_updated`,
|
||||||
{
|
{
|
||||||
...rootTeamCollectionsCasted,
|
...rootTeamCollection,
|
||||||
title: 'NewTitle',
|
title: 'NewTitle',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1057,8 +832,9 @@ describe('moveCollection', () => {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
expect(result).toEqualRight({
|
expect(result).toEqualRight({
|
||||||
...childTeamCollectionCasted,
|
...childTeamCollection,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
|
orderIndex: 2,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1114,8 +890,9 @@ describe('moveCollection', () => {
|
|||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_coll/${childTeamCollection.teamID}/coll_moved`,
|
`team_coll/${childTeamCollection.teamID}/coll_moved`,
|
||||||
{
|
{
|
||||||
...childTeamCollectionCasted,
|
...childTeamCollection,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
|
orderIndex: 2,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1154,8 +931,9 @@ describe('moveCollection', () => {
|
|||||||
childTeamCollection_2.id,
|
childTeamCollection_2.id,
|
||||||
);
|
);
|
||||||
expect(result).toEqualRight({
|
expect(result).toEqualRight({
|
||||||
...rootTeamCollectionsCasted,
|
...rootTeamCollection,
|
||||||
parentID: childTeamCollection_2Casted.id,
|
parentID: childTeamCollection_2.id,
|
||||||
|
orderIndex: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1195,8 +973,9 @@ describe('moveCollection', () => {
|
|||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_coll/${childTeamCollection_2.teamID}/coll_moved`,
|
`team_coll/${childTeamCollection_2.teamID}/coll_moved`,
|
||||||
{
|
{
|
||||||
...rootTeamCollectionsCasted,
|
...rootTeamCollection,
|
||||||
parentID: childTeamCollection_2Casted.id,
|
parentID: childTeamCollection_2.id,
|
||||||
|
orderIndex: 1,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1235,8 +1014,9 @@ describe('moveCollection', () => {
|
|||||||
childTeamCollection_2.id,
|
childTeamCollection_2.id,
|
||||||
);
|
);
|
||||||
expect(result).toEqualRight({
|
expect(result).toEqualRight({
|
||||||
...childTeamCollectionCasted,
|
...childTeamCollection,
|
||||||
parentID: childTeamCollection_2Casted.id,
|
parentID: childTeamCollection_2.id,
|
||||||
|
orderIndex: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1276,8 +1056,9 @@ describe('moveCollection', () => {
|
|||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_coll/${childTeamCollection.teamID}/coll_moved`,
|
`team_coll/${childTeamCollection.teamID}/coll_moved`,
|
||||||
{
|
{
|
||||||
...childTeamCollectionCasted,
|
...childTeamCollection,
|
||||||
parentID: childTeamCollection_2Casted.id,
|
parentID: childTeamCollection_2.id,
|
||||||
|
orderIndex: 1,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1373,7 +1154,7 @@ describe('updateCollectionOrder', () => {
|
|||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_coll/${childTeamCollectionList[4].teamID}/coll_order_updated`,
|
`team_coll/${childTeamCollectionList[4].teamID}/coll_order_updated`,
|
||||||
{
|
{
|
||||||
collection: rootTeamCollectionListCasted[4],
|
collection: rootTeamCollectionList[4],
|
||||||
nextCollection: null,
|
nextCollection: null,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1454,8 +1235,8 @@ describe('updateCollectionOrder', () => {
|
|||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_coll/${childTeamCollectionList[2].teamID}/coll_order_updated`,
|
`team_coll/${childTeamCollectionList[2].teamID}/coll_order_updated`,
|
||||||
{
|
{
|
||||||
collection: childTeamCollectionListCasted[4],
|
collection: childTeamCollectionList[4],
|
||||||
nextCollection: childTeamCollectionListCasted[2],
|
nextCollection: childTeamCollectionList[2],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1521,7 +1302,7 @@ describe('importCollectionsFromJSON', () => {
|
|||||||
);
|
);
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_coll/${rootTeamCollection.teamID}/coll_added`,
|
`team_coll/${rootTeamCollection.teamID}/coll_added`,
|
||||||
rootTeamCollectionsCasted,
|
rootTeamCollection,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1640,7 +1421,7 @@ describe('replaceCollectionsWithJSON', () => {
|
|||||||
);
|
);
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_coll/${rootTeamCollection.teamID}/coll_added`,
|
`team_coll/${rootTeamCollection.teamID}/coll_added`,
|
||||||
rootTeamCollectionsCasted,
|
rootTeamCollection,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1677,64 +1458,4 @@ describe('totalCollectionsInTeam', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('updateTeamCollection', () => {
|
|
||||||
test('should throw TEAM_COLL_SHORT_TITLE if title is invalid', async () => {
|
|
||||||
const result = await teamCollectionService.updateTeamCollection(
|
|
||||||
rootTeamCollection.id,
|
|
||||||
JSON.stringify(rootTeamCollection.data),
|
|
||||||
'de',
|
|
||||||
);
|
|
||||||
expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw TEAM_COLL_DATA_INVALID is collection data is invalid', async () => {
|
|
||||||
const result = await teamCollectionService.updateTeamCollection(
|
|
||||||
rootTeamCollection.id,
|
|
||||||
'{',
|
|
||||||
rootTeamCollection.title,
|
|
||||||
);
|
|
||||||
expect(result).toEqualLeft(TEAM_COLL_DATA_INVALID);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw TEAM_COLL_NOT_FOUND is collectionID is invalid', async () => {
|
|
||||||
mockPrisma.teamCollection.update.mockRejectedValueOnce('RecordNotFound');
|
|
||||||
|
|
||||||
const result = await teamCollectionService.updateTeamCollection(
|
|
||||||
'invalid_id',
|
|
||||||
JSON.stringify(rootTeamCollection.data),
|
|
||||||
rootTeamCollection.title,
|
|
||||||
);
|
|
||||||
expect(result).toEqualLeft(TEAM_COLL_NOT_FOUND);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should successfully update a collection', async () => {
|
|
||||||
mockPrisma.teamCollection.update.mockResolvedValueOnce(rootTeamCollection);
|
|
||||||
|
|
||||||
const result = await teamCollectionService.updateTeamCollection(
|
|
||||||
rootTeamCollection.id,
|
|
||||||
JSON.stringify({ foo: 'bar' }),
|
|
||||||
'new_title',
|
|
||||||
);
|
|
||||||
expect(result).toEqualRight({
|
|
||||||
data: JSON.stringify({ foo: 'bar' }),
|
|
||||||
title: 'new_title',
|
|
||||||
...rootTeamCollectionsCasted,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should send pubsub message to "team_coll/<teamID>/coll_updated" if TeamCollection is updated successfully', async () => {
|
|
||||||
mockPrisma.teamCollection.update.mockResolvedValueOnce(rootTeamCollection);
|
|
||||||
|
|
||||||
const result = await teamCollectionService.updateTeamCollection(
|
|
||||||
rootTeamCollection.id,
|
|
||||||
JSON.stringify(rootTeamCollection.data),
|
|
||||||
rootTeamCollection.title,
|
|
||||||
);
|
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
|
||||||
`team_coll/${rootTeamCollection.teamID}/coll_updated`,
|
|
||||||
rootTeamCollectionsCasted,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//ToDo: write test cases for exportCollectionsToJSON
|
//ToDo: write test cases for exportCollectionsToJSON
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
TEAM_COLL_IS_PARENT_COLL,
|
TEAM_COLL_IS_PARENT_COLL,
|
||||||
TEAM_COL_SAME_NEXT_COLL,
|
TEAM_COL_SAME_NEXT_COLL,
|
||||||
TEAM_COL_REORDERING_FAILED,
|
TEAM_COL_REORDERING_FAILED,
|
||||||
TEAM_COLL_DATA_INVALID,
|
|
||||||
} from '../errors';
|
} from '../errors';
|
||||||
import { PubSubService } from '../pubsub/pubsub.service';
|
import { PubSubService } from '../pubsub/pubsub.service';
|
||||||
import { isValidLength } from 'src/utils';
|
import { isValidLength } from 'src/utils';
|
||||||
@@ -70,7 +69,6 @@ export class TeamCollectionService {
|
|||||||
this.generatePrismaQueryObjForFBCollFolder(f, teamID, index + 1),
|
this.generatePrismaQueryObjForFBCollFolder(f, teamID, index + 1),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
data: folder.data ?? undefined,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +118,6 @@ export class TeamCollectionService {
|
|||||||
name: collection.right.title,
|
name: collection.right.title,
|
||||||
folders: childrenCollectionObjects,
|
folders: childrenCollectionObjects,
|
||||||
requests: requests.map((x) => x.request),
|
requests: requests.map((x) => x.request),
|
||||||
data: JSON.stringify(collection.right.data),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return E.right(result);
|
return E.right(result);
|
||||||
@@ -201,11 +198,8 @@ export class TeamCollectionService {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
teamCollections.forEach((collection) =>
|
teamCollections.forEach((x) =>
|
||||||
this.pubsub.publish(
|
this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x),
|
||||||
`team_coll/${destTeamID}/coll_added`,
|
|
||||||
this.cast(collection),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return E.right(true);
|
return E.right(true);
|
||||||
@@ -274,11 +268,8 @@ export class TeamCollectionService {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
teamCollections.forEach((collections) =>
|
teamCollections.forEach((x) =>
|
||||||
this.pubsub.publish(
|
this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x),
|
||||||
`team_coll/${destTeamID}/coll_added`,
|
|
||||||
this.cast(collections),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return E.right(true);
|
return E.right(true);
|
||||||
@@ -286,17 +277,11 @@ export class TeamCollectionService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Typecast a database TeamCollection to a TeamCollection model
|
* Typecast a database TeamCollection to a TeamCollection model
|
||||||
*
|
|
||||||
* @param teamCollection database TeamCollection
|
* @param teamCollection database TeamCollection
|
||||||
* @returns TeamCollection model
|
* @returns TeamCollection model
|
||||||
*/
|
*/
|
||||||
private cast(teamCollection: DBTeamCollection): TeamCollection {
|
private cast(teamCollection: DBTeamCollection): TeamCollection {
|
||||||
return <TeamCollection>{
|
return <TeamCollection>{ ...teamCollection };
|
||||||
id: teamCollection.id,
|
|
||||||
title: teamCollection.title,
|
|
||||||
parentID: teamCollection.parentID,
|
|
||||||
data: !teamCollection.data ? null : JSON.stringify(teamCollection.data),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -339,7 +324,7 @@ export class TeamCollectionService {
|
|||||||
});
|
});
|
||||||
if (!teamCollection) return null;
|
if (!teamCollection) return null;
|
||||||
|
|
||||||
return !teamCollection.parent ? null : this.cast(teamCollection.parent);
|
return teamCollection.parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -350,12 +335,12 @@ export class TeamCollectionService {
|
|||||||
* @param take Number of items we want returned
|
* @param take Number of items we want returned
|
||||||
* @returns A list of child collections
|
* @returns A list of child collections
|
||||||
*/
|
*/
|
||||||
async getChildrenOfCollection(
|
getChildrenOfCollection(
|
||||||
collectionID: string,
|
collectionID: string,
|
||||||
cursor: string | null,
|
cursor: string | null,
|
||||||
take: number,
|
take: number,
|
||||||
) {
|
) {
|
||||||
const res = await this.prisma.teamCollection.findMany({
|
return this.prisma.teamCollection.findMany({
|
||||||
where: {
|
where: {
|
||||||
parentID: collectionID,
|
parentID: collectionID,
|
||||||
},
|
},
|
||||||
@@ -366,12 +351,6 @@ export class TeamCollectionService {
|
|||||||
skip: cursor ? 1 : 0,
|
skip: cursor ? 1 : 0,
|
||||||
cursor: cursor ? { id: cursor } : undefined,
|
cursor: cursor ? { id: cursor } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const childCollections = res.map((teamCollection) =>
|
|
||||||
this.cast(teamCollection),
|
|
||||||
);
|
|
||||||
|
|
||||||
return childCollections;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -387,7 +366,7 @@ export class TeamCollectionService {
|
|||||||
cursor: string | null,
|
cursor: string | null,
|
||||||
take: number,
|
take: number,
|
||||||
) {
|
) {
|
||||||
const res = await this.prisma.teamCollection.findMany({
|
return this.prisma.teamCollection.findMany({
|
||||||
where: {
|
where: {
|
||||||
teamID,
|
teamID,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
@@ -399,12 +378,6 @@ export class TeamCollectionService {
|
|||||||
skip: cursor ? 1 : 0,
|
skip: cursor ? 1 : 0,
|
||||||
cursor: cursor ? { id: cursor } : undefined,
|
cursor: cursor ? { id: cursor } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const teamCollections = res.map((teamCollection) =>
|
|
||||||
this.cast(teamCollection),
|
|
||||||
);
|
|
||||||
|
|
||||||
return teamCollections;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -497,7 +470,6 @@ export class TeamCollectionService {
|
|||||||
async createCollection(
|
async createCollection(
|
||||||
teamID: string,
|
teamID: string,
|
||||||
title: string,
|
title: string,
|
||||||
data: string | null = null,
|
|
||||||
parentTeamCollectionID: string | null,
|
parentTeamCollectionID: string | null,
|
||||||
) {
|
) {
|
||||||
const isTitleValid = isValidLength(title, this.TITLE_LENGTH);
|
const isTitleValid = isValidLength(title, this.TITLE_LENGTH);
|
||||||
@@ -509,13 +481,6 @@ export class TeamCollectionService {
|
|||||||
if (O.isNone(isOwner)) return E.left(TEAM_NOT_OWNER);
|
if (O.isNone(isOwner)) return E.left(TEAM_NOT_OWNER);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data === '') return E.left(TEAM_COLL_DATA_INVALID);
|
|
||||||
if (data) {
|
|
||||||
const jsonReq = stringToJson(data);
|
|
||||||
if (E.isLeft(jsonReq)) return E.left(TEAM_COLL_DATA_INVALID);
|
|
||||||
data = jsonReq.right;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isParent = parentTeamCollectionID
|
const isParent = parentTeamCollectionID
|
||||||
? {
|
? {
|
||||||
connect: {
|
connect: {
|
||||||
@@ -533,23 +498,18 @@ export class TeamCollectionService {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
parent: isParent,
|
parent: isParent,
|
||||||
data: data ?? undefined,
|
|
||||||
orderIndex: !parentTeamCollectionID
|
orderIndex: !parentTeamCollectionID
|
||||||
? (await this.getRootCollectionsCount(teamID)) + 1
|
? (await this.getRootCollectionsCount(teamID)) + 1
|
||||||
: (await this.getChildCollectionsCount(parentTeamCollectionID)) + 1,
|
: (await this.getChildCollectionsCount(parentTeamCollectionID)) + 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.pubsub.publish(
|
this.pubsub.publish(`team_coll/${teamID}/coll_added`, teamCollection);
|
||||||
`team_coll/${teamID}/coll_added`,
|
|
||||||
this.cast(teamCollection),
|
|
||||||
);
|
|
||||||
|
|
||||||
return E.right(this.cast(teamCollection));
|
return E.right(this.cast(teamCollection));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Use updateTeamCollection method instead
|
|
||||||
* Update the title of a TeamCollection
|
* Update the title of a TeamCollection
|
||||||
*
|
*
|
||||||
* @param collectionID The Collection ID
|
* @param collectionID The Collection ID
|
||||||
@@ -572,10 +532,10 @@ export class TeamCollectionService {
|
|||||||
|
|
||||||
this.pubsub.publish(
|
this.pubsub.publish(
|
||||||
`team_coll/${updatedTeamCollection.teamID}/coll_updated`,
|
`team_coll/${updatedTeamCollection.teamID}/coll_updated`,
|
||||||
this.cast(updatedTeamCollection),
|
updatedTeamCollection,
|
||||||
);
|
);
|
||||||
|
|
||||||
return E.right(this.cast(updatedTeamCollection));
|
return E.right(updatedTeamCollection);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return E.left(TEAM_COLL_NOT_FOUND);
|
return E.left(TEAM_COLL_NOT_FOUND);
|
||||||
}
|
}
|
||||||
@@ -734,8 +694,8 @@ export class TeamCollectionService {
|
|||||||
* @returns An Option of boolean, is parent or not
|
* @returns An Option of boolean, is parent or not
|
||||||
*/
|
*/
|
||||||
private async isParent(
|
private async isParent(
|
||||||
collection: DBTeamCollection,
|
collection: TeamCollection,
|
||||||
destCollection: DBTeamCollection,
|
destCollection: TeamCollection,
|
||||||
): Promise<O.Option<boolean>> {
|
): Promise<O.Option<boolean>> {
|
||||||
//* Recursively check if collection is a parent by going up the tree of child-parent collections until we reach a root collection i.e parentID === null
|
//* Recursively check if collection is a parent by going up the tree of child-parent collections until we reach a root collection i.e parentID === null
|
||||||
//* Valid condition, isParent returns false
|
//* Valid condition, isParent returns false
|
||||||
@@ -1011,49 +971,4 @@ export class TeamCollectionService {
|
|||||||
const teamCollectionsCount = this.prisma.teamCollection.count();
|
const teamCollectionsCount = this.prisma.teamCollection.count();
|
||||||
return teamCollectionsCount;
|
return teamCollectionsCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update Team Collection details
|
|
||||||
*
|
|
||||||
* @param collectionID Collection ID
|
|
||||||
* @param collectionData new header data in a JSONified string form
|
|
||||||
* @param newTitle New title of the collection
|
|
||||||
* @returns Updated TeamCollection
|
|
||||||
*/
|
|
||||||
async updateTeamCollection(
|
|
||||||
collectionID: string,
|
|
||||||
collectionData: string = null,
|
|
||||||
newTitle: string = null,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
if (newTitle != null) {
|
|
||||||
const isTitleValid = isValidLength(newTitle, this.TITLE_LENGTH);
|
|
||||||
if (!isTitleValid) return E.left(TEAM_COLL_SHORT_TITLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collectionData === '') return E.left(TEAM_COLL_DATA_INVALID);
|
|
||||||
if (collectionData) {
|
|
||||||
const jsonReq = stringToJson(collectionData);
|
|
||||||
if (E.isLeft(jsonReq)) return E.left(TEAM_COLL_DATA_INVALID);
|
|
||||||
collectionData = jsonReq.right;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedTeamCollection = await this.prisma.teamCollection.update({
|
|
||||||
where: { id: collectionID },
|
|
||||||
data: {
|
|
||||||
data: collectionData ?? undefined,
|
|
||||||
title: newTitle ?? undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.pubsub.publish(
|
|
||||||
`team_coll/${updatedTeamCollection.teamID}/coll_updated`,
|
|
||||||
this.cast(updatedTeamCollection),
|
|
||||||
);
|
|
||||||
|
|
||||||
return E.right(this.cast(updatedTeamCollection));
|
|
||||||
} catch (e) {
|
|
||||||
return E.left(TEAM_COLL_NOT_FOUND);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { MailerModule } from 'src/mailer/mailer.module';
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||||
import { PubSubModule } from 'src/pubsub/pubsub.module';
|
import { PubSubModule } from 'src/pubsub/pubsub.module';
|
||||||
import { TeamModule } from 'src/team/team.module';
|
import { TeamModule } from 'src/team/team.module';
|
||||||
@@ -11,7 +12,7 @@ import { TeamInviteeGuard } from './team-invitee.guard';
|
|||||||
import { TeamTeamInviteExtResolver } from './team-teaminvite-ext.resolver';
|
import { TeamTeamInviteExtResolver } from './team-teaminvite-ext.resolver';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, TeamModule, PubSubModule, UserModule],
|
imports: [PrismaModule, TeamModule, PubSubModule, UserModule, MailerModule],
|
||||||
providers: [
|
providers: [
|
||||||
TeamInvitationService,
|
TeamInvitationService,
|
||||||
TeamInvitationResolver,
|
TeamInvitationResolver,
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { UserService } from 'src/user/user.service';
|
|||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
import { validateEmail } from '../utils';
|
import { validateEmail } from '../utils';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamInvitationService {
|
export class TeamInvitationService {
|
||||||
@@ -29,8 +28,8 @@ export class TeamInvitationService {
|
|||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
private readonly teamService: TeamService,
|
private readonly teamService: TeamService,
|
||||||
private readonly mailerService: MailerService,
|
private readonly mailerService: MailerService,
|
||||||
|
|
||||||
private readonly pubsub: PubSubService,
|
private readonly pubsub: PubSubService,
|
||||||
private readonly configService: ConfigService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -151,9 +150,7 @@ export class TeamInvitationService {
|
|||||||
template: 'team-invitation',
|
template: 'team-invitation',
|
||||||
variables: {
|
variables: {
|
||||||
invitee: creator.displayName ?? 'A Hoppscotch User',
|
invitee: creator.displayName ?? 'A Hoppscotch User',
|
||||||
action_url: `${this.configService.get('VITE_BASE_URL')}/join-team?id=${
|
action_url: `${process.env.VITE_BASE_URL}/join-team?id=${dbInvitation.id}`,
|
||||||
dbInvitation.id
|
|
||||||
}`,
|
|
||||||
invite_team_name: team.name,
|
invite_team_name: team.name,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ const teamCollection: DbTeamCollection = {
|
|||||||
id: 'team-coll-1',
|
id: 'team-coll-1',
|
||||||
parentID: null,
|
parentID: null,
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
data: {},
|
|
||||||
title: 'Team Collection 1',
|
title: 'Team Collection 1',
|
||||||
orderIndex: 1,
|
orderIndex: 1,
|
||||||
createdOn: new Date(),
|
createdOn: new Date(),
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
// This interface defines how data will be received from the app when we are importing Hoppscotch collections
|
|
||||||
export interface CollectionFolder {
|
export interface CollectionFolder {
|
||||||
id?: string;
|
id?: string;
|
||||||
folders: CollectionFolder[];
|
folders: CollectionFolder[];
|
||||||
requests: any[];
|
requests: any[];
|
||||||
name: string;
|
name: string;
|
||||||
data?: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
export enum InfraConfigEnum {
|
|
||||||
MAILER_SMTP_URL = 'MAILER_SMTP_URL',
|
|
||||||
MAILER_ADDRESS_FROM = 'MAILER_ADDRESS_FROM',
|
|
||||||
|
|
||||||
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
|
|
||||||
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
|
|
||||||
|
|
||||||
GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID',
|
|
||||||
GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET',
|
|
||||||
|
|
||||||
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
|
||||||
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
|
||||||
|
|
||||||
VITE_ALLOWED_AUTH_PROVIDERS = 'VITE_ALLOWED_AUTH_PROVIDERS',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum InfraConfigEnumForClient {
|
|
||||||
MAILER_SMTP_URL = 'MAILER_SMTP_URL',
|
|
||||||
MAILER_ADDRESS_FROM = 'MAILER_ADDRESS_FROM',
|
|
||||||
|
|
||||||
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
|
|
||||||
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
|
|
||||||
|
|
||||||
GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID',
|
|
||||||
GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET',
|
|
||||||
|
|
||||||
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
|
||||||
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
|
||||||
}
|
|
||||||
@@ -6,13 +6,6 @@ import { PaginationArgs } from 'src/types/input-types.args';
|
|||||||
export class CreateRootUserCollectionArgs {
|
export class CreateRootUserCollectionArgs {
|
||||||
@Field({ name: 'title', description: 'Title of the new user collection' })
|
@Field({ name: 'title', description: 'Title of the new user collection' })
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
@Field({
|
|
||||||
name: 'data',
|
|
||||||
description: 'JSON string representing the collection data',
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
data: string;
|
|
||||||
}
|
}
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
export class CreateChildUserCollectionArgs {
|
export class CreateChildUserCollectionArgs {
|
||||||
@@ -24,13 +17,6 @@ export class CreateChildUserCollectionArgs {
|
|||||||
description: 'ID of the parent to the new user collection',
|
description: 'ID of the parent to the new user collection',
|
||||||
})
|
})
|
||||||
parentUserCollectionID: string;
|
parentUserCollectionID: string;
|
||||||
|
|
||||||
@Field({
|
|
||||||
name: 'data',
|
|
||||||
description: 'JSON string representing the collection data',
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
data: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
@@ -109,26 +95,3 @@ export class ImportUserCollectionsFromJSONArgs {
|
|||||||
})
|
})
|
||||||
parentCollectionID?: string;
|
parentCollectionID?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ArgsType()
|
|
||||||
export class UpdateUserCollectionsArgs {
|
|
||||||
@Field(() => ID, {
|
|
||||||
name: 'userCollectionID',
|
|
||||||
description: 'ID of the user collection',
|
|
||||||
})
|
|
||||||
userCollectionID: string;
|
|
||||||
|
|
||||||
@Field({
|
|
||||||
name: 'newTitle',
|
|
||||||
description: 'The updated title of the user collection',
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
newTitle: string;
|
|
||||||
|
|
||||||
@Field({
|
|
||||||
name: 'data',
|
|
||||||
description: 'JSON string representing the collection data',
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
data: string;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import {
|
|||||||
MoveUserCollectionArgs,
|
MoveUserCollectionArgs,
|
||||||
RenameUserCollectionsArgs,
|
RenameUserCollectionsArgs,
|
||||||
UpdateUserCollectionArgs,
|
UpdateUserCollectionArgs,
|
||||||
UpdateUserCollectionsArgs,
|
|
||||||
} from './input-type.args';
|
} from './input-type.args';
|
||||||
import { ReqType } from 'src/types/RequestTypes';
|
import { ReqType } from 'src/types/RequestTypes';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
@@ -143,13 +142,7 @@ export class UserCollectionResolver {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (E.isLeft(userCollection)) throwErr(userCollection.left);
|
if (E.isLeft(userCollection)) throwErr(userCollection.left);
|
||||||
return <UserCollection>{
|
return userCollection.right;
|
||||||
...userCollection.right,
|
|
||||||
userID: userCollection.right.userUid,
|
|
||||||
data: !userCollection.right.data
|
|
||||||
? null
|
|
||||||
: JSON.stringify(userCollection.right.data),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query(() => UserCollectionExportJSONData, {
|
@Query(() => UserCollectionExportJSONData, {
|
||||||
@@ -198,7 +191,6 @@ export class UserCollectionResolver {
|
|||||||
await this.userCollectionService.createUserCollection(
|
await this.userCollectionService.createUserCollection(
|
||||||
user,
|
user,
|
||||||
args.title,
|
args.title,
|
||||||
args.data,
|
|
||||||
null,
|
null,
|
||||||
ReqType.REST,
|
ReqType.REST,
|
||||||
);
|
);
|
||||||
@@ -220,7 +212,6 @@ export class UserCollectionResolver {
|
|||||||
await this.userCollectionService.createUserCollection(
|
await this.userCollectionService.createUserCollection(
|
||||||
user,
|
user,
|
||||||
args.title,
|
args.title,
|
||||||
args.data,
|
|
||||||
null,
|
null,
|
||||||
ReqType.GQL,
|
ReqType.GQL,
|
||||||
);
|
);
|
||||||
@@ -241,7 +232,6 @@ export class UserCollectionResolver {
|
|||||||
await this.userCollectionService.createUserCollection(
|
await this.userCollectionService.createUserCollection(
|
||||||
user,
|
user,
|
||||||
args.title,
|
args.title,
|
||||||
args.data,
|
|
||||||
args.parentUserCollectionID,
|
args.parentUserCollectionID,
|
||||||
ReqType.GQL,
|
ReqType.GQL,
|
||||||
);
|
);
|
||||||
@@ -262,7 +252,6 @@ export class UserCollectionResolver {
|
|||||||
await this.userCollectionService.createUserCollection(
|
await this.userCollectionService.createUserCollection(
|
||||||
user,
|
user,
|
||||||
args.title,
|
args.title,
|
||||||
args.data,
|
|
||||||
args.parentUserCollectionID,
|
args.parentUserCollectionID,
|
||||||
ReqType.REST,
|
ReqType.REST,
|
||||||
);
|
);
|
||||||
@@ -370,26 +359,6 @@ export class UserCollectionResolver {
|
|||||||
return importedCollection.right;
|
return importedCollection.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => UserCollection, {
|
|
||||||
description: 'Update a UserCollection',
|
|
||||||
})
|
|
||||||
@UseGuards(GqlAuthGuard)
|
|
||||||
async updateUserCollection(
|
|
||||||
@GqlUser() user: AuthUser,
|
|
||||||
@Args() args: UpdateUserCollectionsArgs,
|
|
||||||
) {
|
|
||||||
const updatedUserCollection =
|
|
||||||
await this.userCollectionService.updateUserCollection(
|
|
||||||
args.newTitle,
|
|
||||||
args.data,
|
|
||||||
args.userCollectionID,
|
|
||||||
user.uid,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (E.isLeft(updatedUserCollection)) throwErr(updatedUserCollection.left);
|
|
||||||
return updatedUserCollection.right;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscriptions
|
// Subscriptions
|
||||||
@Subscription(() => UserCollection, {
|
@Subscription(() => UserCollection, {
|
||||||
description: 'Listen for User Collection Creation',
|
description: 'Listen for User Collection Creation',
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,6 @@ import {
|
|||||||
USER_NOT_FOUND,
|
USER_NOT_FOUND,
|
||||||
USER_NOT_OWNER,
|
USER_NOT_OWNER,
|
||||||
USER_COLL_INVALID_JSON,
|
USER_COLL_INVALID_JSON,
|
||||||
USER_COLL_DATA_INVALID,
|
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
@@ -44,12 +43,8 @@ export class UserCollectionService {
|
|||||||
*/
|
*/
|
||||||
private cast(collection: UserCollection) {
|
private cast(collection: UserCollection) {
|
||||||
return <UserCollectionModel>{
|
return <UserCollectionModel>{
|
||||||
id: collection.id,
|
...collection,
|
||||||
title: collection.title,
|
|
||||||
type: collection.type,
|
|
||||||
parentID: collection.parentID,
|
|
||||||
userID: collection.userUid,
|
userID: collection.userUid,
|
||||||
data: !collection.data ? null : JSON.stringify(collection.data),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +146,7 @@ export class UserCollectionService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return !parent ? null : this.cast(parent);
|
return parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -169,7 +164,7 @@ export class UserCollectionService {
|
|||||||
take: number,
|
take: number,
|
||||||
type: ReqType,
|
type: ReqType,
|
||||||
) {
|
) {
|
||||||
const res = await this.prisma.userCollection.findMany({
|
return this.prisma.userCollection.findMany({
|
||||||
where: {
|
where: {
|
||||||
parentID: collectionID,
|
parentID: collectionID,
|
||||||
type: type,
|
type: type,
|
||||||
@@ -181,12 +176,6 @@ export class UserCollectionService {
|
|||||||
skip: cursor ? 1 : 0,
|
skip: cursor ? 1 : 0,
|
||||||
cursor: cursor ? { id: cursor } : undefined,
|
cursor: cursor ? { id: cursor } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const childCollections = res.map((childCollection) =>
|
|
||||||
this.cast(childCollection),
|
|
||||||
);
|
|
||||||
|
|
||||||
return childCollections;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -222,20 +211,12 @@ export class UserCollectionService {
|
|||||||
async createUserCollection(
|
async createUserCollection(
|
||||||
user: AuthUser,
|
user: AuthUser,
|
||||||
title: string,
|
title: string,
|
||||||
data: string | null = null,
|
|
||||||
parentUserCollectionID: string | null,
|
parentUserCollectionID: string | null,
|
||||||
type: ReqType,
|
type: ReqType,
|
||||||
) {
|
) {
|
||||||
const isTitleValid = isValidLength(title, this.TITLE_LENGTH);
|
const isTitleValid = isValidLength(title, this.TITLE_LENGTH);
|
||||||
if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE);
|
if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE);
|
||||||
|
|
||||||
if (data === '') return E.left(USER_COLL_DATA_INVALID);
|
|
||||||
if (data) {
|
|
||||||
const jsonReq = stringToJson(data);
|
|
||||||
if (E.isLeft(jsonReq)) return E.left(USER_COLL_DATA_INVALID);
|
|
||||||
data = jsonReq.right;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If creating a child collection
|
// If creating a child collection
|
||||||
if (parentUserCollectionID !== null) {
|
if (parentUserCollectionID !== null) {
|
||||||
const parentCollection = await this.getUserCollection(
|
const parentCollection = await this.getUserCollection(
|
||||||
@@ -270,19 +251,15 @@ export class UserCollectionService {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
parent: isParent,
|
parent: isParent,
|
||||||
data: data ?? undefined,
|
|
||||||
orderIndex: !parentUserCollectionID
|
orderIndex: !parentUserCollectionID
|
||||||
? (await this.getRootCollectionsCount(user.uid)) + 1
|
? (await this.getRootCollectionsCount(user.uid)) + 1
|
||||||
: (await this.getChildCollectionsCount(parentUserCollectionID)) + 1,
|
: (await this.getChildCollectionsCount(parentUserCollectionID)) + 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.pubsub.publish(
|
await this.pubsub.publish(`user_coll/${user.uid}/created`, userCollection);
|
||||||
`user_coll/${user.uid}/created`,
|
|
||||||
this.cast(userCollection),
|
|
||||||
);
|
|
||||||
|
|
||||||
return E.right(this.cast(userCollection));
|
return E.right(userCollection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -299,7 +276,7 @@ export class UserCollectionService {
|
|||||||
take: number,
|
take: number,
|
||||||
type: ReqType,
|
type: ReqType,
|
||||||
) {
|
) {
|
||||||
const res = await this.prisma.userCollection.findMany({
|
return this.prisma.userCollection.findMany({
|
||||||
where: {
|
where: {
|
||||||
userUid: user.uid,
|
userUid: user.uid,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
@@ -312,12 +289,6 @@ export class UserCollectionService {
|
|||||||
skip: cursor ? 1 : 0,
|
skip: cursor ? 1 : 0,
|
||||||
cursor: cursor ? { id: cursor } : undefined,
|
cursor: cursor ? { id: cursor } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const userCollections = res.map((childCollection) =>
|
|
||||||
this.cast(childCollection),
|
|
||||||
);
|
|
||||||
|
|
||||||
return userCollections;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -336,7 +307,7 @@ export class UserCollectionService {
|
|||||||
take: number,
|
take: number,
|
||||||
type: ReqType,
|
type: ReqType,
|
||||||
) {
|
) {
|
||||||
const res = await this.prisma.userCollection.findMany({
|
return this.prisma.userCollection.findMany({
|
||||||
where: {
|
where: {
|
||||||
userUid: user.uid,
|
userUid: user.uid,
|
||||||
parentID: userCollectionID,
|
parentID: userCollectionID,
|
||||||
@@ -346,16 +317,9 @@ export class UserCollectionService {
|
|||||||
skip: cursor ? 1 : 0,
|
skip: cursor ? 1 : 0,
|
||||||
cursor: cursor ? { id: cursor } : undefined,
|
cursor: cursor ? { id: cursor } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const childCollections = res.map((childCollection) =>
|
|
||||||
this.cast(childCollection),
|
|
||||||
);
|
|
||||||
|
|
||||||
return childCollections;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Use updateUserCollection method instead
|
|
||||||
* Update the title of a UserCollection
|
* Update the title of a UserCollection
|
||||||
*
|
*
|
||||||
* @param newTitle The new title of collection
|
* @param newTitle The new title of collection
|
||||||
@@ -387,10 +351,10 @@ export class UserCollectionService {
|
|||||||
|
|
||||||
this.pubsub.publish(
|
this.pubsub.publish(
|
||||||
`user_coll/${updatedUserCollection.userUid}/updated`,
|
`user_coll/${updatedUserCollection.userUid}/updated`,
|
||||||
this.cast(updatedUserCollection),
|
updatedUserCollection,
|
||||||
);
|
);
|
||||||
|
|
||||||
return E.right(this.cast(updatedUserCollection));
|
return E.right(updatedUserCollection);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return E.left(USER_COLL_NOT_FOUND);
|
return E.left(USER_COLL_NOT_FOUND);
|
||||||
}
|
}
|
||||||
@@ -627,10 +591,10 @@ export class UserCollectionService {
|
|||||||
|
|
||||||
this.pubsub.publish(
|
this.pubsub.publish(
|
||||||
`user_coll/${collection.right.userUid}/moved`,
|
`user_coll/${collection.right.userUid}/moved`,
|
||||||
this.cast(updatedCollection.right),
|
updatedCollection.right,
|
||||||
);
|
);
|
||||||
|
|
||||||
return E.right(this.cast(updatedCollection.right));
|
return E.right(updatedCollection.right);
|
||||||
}
|
}
|
||||||
|
|
||||||
// destCollectionID != null i.e move into another collection
|
// destCollectionID != null i.e move into another collection
|
||||||
@@ -678,10 +642,10 @@ export class UserCollectionService {
|
|||||||
|
|
||||||
this.pubsub.publish(
|
this.pubsub.publish(
|
||||||
`user_coll/${collection.right.userUid}/moved`,
|
`user_coll/${collection.right.userUid}/moved`,
|
||||||
this.cast(updatedCollection.right),
|
updatedCollection.right,
|
||||||
);
|
);
|
||||||
|
|
||||||
return E.right(this.cast(updatedCollection.right));
|
return E.right(updatedCollection.right);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -882,7 +846,6 @@ export class UserCollectionService {
|
|||||||
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
|
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
data: JSON.stringify(collection.right.data),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return E.right(result);
|
return E.right(result);
|
||||||
@@ -955,7 +918,6 @@ export class UserCollectionService {
|
|||||||
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
|
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
data: JSON.stringify(parentCollection.right.data),
|
|
||||||
}),
|
}),
|
||||||
collectionType: parentCollection.right.type,
|
collectionType: parentCollection.right.type,
|
||||||
});
|
});
|
||||||
@@ -1009,7 +971,6 @@ export class UserCollectionService {
|
|||||||
this.generatePrismaQueryObj(f, userID, index + 1, reqType),
|
this.generatePrismaQueryObj(f, userID, index + 1, reqType),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
data: folder.data ?? undefined,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1079,63 +1040,10 @@ export class UserCollectionService {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
userCollections.forEach((collection) =>
|
userCollections.forEach((x) =>
|
||||||
this.pubsub.publish(`user_coll/${userID}/created`, this.cast(collection)),
|
this.pubsub.publish(`user_coll/${userID}/created`, x),
|
||||||
);
|
);
|
||||||
|
|
||||||
return E.right(true);
|
return E.right(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a UserCollection
|
|
||||||
*
|
|
||||||
* @param newTitle The new title of collection
|
|
||||||
* @param userCollectionID The Collection Id
|
|
||||||
* @param userID The User UID
|
|
||||||
* @returns An Either of the updated UserCollection
|
|
||||||
*/
|
|
||||||
async updateUserCollection(
|
|
||||||
newTitle: string = null,
|
|
||||||
collectionData: string | null = null,
|
|
||||||
userCollectionID: string,
|
|
||||||
userID: string,
|
|
||||||
) {
|
|
||||||
if (collectionData === '') return E.left(USER_COLL_DATA_INVALID);
|
|
||||||
|
|
||||||
if (collectionData) {
|
|
||||||
const jsonReq = stringToJson(collectionData);
|
|
||||||
if (E.isLeft(jsonReq)) return E.left(USER_COLL_DATA_INVALID);
|
|
||||||
collectionData = jsonReq.right;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newTitle != null) {
|
|
||||||
const isTitleValid = isValidLength(newTitle, this.TITLE_LENGTH);
|
|
||||||
if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check to see is the collection belongs to the user
|
|
||||||
const isOwner = await this.isOwnerCheck(userCollectionID, userID);
|
|
||||||
if (O.isNone(isOwner)) return E.left(USER_NOT_OWNER);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const updatedUserCollection = await this.prisma.userCollection.update({
|
|
||||||
where: {
|
|
||||||
id: userCollectionID,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
data: collectionData ?? undefined,
|
|
||||||
title: newTitle ?? undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.pubsub.publish(
|
|
||||||
`user_coll/${updatedUserCollection.userUid}/updated`,
|
|
||||||
this.cast(updatedUserCollection),
|
|
||||||
);
|
|
||||||
|
|
||||||
return E.right(this.cast(updatedUserCollection));
|
|
||||||
} catch (error) {
|
|
||||||
return E.left(USER_COLL_NOT_FOUND);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,6 @@ export class UserCollection {
|
|||||||
})
|
})
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
@Field({
|
|
||||||
description: 'JSON string representing the collection data',
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
data: string;
|
|
||||||
|
|
||||||
@Field(() => ReqType, {
|
@Field(() => ReqType, {
|
||||||
description: 'Type of the user collection',
|
description: 'Type of the user collection',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -131,26 +131,6 @@ export const validateEmail = (email: string) => {
|
|||||||
).test(email);
|
).test(email);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks to see if the URL is valid or not
|
|
||||||
* @param url The URL to validate
|
|
||||||
* @returns boolean
|
|
||||||
*/
|
|
||||||
export const validateSMTPUrl = (url: string) => {
|
|
||||||
// Possible valid formats
|
|
||||||
// smtp(s)://mail.example.com
|
|
||||||
// smtp(s)://user:pass@mail.example.com
|
|
||||||
// smtp(s)://mail.example.com:587
|
|
||||||
// smtp(s)://user:pass@mail.example.com:587
|
|
||||||
|
|
||||||
if (!url || url.length === 0) return false;
|
|
||||||
|
|
||||||
const regex =
|
|
||||||
/^(smtp|smtps):\/\/(?:([^:]+):([^@]+)@)?((?!\.)[^:]+)(?::(\d+))?$/;
|
|
||||||
if (regex.test(url)) return true;
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* String to JSON parser
|
* String to JSON parser
|
||||||
* @param {str} str The string to parse
|
* @param {str} str The string to parse
|
||||||
@@ -184,20 +164,18 @@ export function isValidLength(title: string, length: number) {
|
|||||||
* It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
|
* It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
|
||||||
* If not, it throws an error.
|
* If not, it throws an error.
|
||||||
*/
|
*/
|
||||||
export function checkEnvironmentAuthProvider(
|
export function checkEnvironmentAuthProvider() {
|
||||||
VITE_ALLOWED_AUTH_PROVIDERS: string,
|
if (!process.env.hasOwnProperty('VITE_ALLOWED_AUTH_PROVIDERS')) {
|
||||||
) {
|
|
||||||
if (!VITE_ALLOWED_AUTH_PROVIDERS) {
|
|
||||||
throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS);
|
throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (VITE_ALLOWED_AUTH_PROVIDERS === '') {
|
if (process.env.VITE_ALLOWED_AUTH_PROVIDERS === '') {
|
||||||
throw new Error(ENV_EMPTY_AUTH_PROVIDERS);
|
throw new Error(ENV_EMPTY_AUTH_PROVIDERS);
|
||||||
}
|
}
|
||||||
|
|
||||||
const givenAuthProviders = VITE_ALLOWED_AUTH_PROVIDERS.split(',').map(
|
const givenAuthProviders = process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(
|
||||||
(provider) => provider.toLocaleUpperCase(),
|
',',
|
||||||
);
|
).map((provider) => provider.toLocaleUpperCase());
|
||||||
const supportedAuthProviders = Object.values(AuthProvider).map(
|
const supportedAuthProviders = Object.values(AuthProvider).map(
|
||||||
(provider: string) => provider.toLocaleUpperCase(),
|
(provider: string) => provider.toLocaleUpperCase(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ module.exports = {
|
|||||||
// The glob patterns Jest uses to detect test files
|
// The glob patterns Jest uses to detect test files
|
||||||
testMatch: [
|
testMatch: [
|
||||||
// "**/__tests__/**/*.[jt]s?(x)",
|
// "**/__tests__/**/*.[jt]s?(x)",
|
||||||
"**/src/__tests__/commands/**/*.*.ts",
|
"**/src/__tests__/**/*.*.ts",
|
||||||
],
|
],
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||||
|
|||||||
@@ -19,9 +19,8 @@
|
|||||||
"debugger": "node debugger.js 9999",
|
"debugger": "node debugger.js 9999",
|
||||||
"prepublish": "pnpm exec tsup",
|
"prepublish": "pnpm exec tsup",
|
||||||
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",
|
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",
|
||||||
"test": "pnpm run build && jest && rm -rf dist",
|
|
||||||
"do-typecheck": "pnpm exec tsc --noEmit",
|
"do-typecheck": "pnpm exec tsc --noEmit",
|
||||||
"do-test": "pnpm test"
|
"test": "pnpm run build && jest && rm -rf dist"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"cli",
|
"cli",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ describe("Test 'hopp test <file> --env <file>' command:", () => {
|
|||||||
const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
|
const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
|
||||||
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
|
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
|
||||||
const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`;
|
const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`;
|
||||||
const { error, stdout } = await execAsync(cmd);
|
const { error } = await execAsync(cmd);
|
||||||
|
|
||||||
expect(error).toBeNull();
|
expect(error).toBeNull();
|
||||||
});
|
});
|
||||||
@@ -129,6 +129,7 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
|
|||||||
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
|
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
|
||||||
const { stderr } = await execAsync(cmd);
|
const { stderr } = await execAsync(cmd);
|
||||||
const out = getErrorCode(stderr);
|
const out = getErrorCode(stderr);
|
||||||
|
console.log("invalid value thing", out)
|
||||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"URL": "https://echo.hoppscotch.io",
|
"URL": "https://echo.hoppscotch.io",
|
||||||
"HOST": "echo.hoppscotch.io",
|
"HOST": "echo.hoppscotch.io",
|
||||||
|
"X-COUNTRY": "IN",
|
||||||
"BODY_VALUE": "body_value",
|
"BODY_VALUE": "body_value",
|
||||||
"BODY_KEY": "body_key"
|
"BODY_KEY": "body_key"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"method": "POST",
|
"method": "POST",
|
||||||
"auth": { "authType": "none", "authActive": true },
|
"auth": { "authType": "none", "authActive": true },
|
||||||
"preRequestScript": "",
|
"preRequestScript": "",
|
||||||
"testScript": "const HOST = pw.env.get(\"HOST\");\nconst UNSET_ENV = pw.env.get(\"UNSET_ENV\");\nconst EXPECTED_URL = \"https://echo.hoppscotch.io\";\nconst URL = pw.env.get(\"URL\");\nconst BODY_VALUE = pw.env.get(\"BODY_VALUE\");\n\n// Check JSON response property\npw.test(\"Check headers properties.\", ()=> {\n pw.expect(pw.response.body.headers.host).toBe(HOST);\n});\n\npw.test(\"Check data properties.\", () => {\n\tconst DATA = pw.response.body.data;\n \n pw.expect(DATA).toBeType(\"string\");\n pw.expect(JSON.parse(DATA).body_key).toBe(BODY_VALUE);\n});\n\npw.test(\"Check request URL.\", () => {\n pw.expect(URL).toBe(EXPECTED_URL);\n})\n\npw.test(\"Check unset ENV.\", () => {\n pw.expect(UNSET_ENV).toBeType(\"undefined\");\n})",
|
"testScript": "const HOST = pw.env.get(\"HOST\");\nconst UNSET_ENV = pw.env.get(\"UNSET_ENV\");\nconst EXPECTED_URL = \"https://echo.hoppscotch.io\";\nconst URL = pw.env.get(\"URL\");\nconst X_COUNTRY = pw.env.get(\"X-COUNTRY\");\nconst BODY_VALUE = pw.env.get(\"BODY_VALUE\");\n\n// Check JSON response property\npw.test(\"Check headers properties.\", ()=> {\n pw.expect(pw.response.body.headers.host).toBe(HOST);\n\t pw.expect(pw.response.body.headers[\"x-country\"]).toBe(X_COUNTRY); \n});\n\npw.test(\"Check data properties.\", () => {\n\tconst DATA = pw.response.body.data;\n \n pw.expect(DATA).toBeType(\"string\");\n pw.expect(JSON.parse(DATA).body_key).toBe(BODY_VALUE);\n});\n\npw.test(\"Check request URL.\", () => {\n pw.expect(URL).toBe(EXPECTED_URL);\n})\n\npw.test(\"Check unset ENV.\", () => {\n pw.expect(UNSET_ENV).toBeType(\"undefined\");\n})",
|
||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"
|
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { HoppCollection } from "@hoppscotch/data";
|
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||||
import { HoppEnvs } from "./request";
|
import { HoppEnvs } from "./request";
|
||||||
|
|
||||||
export type CollectionRunnerParam = {
|
export type CollectionRunnerParam = {
|
||||||
collections: HoppCollection[];
|
collections: HoppCollection<HoppRESTRequest>[];
|
||||||
envs: HoppEnvs;
|
envs: HoppEnvs;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export type HoppEnvs = {
|
|||||||
|
|
||||||
export type CollectionStack = {
|
export type CollectionStack = {
|
||||||
path: string;
|
path: string;
|
||||||
collection: HoppCollection;
|
collection: HoppCollection<HoppRESTRequest>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RequestReport = {
|
export type RequestReport = {
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { HoppCollection, isHoppRESTRequest } from "@hoppscotch/data";
|
import {
|
||||||
|
HoppCollection,
|
||||||
|
HoppRESTRequest,
|
||||||
|
isHoppRESTRequest,
|
||||||
|
} from "@hoppscotch/data";
|
||||||
import * as A from "fp-ts/Array";
|
import * as A from "fp-ts/Array";
|
||||||
import { CommanderError } from "commander";
|
import { CommanderError } from "commander";
|
||||||
import { HoppCLIError, HoppErrnoException } from "../types/errors";
|
import { HoppCLIError, HoppErrnoException } from "../types/errors";
|
||||||
@@ -20,7 +24,9 @@ export const hasProperty = <P extends PropertyKey>(
|
|||||||
* @returns True, if unknown parameter is valid Hoppscotch REST Collection;
|
* @returns True, if unknown parameter is valid Hoppscotch REST Collection;
|
||||||
* False, otherwise.
|
* False, otherwise.
|
||||||
*/
|
*/
|
||||||
export const isRESTCollection = (param: unknown): param is HoppCollection => {
|
export const isRESTCollection = (
|
||||||
|
param: unknown
|
||||||
|
): param is HoppCollection<HoppRESTRequest> => {
|
||||||
if (!!param && typeof param === "object") {
|
if (!!param && typeof param === "object") {
|
||||||
if (!hasProperty(param, "v") || typeof param.v !== "number") {
|
if (!hasProperty(param, "v") || typeof param.v !== "number") {
|
||||||
return false;
|
return false;
|
||||||
@@ -56,6 +62,7 @@ export const isRESTCollection = (param: unknown): param is HoppCollection => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if given error data is of type HoppCLIError, based on existence
|
* Checks if given error data is of type HoppCLIError, based on existence
|
||||||
* of code property.
|
* of code property.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { pipe } from "fp-ts/function";
|
|||||||
import { bold } from "chalk";
|
import { bold } from "chalk";
|
||||||
import { log } from "console";
|
import { log } from "console";
|
||||||
import round from "lodash/round";
|
import round from "lodash/round";
|
||||||
import { HoppCollection } from "@hoppscotch/data";
|
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||||
import {
|
import {
|
||||||
HoppEnvs,
|
HoppEnvs,
|
||||||
CollectionStack,
|
CollectionStack,
|
||||||
@@ -41,9 +41,9 @@ const { WARN, FAIL } = exceptionColors;
|
|||||||
* @param param Data of hopp-collection with hopp-requests, envs to be processed.
|
* @param param Data of hopp-collection with hopp-requests, envs to be processed.
|
||||||
* @returns List of report for each processed request.
|
* @returns List of report for each processed request.
|
||||||
*/
|
*/
|
||||||
export const collectionsRunner = async (
|
export const collectionsRunner =
|
||||||
param: CollectionRunnerParam
|
async (param: CollectionRunnerParam): Promise<RequestReport[]> =>
|
||||||
): Promise<RequestReport[]> => {
|
{
|
||||||
const envs: HoppEnvs = param.envs;
|
const envs: HoppEnvs = param.envs;
|
||||||
const delay = param.delay ?? 0;
|
const delay = param.delay ?? 0;
|
||||||
const requestsReport: RequestReport[] = [];
|
const requestsReport: RequestReport[] = [];
|
||||||
@@ -92,7 +92,7 @@ export const collectionsRunner = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
return requestsReport;
|
return requestsReport;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms collections to generate collection-stack which describes each collection's
|
* Transforms collections to generate collection-stack which describes each collection's
|
||||||
@@ -100,7 +100,9 @@ export const collectionsRunner = async (
|
|||||||
* @param collections Hopp-collection objects to be mapped to collection-stack type.
|
* @param collections Hopp-collection objects to be mapped to collection-stack type.
|
||||||
* @returns Mapped collections to collection-stack.
|
* @returns Mapped collections to collection-stack.
|
||||||
*/
|
*/
|
||||||
const getCollectionStack = (collections: HoppCollection[]): CollectionStack[] =>
|
const getCollectionStack = (
|
||||||
|
collections: HoppCollection<HoppRESTRequest>[]
|
||||||
|
): CollectionStack[] =>
|
||||||
pipe(
|
pipe(
|
||||||
collections,
|
collections,
|
||||||
A.map(
|
A.map(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import fs from "fs/promises";
|
|||||||
import { FormDataEntry } from "../types/request";
|
import { FormDataEntry } from "../types/request";
|
||||||
import { error } from "../types/errors";
|
import { error } from "../types/errors";
|
||||||
import { isRESTCollection, isHoppErrnoException } from "./checks";
|
import { isRESTCollection, isHoppErrnoException } from "./checks";
|
||||||
import { HoppCollection } from "@hoppscotch/data";
|
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses array of FormDataEntry to FormData.
|
* Parses array of FormDataEntry to FormData.
|
||||||
@@ -35,20 +35,20 @@ export const parseErrorMessage = (e: unknown) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function readJsonFile(path: string): Promise<unknown> {
|
export async function readJsonFile(path: string): Promise<unknown> {
|
||||||
if (!path.endsWith(".json")) {
|
if(!path.endsWith('.json')) {
|
||||||
throw error({ code: "INVALID_FILE_TYPE", data: path });
|
throw error({ code: "INVALID_FILE_TYPE", data: path })
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.access(path);
|
await fs.access(path)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw error({ code: "FILE_NOT_FOUND", path: path });
|
throw error({ code: "FILE_NOT_FOUND", path: path })
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return JSON.parse((await fs.readFile(path)).toString());
|
return JSON.parse((await fs.readFile(path)).toString())
|
||||||
} catch (e) {
|
} catch(e) {
|
||||||
throw error({ code: "UNKNOWN_ERROR", data: e });
|
throw error({ code: "UNKNOWN_ERROR", data: e })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,24 +56,22 @@ export async function readJsonFile(path: string): Promise<unknown> {
|
|||||||
* Parses collection json file for given path:context.path, and validates
|
* Parses collection json file for given path:context.path, and validates
|
||||||
* the parsed collectiona array.
|
* the parsed collectiona array.
|
||||||
* @param path Collection json file path.
|
* @param path Collection json file path.
|
||||||
* @returns For successful parsing we get array of HoppCollection,
|
* @returns For successful parsing we get array of HoppCollection<HoppRESTRequest>,
|
||||||
*/
|
*/
|
||||||
export async function parseCollectionData(
|
export async function parseCollectionData(
|
||||||
path: string
|
path: string
|
||||||
): Promise<HoppCollection[]> {
|
): Promise<HoppCollection<HoppRESTRequest>[]> {
|
||||||
let contents = await readJsonFile(path);
|
let contents = await readJsonFile(path)
|
||||||
|
|
||||||
const maybeArrayOfCollections: unknown[] = Array.isArray(contents)
|
const maybeArrayOfCollections: unknown[] = Array.isArray(contents) ? contents : [contents]
|
||||||
? contents
|
|
||||||
: [contents];
|
|
||||||
|
|
||||||
if (maybeArrayOfCollections.some((x) => !isRESTCollection(x))) {
|
if(maybeArrayOfCollections.some((x) => !isRESTCollection(x))) {
|
||||||
throw error({
|
throw error({
|
||||||
code: "MALFORMED_COLLECTION",
|
code: "MALFORMED_COLLECTION",
|
||||||
path,
|
path,
|
||||||
data: "Please check the collection data.",
|
data: "Please check the collection data.",
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return maybeArrayOfCollections as HoppCollection[];
|
return maybeArrayOfCollections as HoppCollection<HoppRESTRequest>[]
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -6,24 +6,23 @@ import {
|
|||||||
parseTemplateString,
|
parseTemplateString,
|
||||||
parseTemplateStringE,
|
parseTemplateStringE,
|
||||||
} from "@hoppscotch/data";
|
} from "@hoppscotch/data";
|
||||||
import { runPreRequestScript } from "@hoppscotch/js-sandbox/node";
|
import { runPreRequestScript } from "@hoppscotch/js-sandbox";
|
||||||
import * as A from "fp-ts/Array";
|
|
||||||
import * as E from "fp-ts/Either";
|
|
||||||
import * as O from "fp-ts/Option";
|
|
||||||
import * as RA from "fp-ts/ReadonlyArray";
|
|
||||||
import * as TE from "fp-ts/TaskEither";
|
|
||||||
import { flow, pipe } from "fp-ts/function";
|
import { flow, pipe } from "fp-ts/function";
|
||||||
|
import * as TE from "fp-ts/TaskEither";
|
||||||
|
import * as E from "fp-ts/Either";
|
||||||
|
import * as RA from "fp-ts/ReadonlyArray";
|
||||||
|
import * as A from "fp-ts/Array";
|
||||||
|
import * as O from "fp-ts/Option";
|
||||||
import * as S from "fp-ts/string";
|
import * as S from "fp-ts/string";
|
||||||
import qs from "qs";
|
import qs from "qs";
|
||||||
|
|
||||||
import { EffectiveHoppRESTRequest } from "../interfaces/request";
|
import { EffectiveHoppRESTRequest } from "../interfaces/request";
|
||||||
import { HoppCLIError, error } from "../types/errors";
|
import { error, HoppCLIError } from "../types/errors";
|
||||||
import { HoppEnvs } from "../types/request";
|
import { HoppEnvs } from "../types/request";
|
||||||
import { PreRequestMetrics } from "../types/response";
|
|
||||||
import { isHoppCLIError } from "./checks";
|
import { isHoppCLIError } from "./checks";
|
||||||
import { arrayFlatMap, arraySort, tupleToRecord } from "./functions/array";
|
import { tupleToRecord, arraySort, arrayFlatMap } from "./functions/array";
|
||||||
import { getEffectiveFinalMetaData } from "./getters";
|
|
||||||
import { toFormData } from "./mutators";
|
import { toFormData } from "./mutators";
|
||||||
|
import { getEffectiveFinalMetaData } from "./getters";
|
||||||
|
import { PreRequestMetrics } from "../types/response";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs pre-request-script runner over given request which extracts set ENVs and
|
* Runs pre-request-script runner over given request which extracts set ENVs and
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
import { HoppRESTRequest } from "@hoppscotch/data";
|
import { HoppRESTRequest } from "@hoppscotch/data";
|
||||||
import { TestDescriptor } from "@hoppscotch/js-sandbox";
|
import { execTestScript, TestDescriptor } from "@hoppscotch/js-sandbox";
|
||||||
import { runTestScript } from "@hoppscotch/js-sandbox/node";
|
|
||||||
import * as A from "fp-ts/Array";
|
|
||||||
import * as RA from "fp-ts/ReadonlyArray";
|
|
||||||
import * as T from "fp-ts/Task";
|
|
||||||
import * as TE from "fp-ts/TaskEither";
|
|
||||||
import { flow, pipe } from "fp-ts/function";
|
|
||||||
import { hrtime } from "process";
|
import { hrtime } from "process";
|
||||||
|
import { flow, pipe } from "fp-ts/function";
|
||||||
|
import * as RA from "fp-ts/ReadonlyArray";
|
||||||
|
import * as A from "fp-ts/Array";
|
||||||
|
import * as TE from "fp-ts/TaskEither";
|
||||||
|
import * as T from "fp-ts/Task";
|
||||||
import {
|
import {
|
||||||
RequestRunnerResponse,
|
RequestRunnerResponse,
|
||||||
TestReport,
|
TestReport,
|
||||||
TestScriptParams,
|
TestScriptParams,
|
||||||
} from "../interfaces/response";
|
} from "../interfaces/response";
|
||||||
import { HoppCLIError, error } from "../types/errors";
|
import { error, HoppCLIError } from "../types/errors";
|
||||||
import { HoppEnvs } from "../types/request";
|
import { HoppEnvs } from "../types/request";
|
||||||
import { ExpectResult, TestMetrics, TestRunnerRes } from "../types/response";
|
import { ExpectResult, TestMetrics, TestRunnerRes } from "../types/response";
|
||||||
import { getDurationInSeconds } from "./getters";
|
import { getDurationInSeconds } from "./getters";
|
||||||
@@ -38,7 +36,7 @@ export const testRunner = (
|
|||||||
pipe(
|
pipe(
|
||||||
TE.of(testScriptData),
|
TE.of(testScriptData),
|
||||||
TE.chain(({ testScript, response, envs }) =>
|
TE.chain(({ testScript, response, envs }) =>
|
||||||
runTestScript(testScript, envs, response)
|
execTestScript(testScript, envs, response)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -69,7 +69,5 @@ module.exports = {
|
|||||||
"Do not use 'localStorage' directly. Please use the PersistenceService",
|
"Do not use 'localStorage' directly. Please use the PersistenceService",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
eqeqeq: 1,
|
|
||||||
"no-else-return": 1,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ module.exports = {
|
|||||||
printWidth: 80,
|
printWidth: 80,
|
||||||
useTabs: false,
|
useTabs: false,
|
||||||
tabWidth: 2,
|
tabWidth: 2,
|
||||||
|
plugins: ["prettier-plugin-tailwindcss"],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width=".88em" height="1em" viewBox="0 0 21 24"><path fill="currentColor" d="M12.731 2.751 17.666 5.6a2.138 2.138 0 1 1 2.07 3.548l-.015.003v5.7a2.14 2.14 0 1 1-2.098 3.502l-.002-.002-4.905 2.832a2.14 2.14 0 1 1-4.079.054l-.004.015-4.941-2.844a2.14 2.14 0 1 1-2.067-3.556l.015-.003V9.15a2.14 2.14 0 1 1 1.58-3.926l-.01-.005c.184.106.342.231.479.376l.001.001 4.938-2.85a2.14 2.14 0 1 1 4.096.021l.004-.015zm-.515.877a.766.766 0 0 1-.057.057l-.001.001 6.461 11.19c.026-.009.056-.016.082-.023V9.146a2.14 2.14 0 0 1-1.555-2.603l-.003.015.019-.072zm-3.015.059-.06-.06-4.946 2.852A2.137 2.137 0 0 1 2.749 9.12l-.015.004-.076.021v5.708l.084.023 6.461-11.19zm2.076.507a2.164 2.164 0 0 1-1.207-.004l.015.004-6.46 11.189c.286.276.496.629.597 1.026l.003.015h12.911c.102-.413.313-.768.599-1.043l.001-.001L11.28 4.194zm.986 16.227 4.917-2.838a1.748 1.748 0 0 1-.038-.142H4.222l-.021.083 4.939 2.852c.39-.403.936-.653 1.54-.653.626 0 1.189.268 1.581.696l.001.002z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width=".88em" height="1em" viewBox="0 0 21 24" class="iconify iconify--fontisto"><path fill="currentColor" d="M12.731 2.751 17.666 5.6a2.138 2.138 0 1 1 2.07 3.548l-.015.003v5.7a2.14 2.14 0 1 1-2.098 3.502l-.002-.002-4.905 2.832a2.14 2.14 0 1 1-4.079.054l-.004.015-4.941-2.844a2.14 2.14 0 1 1-2.067-3.556l.015-.003V9.15a2.14 2.14 0 1 1 1.58-3.926l-.01-.005c.184.106.342.231.479.376l.001.001 4.938-2.85a2.14 2.14 0 1 1 4.096.021l.004-.015zm-.515.877a.766.766 0 0 1-.057.057l-.001.001 6.461 11.19c.026-.009.056-.016.082-.023V9.146a2.14 2.14 0 0 1-1.555-2.603l-.003.015.019-.072zm-3.015.059-.06-.06-4.946 2.852A2.137 2.137 0 0 1 2.749 9.12l-.015.004-.076.021v5.708l.084.023 6.461-11.19zm2.076.507a2.164 2.164 0 0 1-1.207-.004l.015.004-6.46 11.189c.286.276.496.629.597 1.026l.003.015h12.911c.102-.413.313-.768.599-1.043l.001-.001L11.28 4.194zm.986 16.227 4.917-2.838a1.748 1.748 0 0 1-.038-.142H4.222l-.021.083 4.939 2.852c.39-.403.936-.653 1.54-.653.626 0 1.189.268 1.581.696l.001.002z"/></svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1017 B After Width: | Height: | Size: 1.0 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M10.133 1h4.409a.5.5 0 0 1 .5.5v4.422c0 .026-.035.033-.045.01l-.048-.112a9.095 9.095 0 0 0-4.825-4.776c-.023-.01-.016-.044.01-.044Zm-8.588.275h-.5v1h.5c7.027 0 12.229 5.199 12.229 12.226v.5h1v-.5c0-7.58-5.65-13.226-13.229-13.226Zm.034 4.22h-.5v1h.5c2.361 0 4.348.837 5.744 2.238 1.395 1.401 2.227 3.395 2.227 5.758v.5h1v-.5c0-2.604-.921-4.859-2.52-6.463-1.596-1.605-3.845-2.532-6.45-2.532Zm-.528 8.996v-4.423c0-.041.033-.074.074-.074a4.923 4.923 0 0 1 4.923 4.922.074.074 0 0 1-.074.074H1.551a.5.5 0 0 1-.5-.5Z" clip-rule="evenodd"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 684 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M9.277 2.084a.5.5 0 0 1 .185.607l-2.269 5.5a.5.5 0 0 1-.462.309H3.5a.5.5 0 0 1-.354-.854l5.5-5.5a.5.5 0 0 1 .631-.062ZM4.707 7.5h1.69l1.186-2.875L4.707 7.5Zm2.016 6.416a.5.5 0 0 1-.185-.607l2.269-5.5a.5.5 0 0 1 .462-.309H12.5a.5.5 0 0 1 .354.854l-5.5 5.5a.5.5 0 0 1-.631.062Zm4.57-5.416h-1.69l-1.186 2.875L11.293 8.5Z" clip-rule="evenodd"/><path fill="currentColor" fill-rule="evenodd" d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1 0A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" clip-rule="evenodd"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 633 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" d="M1 2h4.257a2.5 2.5 0 0 1 1.768.732L9.293 5 5 9.293 3.732 8.025A2.5 2.5 0 0 1 3 6.257V4H2v2.257a3.5 3.5 0 0 0 1.025 2.475L5 10.707l1.25-1.25 2.396 2.397.708-.708L6.957 8.75 8.75 6.957l2.396 2.397.708-.708L9.457 6.25 10.707 5 7.732 2.025A3.5 3.5 0 0 0 5.257 1H1v1ZM10.646 2.354l2.622 2.62A2.5 2.5 0 0 1 14 6.744V12h1V6.743a3.5 3.5 0 0 0-1.025-2.475l-2.621-2.622-.707.708ZM4.268 13.975l-2.622-2.621.708-.708 2.62 2.622A2.5 2.5 0 0 0 6.744 14H15v1H6.743a3.5 3.5 0 0 1-2.475-1.025Z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 610 B |
@@ -29,6 +29,14 @@
|
|||||||
@apply antialiased;
|
@apply antialiased;
|
||||||
accent-color: var(--accent-color);
|
accent-color: var(--accent-color);
|
||||||
font-variant-ligatures: common-ligatures;
|
font-variant-ligatures: common-ligatures;
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
--info-color: #ec4899;
|
||||||
|
--success-color: #10b981;
|
||||||
|
--blue-color: #3b82f6;
|
||||||
|
--warning-color: #f59e0b;
|
||||||
|
--cl-error-color: #ef4444;
|
||||||
|
--sv-error-color: #dc2626;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
@@ -57,7 +65,7 @@ input::placeholder,
|
|||||||
textarea::placeholder,
|
textarea::placeholder,
|
||||||
.cm-placeholder {
|
.cm-placeholder {
|
||||||
@apply text-secondary;
|
@apply text-secondary;
|
||||||
@apply opacity-50 #{!important};
|
@apply opacity-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
@@ -76,7 +84,7 @@ body {
|
|||||||
@apply font-medium;
|
@apply font-medium;
|
||||||
@apply select-none;
|
@apply select-none;
|
||||||
@apply overflow-x-hidden;
|
@apply overflow-x-hidden;
|
||||||
@apply leading-body #{!important};
|
@apply leading-body;
|
||||||
animation: fade 300ms forwards;
|
animation: fade 300ms forwards;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
-webkit-touch-callout: none;
|
-webkit-touch-callout: none;
|
||||||
@@ -158,7 +166,7 @@ a {
|
|||||||
@apply shadow-none #{!important};
|
@apply shadow-none #{!important};
|
||||||
@apply fixed;
|
@apply fixed;
|
||||||
@apply inline-flex;
|
@apply inline-flex;
|
||||||
@apply -mt-7;
|
@apply -mt-8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +182,7 @@ a {
|
|||||||
@apply font-semibold;
|
@apply font-semibold;
|
||||||
@apply px-2 py-1;
|
@apply px-2 py-1;
|
||||||
@apply truncate;
|
@apply truncate;
|
||||||
@apply leading-body;
|
@apply leading-normal;
|
||||||
@apply items-center;
|
@apply items-center;
|
||||||
|
|
||||||
kbd {
|
kbd {
|
||||||
@@ -221,7 +229,7 @@ a {
|
|||||||
@apply overflow-y-auto;
|
@apply overflow-y-auto;
|
||||||
@apply text-body text-secondary;
|
@apply text-body text-secondary;
|
||||||
@apply p-2;
|
@apply p-2;
|
||||||
@apply leading-body;
|
@apply leading-normal;
|
||||||
@apply focus:outline-none;
|
@apply focus:outline-none;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
|
|
||||||
@@ -253,7 +261,7 @@ a {
|
|||||||
|
|
||||||
hr {
|
hr {
|
||||||
@apply border-b border-dividerLight;
|
@apply border-b border-dividerLight;
|
||||||
@apply my-2 #{!important};
|
@apply my-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
@@ -342,33 +350,48 @@ pre.ace_editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.select-wrapper {
|
||||||
|
@apply flex flex-1;
|
||||||
|
@apply relative;
|
||||||
|
@apply after:absolute;
|
||||||
|
@apply after:flex;
|
||||||
|
@apply after:inset-y-0;
|
||||||
|
@apply after:items-center;
|
||||||
|
@apply after:justify-center;
|
||||||
|
@apply after:pointer-events-none;
|
||||||
|
@apply after:font-icon;
|
||||||
|
@apply after:text-current;
|
||||||
|
@apply after:right-3;
|
||||||
|
@apply after:content-["\e5cf"];
|
||||||
|
@apply after:text-lg;
|
||||||
|
}
|
||||||
|
|
||||||
.info-response {
|
.info-response {
|
||||||
color: var(--status-info-color);
|
color: var(--info-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-response {
|
.success-response {
|
||||||
color: var(--status-success-color);
|
color: var(--success-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.redirect-response {
|
.redir-response {
|
||||||
color: var(--status-redirect-color);
|
color: var(--warning-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.critical-error-response {
|
.cl-error-response {
|
||||||
color: var(--status-critical-error-color);
|
color: var(--cl-error-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-error-response {
|
.sv-error-response {
|
||||||
color: var(--status-server-error-color);
|
color: var(--sv-error-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.missing-data-response {
|
.missing-data-response {
|
||||||
color: var(--status-missing-data-color);
|
@apply text-secondaryLight;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toasted-container {
|
.toasted-container {
|
||||||
@apply max-w-md;
|
@apply max-w-md;
|
||||||
@apply z-[10000];
|
|
||||||
|
|
||||||
.toasted {
|
.toasted {
|
||||||
&.toasted-primary {
|
&.toasted-primary {
|
||||||
@@ -514,13 +537,12 @@ pre.ace_editor {
|
|||||||
@apply inline-flex;
|
@apply inline-flex;
|
||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
@apply text-tiny;
|
@apply text-tiny;
|
||||||
@apply bg-dividerLight;
|
@apply bg-divider;
|
||||||
@apply rounded;
|
@apply rounded;
|
||||||
@apply ml-2;
|
@apply ml-2;
|
||||||
@apply px-0.5;
|
@apply px-1;
|
||||||
@apply min-w-[1rem];
|
@apply min-w-5;
|
||||||
@apply min-h-[1rem];
|
@apply min-h-5;
|
||||||
@apply leading-none;
|
|
||||||
@apply items-center;
|
@apply items-center;
|
||||||
@apply justify-center;
|
@apply justify-center;
|
||||||
@apply border border-dividerDark;
|
@apply border border-dividerDark;
|
||||||
|
|||||||
@@ -1,89 +1,89 @@
|
|||||||
@mixin green-theme {
|
@mixin green-theme {
|
||||||
--accent-color: theme("colors.emerald.500");
|
--accent-color: #10b981;
|
||||||
--accent-light-color: theme("colors.emerald.400");
|
--accent-light-color: #34d399;
|
||||||
--accent-dark-color: theme("colors.emerald.600");
|
--accent-dark-color: #059669;
|
||||||
--accent-contrast-color: theme("colors.white");
|
--accent-contrast-color: #fff;
|
||||||
--gradient-from-color: theme("colors.emerald.400");
|
--gradient-from-color: #a7f3d0;
|
||||||
--gradient-via-color: theme("colors.emerald.500");
|
--gradient-via-color: #34d399;
|
||||||
--gradient-to-color: theme("colors.emerald.600");
|
--gradient-to-color: #059669;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin teal-theme {
|
@mixin teal-theme {
|
||||||
--accent-color: theme("colors.teal.500");
|
--accent-color: #14b8a6;
|
||||||
--accent-light-color: theme("colors.teal.400");
|
--accent-light-color: #2dd4bf;
|
||||||
--accent-dark-color: theme("colors.teal.600");
|
--accent-dark-color: #0d9488;
|
||||||
--accent-contrast-color: theme("colors.white");
|
--accent-contrast-color: #fff;
|
||||||
--gradient-from-color: theme("colors.teal.400");
|
--gradient-from-color: #99f6e4;
|
||||||
--gradient-via-color: theme("colors.teal.500");
|
--gradient-via-color: #2dd4bf;
|
||||||
--gradient-to-color: theme("colors.teal.600");
|
--gradient-to-color: #0d9488;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin blue-theme {
|
@mixin blue-theme {
|
||||||
--accent-color: theme("colors.blue.500");
|
--accent-color: #3b82f6;
|
||||||
--accent-light-color: theme("colors.blue.400");
|
--accent-light-color: #60a5fa;
|
||||||
--accent-dark-color: theme("colors.blue.600");
|
--accent-dark-color: #2563eb;
|
||||||
--accent-contrast-color: theme("colors.white");
|
--accent-contrast-color: #fff;
|
||||||
--gradient-from-color: theme("colors.blue.400");
|
--gradient-from-color: #bfdbfe;
|
||||||
--gradient-via-color: theme("colors.blue.500");
|
--gradient-via-color: #60a5fa;
|
||||||
--gradient-to-color: theme("colors.blue.600");
|
--gradient-to-color: #2563eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin indigo-theme {
|
@mixin indigo-theme {
|
||||||
--accent-color: theme("colors.indigo.500");
|
--accent-color: #6366f1;
|
||||||
--accent-light-color: theme("colors.indigo.400");
|
--accent-light-color: #818cf8;
|
||||||
--accent-dark-color: theme("colors.indigo.600");
|
--accent-dark-color: #4f46e5;
|
||||||
--accent-contrast-color: theme("colors.white");
|
--accent-contrast-color: #fff;
|
||||||
--gradient-from-color: theme("colors.indigo.400");
|
--gradient-from-color: #c7d2fe;
|
||||||
--gradient-via-color: theme("colors.indigo.500");
|
--gradient-via-color: #818cf8;
|
||||||
--gradient-to-color: theme("colors.indigo.600");
|
--gradient-to-color: #4f46e5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin purple-theme {
|
@mixin purple-theme {
|
||||||
--accent-color: theme("colors.purple.500");
|
--accent-color: #8b5cf6;
|
||||||
--accent-light-color: theme("colors.purple.400");
|
--accent-light-color: #a78bfa;
|
||||||
--accent-dark-color: theme("colors.purple.600");
|
--accent-dark-color: #7c3aed;
|
||||||
--accent-contrast-color: theme("colors.white");
|
--accent-contrast-color: #fff;
|
||||||
--gradient-from-color: theme("colors.purple.400");
|
--gradient-from-color: #ddd6fe;
|
||||||
--gradient-via-color: theme("colors.purple.500");
|
--gradient-via-color: #a78bfa;
|
||||||
--gradient-to-color: theme("colors.purple.600");
|
--gradient-to-color: #7c3aed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin yellow-theme {
|
@mixin yellow-theme {
|
||||||
--accent-color: theme("colors.amber.500");
|
--accent-color: #f59e0b;
|
||||||
--accent-light-color: theme("colors.amber.400");
|
--accent-light-color: #fbbf24;
|
||||||
--accent-dark-color: theme("colors.amber.600");
|
--accent-dark-color: #d97706;
|
||||||
--accent-contrast-color: theme("colors.white");
|
--accent-contrast-color: #fff;
|
||||||
--gradient-from-color: theme("colors.amber.400");
|
--gradient-from-color: #fde68a;
|
||||||
--gradient-via-color: theme("colors.amber.500");
|
--gradient-via-color: #fbbf24;
|
||||||
--gradient-to-color: theme("colors.amber.600");
|
--gradient-to-color: #d97706;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin orange-theme {
|
@mixin orange-theme {
|
||||||
--accent-color: theme("colors.orange.500");
|
--accent-color: #f97316;
|
||||||
--accent-light-color: theme("colors.orange.400");
|
--accent-light-color: #fb923c;
|
||||||
--accent-dark-color: theme("colors.orange.600");
|
--accent-dark-color: #ea580c;
|
||||||
--accent-contrast-color: theme("colors.white");
|
--accent-contrast-color: #fff;
|
||||||
--gradient-from-color: theme("colors.orange.400");
|
--gradient-from-color: #fed7aa;
|
||||||
--gradient-via-color: theme("colors.orange.500");
|
--gradient-via-color: #fb923c;
|
||||||
--gradient-to-color: theme("colors.orange.600");
|
--gradient-to-color: #ea580c;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin red-theme {
|
@mixin red-theme {
|
||||||
--accent-color: theme("colors.red.500");
|
--accent-color: #ef4444;
|
||||||
--accent-light-color: theme("colors.red.400");
|
--accent-light-color: #f87171;
|
||||||
--accent-dark-color: theme("colors.red.600");
|
--accent-dark-color: #dc2626;
|
||||||
--accent-contrast-color: theme("colors.white");
|
--accent-contrast-color: #fff;
|
||||||
--gradient-from-color: theme("colors.red.400");
|
--gradient-from-color: #fecaca;
|
||||||
--gradient-via-color: theme("colors.red.500");
|
--gradient-via-color: #f87171;
|
||||||
--gradient-to-color: theme("colors.red.600");
|
--gradient-to-color: #dc2626;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin pink-theme {
|
@mixin pink-theme {
|
||||||
--accent-color: theme("colors.pink.500");
|
--accent-color: #ec4899;
|
||||||
--accent-light-color: theme("colors.pink.400");
|
--accent-light-color: #f472b6;
|
||||||
--accent-dark-color: theme("colors.pink.600");
|
--accent-dark-color: #db2777;
|
||||||
--accent-contrast-color: theme("colors.white");
|
--accent-contrast-color: #fff;
|
||||||
--gradient-from-color: theme("colors.pink.400");
|
--gradient-from-color: #fbcfe8;
|
||||||
--gradient-via-color: theme("colors.pink.500");
|
--gradient-via-color: #f472b6;
|
||||||
--gradient-to-color: theme("colors.pink.600");
|
--gradient-to-color: #db2777;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,141 +1,81 @@
|
|||||||
@mixin base-theme {
|
@mixin base-theme {
|
||||||
--font-sans: "Inter Variable", sans-serif;
|
--font-sans: "Inter Variable", sans-serif;
|
||||||
|
--font-icon: "Material Symbols Rounded Variable";
|
||||||
--font-mono: "Roboto Mono Variable", monospace;
|
--font-mono: "Roboto Mono Variable", monospace;
|
||||||
--font-size-body: 0.75rem;
|
--font-size-body: 0.75rem;
|
||||||
--font-size-tiny: 0.625rem;
|
--font-size-tiny: 0.688rem;
|
||||||
--line-height-body: 1rem;
|
--line-height-body: 1rem;
|
||||||
--upper-primary-sticky-fold: 4.125rem;
|
--upper-primary-sticky-fold: 4.125rem;
|
||||||
--upper-secondary-sticky-fold: 6.188rem;
|
--upper-secondary-sticky-fold: 6.188rem;
|
||||||
--upper-tertiary-sticky-fold: 8.25rem;
|
--upper-tertiary-sticky-fold: 8.25rem;
|
||||||
--upper-fourth-sticky-fold: 10.2rem;
|
--upper-fourth-sticky-fold: 10.2rem;
|
||||||
--upper-mobile-primary-sticky-fold: 6.75rem;
|
--upper-mobile-primary-sticky-fold: 6.625rem;
|
||||||
--upper-mobile-secondary-sticky-fold: 8.813rem;
|
--upper-mobile-secondary-sticky-fold: 8.688rem;
|
||||||
--upper-mobile-sticky-fold: 10.875rem;
|
--upper-mobile-sticky-fold: 10.75rem;
|
||||||
--upper-mobile-tertiary-sticky-fold: 8.25rem;
|
--upper-mobile-tertiary-sticky-fold: 8.25rem;
|
||||||
--lower-primary-sticky-fold: 3rem;
|
--lower-primary-sticky-fold: 3rem;
|
||||||
--lower-secondary-sticky-fold: 5.063rem;
|
--lower-secondary-sticky-fold: 5.063rem;
|
||||||
--lower-tertiary-sticky-fold: 7.125rem;
|
--lower-tertiary-sticky-fold: 7.125rem;
|
||||||
--lower-fourth-sticky-fold: 9.188rem;
|
--lower-fourth-sticky-fold: 9.188rem;
|
||||||
--sidebar-primary-sticky-fold: 2rem;
|
--sidebar-primary-sticky-fold: 2rem;
|
||||||
--properties-primary-sticky-fold: 2.05rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin light-theme {
|
|
||||||
--primary-color: theme("colors.white");
|
|
||||||
--primary-light-color: theme("colors.gray.50");
|
|
||||||
--primary-dark-color: theme("colors.gray.100");
|
|
||||||
--primary-contrast-color: #fdfdfd;
|
|
||||||
|
|
||||||
--secondary-color: theme("colors.gray.500");
|
|
||||||
--secondary-light-color: theme("colors.gray.400");
|
|
||||||
--secondary-dark-color: theme("colors.gray.900");
|
|
||||||
|
|
||||||
--divider-color: theme("colors.gray.100");
|
|
||||||
--divider-light-color: theme("colors.gray.100");
|
|
||||||
--divider-dark-color: theme("colors.gray.300");
|
|
||||||
|
|
||||||
--banner-info-color: theme("colors.stone.100");
|
|
||||||
--banner-warning-color: theme("colors.yellow.100");
|
|
||||||
--banner-error-color: theme("colors.red.100");
|
|
||||||
|
|
||||||
--tooltip-color: theme("colors.neutral.800");
|
|
||||||
--popover-color: theme("colors.white");
|
|
||||||
|
|
||||||
--method-get-color: theme("colors.green.500");
|
|
||||||
--method-post-color: theme("colors.amber.500");
|
|
||||||
--method-put-color: theme("colors.blue.500");
|
|
||||||
--method-patch-color: theme("colors.purple.500");
|
|
||||||
--method-delete-color: theme("colors.red.500");
|
|
||||||
--method-head-color: theme("colors.lime.500");
|
|
||||||
--method-options-color: theme("colors.pink.500");
|
|
||||||
--method-default-color: theme("colors.gray.500");
|
|
||||||
|
|
||||||
--status-info-color: theme("colors.blue.500");
|
|
||||||
--status-success-color: theme("colors.green.500");
|
|
||||||
--status-redirect-color: theme("colors.amber.500");
|
|
||||||
--status-critical-error-color: theme("colors.red.500");
|
|
||||||
--status-server-error-color: theme("colors.rose.500");
|
|
||||||
--status-missing-data-color: theme("colors.slate.500");
|
|
||||||
|
|
||||||
--editor-theme: "textmate";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin dark-theme {
|
@mixin dark-theme {
|
||||||
--primary-color: #181818;
|
--primary-color: #181818;
|
||||||
--primary-light-color: #1c1c1e;
|
--primary-light-color: #1c1c1e;
|
||||||
--primary-dark-color: theme("colors.neutral.800");
|
--primary-dark-color: #262626;
|
||||||
--primary-contrast-color: theme("colors.neutral.900");
|
--primary-contrast-color: #171717;
|
||||||
|
|
||||||
--secondary-color: theme("colors.neutral.400");
|
--secondary-color: #a3a3a3;
|
||||||
--secondary-light-color: theme("colors.neutral.500");
|
--secondary-light-color: #737373;
|
||||||
--secondary-dark-color: theme("colors.zinc.50");
|
--secondary-dark-color: #fafafa;
|
||||||
|
|
||||||
--divider-color: #1f1f1f;
|
--divider-color: #262626;
|
||||||
--divider-light-color: #1f1f1f;
|
--divider-light-color: #1f1f1f;
|
||||||
--divider-dark-color: theme("colors.zinc.800");
|
--divider-dark-color: #2d2d2d;
|
||||||
|
|
||||||
--banner-info-color: theme("colors.stone.800");
|
--error-color: #292524;
|
||||||
--banner-warning-color: theme("colors.yellow.800");
|
--tooltip-color: #f5f5f5;
|
||||||
--banner-error-color: theme("colors.red.800");
|
|
||||||
|
|
||||||
--tooltip-color: theme("colors.neutral.100");
|
|
||||||
--popover-color: #1b1b1b;
|
--popover-color: #1b1b1b;
|
||||||
|
|
||||||
--method-get-color: theme("colors.emerald.500");
|
|
||||||
--method-post-color: theme("colors.yellow.500");
|
|
||||||
--method-put-color: theme("colors.sky.500");
|
|
||||||
--method-patch-color: theme("colors.violet.500");
|
|
||||||
--method-delete-color: theme("colors.rose.500");
|
|
||||||
--method-head-color: theme("colors.teal.500");
|
|
||||||
--method-options-color: theme("colors.indigo.500");
|
|
||||||
--method-default-color: theme("colors.neutral.500");
|
|
||||||
|
|
||||||
--status-info-color: theme("colors.blue.500");
|
|
||||||
--status-success-color: theme("colors.green.500");
|
|
||||||
--status-redirect-color: theme("colors.amber.500");
|
|
||||||
--status-critical-error-color: theme("colors.red.500");
|
|
||||||
--status-server-error-color: theme("colors.rose.500");
|
|
||||||
--status-missing-data-color: theme("colors.slate.500");
|
|
||||||
|
|
||||||
--editor-theme: "merbivore_soft";
|
--editor-theme: "merbivore_soft";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin light-theme {
|
||||||
|
--primary-color: #ffffff;
|
||||||
|
--primary-light-color: #f9fafb;
|
||||||
|
--primary-dark-color: #f3f4f6;
|
||||||
|
--primary-contrast-color: #fdfdfd;
|
||||||
|
|
||||||
|
--secondary-color: #6b7280;
|
||||||
|
--secondary-light-color: #9ca3af;
|
||||||
|
--secondary-dark-color: #111827;
|
||||||
|
|
||||||
|
--divider-color: #f3f4f6;
|
||||||
|
--divider-light-color: #f3f4f6;
|
||||||
|
--divider-dark-color: #d1d5db;
|
||||||
|
|
||||||
|
--error-color: #fef3c7;
|
||||||
|
--tooltip-color: #262626;
|
||||||
|
--popover-color: #ffffff;
|
||||||
|
--editor-theme: "textmate";
|
||||||
|
}
|
||||||
|
|
||||||
@mixin black-theme {
|
@mixin black-theme {
|
||||||
--primary-color: #0f0f0f;
|
--primary-color: #0f0f0f;
|
||||||
--primary-light-color: theme("colors.neutral.900");
|
--primary-light-color: #171717;
|
||||||
--primary-dark-color: #181818;
|
--primary-dark-color: #181818;
|
||||||
--primary-contrast-color: #0f0f0f;
|
--primary-contrast-color: #0f0f0f;
|
||||||
|
|
||||||
--secondary-color: theme("colors.neutral.400");
|
--secondary-color: #a3a3a3;
|
||||||
--secondary-light-color: theme("colors.neutral.500");
|
--secondary-light-color: #737373;
|
||||||
--secondary-dark-color: theme("colors.neutral.50");
|
--secondary-dark-color: #f5f5f5;
|
||||||
|
|
||||||
--divider-color: theme("colors.neutral.900");
|
--divider-color: #1c1c1e;
|
||||||
--divider-light-color: theme("colors.neutral.900");
|
--divider-light-color: #181818;
|
||||||
--divider-dark-color: theme("colors.zinc.800");
|
--divider-dark-color: #323232;
|
||||||
|
|
||||||
--banner-info-color: theme("colors.stone.900");
|
|
||||||
--banner-warning-color: theme("colors.yellow.900");
|
|
||||||
--banner-error-color: theme("colors.red.900");
|
|
||||||
|
|
||||||
--tooltip-color: theme("colors.neutral.100");
|
|
||||||
--popover-color: theme("colors.stone.950");
|
|
||||||
|
|
||||||
--method-get-color: theme("colors.emerald.500");
|
|
||||||
--method-post-color: theme("colors.yellow.500");
|
|
||||||
--method-put-color: theme("colors.sky.500");
|
|
||||||
--method-patch-color: theme("colors.violet.500");
|
|
||||||
--method-delete-color: theme("colors.rose.500");
|
|
||||||
--method-head-color: theme("colors.teal.500");
|
|
||||||
--method-options-color: theme("colors.indigo.500");
|
|
||||||
--method-default-color: theme("colors.zinc.500");
|
|
||||||
|
|
||||||
--status-info-color: theme("colors.blue.500");
|
|
||||||
--status-success-color: theme("colors.green.500");
|
|
||||||
--status-redirect-color: theme("colors.amber.500");
|
|
||||||
--status-critical-error-color: theme("colors.red.500");
|
|
||||||
--status-server-error-color: theme("colors.rose.500");
|
|
||||||
--status-missing-data-color: theme("colors.slate.500");
|
|
||||||
|
|
||||||
|
--error-color: #1c1917;
|
||||||
|
--tooltip-color: #f5f5f5;
|
||||||
|
--popover-color: #0f0f0f;
|
||||||
--editor-theme: "twilight";
|
--editor-theme: "twilight";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
@mixin light-editor-theme {
|
@mixin dark-editor-theme {
|
||||||
--editor-type-color: theme("colors.violet.600");
|
--editor-type-color: #a78bfa;
|
||||||
--editor-name-color: theme("colors.red.600");
|
--editor-name-color: #60a5fa;
|
||||||
--editor-operator-color: theme("colors.indigo.600");
|
--editor-operator-color: #818cf8;
|
||||||
--editor-invalid-color: theme("colors.red.600");
|
--editor-invalid-color: #f87171;
|
||||||
--editor-separator-color: theme("colors.gray.600");
|
--editor-separator-color: #9ca3af;
|
||||||
--editor-meta-color: theme("colors.gray.600");
|
--editor-meta-color: #9ca3af;
|
||||||
--editor-variable-color: theme("colors.emerald.600");
|
--editor-variable-color: #34d399;
|
||||||
--editor-link-color: theme("colors.cyan.600");
|
--editor-link-color: #22d3ee;
|
||||||
--editor-process-color: theme("colors.blue.600");
|
--editor-process-color: #e879f9;
|
||||||
--editor-constant-color: theme("colors.fuchsia.600");
|
--editor-constant-color: #a78bfa;
|
||||||
--editor-keyword-color: theme("colors.pink.600");
|
--editor-keyword-color: #f472b6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin dark-editor-theme {
|
@mixin light-editor-theme {
|
||||||
--editor-type-color: theme("colors.violet.400");
|
--editor-type-color: #7c3aed;
|
||||||
--editor-name-color: theme("colors.blue.400");
|
--editor-name-color: #dc2626;
|
||||||
--editor-operator-color: theme("colors.indigo.400");
|
--editor-operator-color: #4f46e5;
|
||||||
--editor-invalid-color: theme("colors.red.400");
|
--editor-invalid-color: #dc2626;
|
||||||
--editor-separator-color: theme("colors.gray.400");
|
--editor-separator-color: #4b5563;
|
||||||
--editor-meta-color: theme("colors.gray.400");
|
--editor-meta-color: #4b5563;
|
||||||
--editor-variable-color: theme("colors.emerald.400");
|
--editor-variable-color: #059669;
|
||||||
--editor-link-color: theme("colors.cyan.400");
|
--editor-link-color: #0891b2;
|
||||||
--editor-process-color: theme("colors.fuchsia.400");
|
--editor-process-color: #2563eb;
|
||||||
--editor-constant-color: theme("colors.violet.400");
|
--editor-constant-color: #c026d3;
|
||||||
--editor-keyword-color: theme("colors.pink.400");
|
--editor-keyword-color: #db2777;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin black-editor-theme {
|
@mixin black-editor-theme {
|
||||||
--editor-type-color: theme("colors.violet.400");
|
--editor-type-color: #a78bfa;
|
||||||
--editor-name-color: theme("colors.fuchsia.400");
|
--editor-name-color: #e879f9;
|
||||||
--editor-operator-color: theme("colors.indigo.400");
|
--editor-operator-color: #818cf8;
|
||||||
--editor-invalid-color: theme("colors.red.400");
|
--editor-invalid-color: #f87171;
|
||||||
--editor-separator-color: theme("colors.gray.400");
|
--editor-separator-color: #9ca3af;
|
||||||
--editor-meta-color: theme("colors.gray.400");
|
--editor-meta-color: #9ca3af;
|
||||||
--editor-variable-color: theme("colors.emerald.400");
|
--editor-variable-color: #34d399;
|
||||||
--editor-link-color: theme("colors.cyan.400");
|
--editor-link-color: #22d3ee;
|
||||||
--editor-process-color: theme("colors.violet.400");
|
--editor-process-color: #a78bfa;
|
||||||
--editor-constant-color: theme("colors.blue.400");
|
--editor-constant-color: #60a5fa;
|
||||||
--editor-keyword-color: theme("colors.pink.400");
|
--editor-keyword-color: #f472b6;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
"connect": "Connect",
|
"connect": "Connect",
|
||||||
"connecting": "Connecting",
|
"connecting": "Connecting",
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
"create": "Create",
|
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"disconnect": "Disconnect",
|
"disconnect": "Disconnect",
|
||||||
"dismiss": "Dismiss",
|
"dismiss": "Dismiss",
|
||||||
@@ -33,7 +32,6 @@
|
|||||||
"open_workspace": "Open workspace",
|
"open_workspace": "Open workspace",
|
||||||
"paste": "Paste",
|
"paste": "Paste",
|
||||||
"prettify": "Prettify",
|
"prettify": "Prettify",
|
||||||
"properties":"Properties",
|
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"restore": "Restore",
|
"restore": "Restore",
|
||||||
@@ -42,7 +40,6 @@
|
|||||||
"scroll_to_top": "Scroll to top",
|
"scroll_to_top": "Scroll to top",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"send": "Send",
|
"send": "Send",
|
||||||
"share": "Share",
|
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
"starting": "Starting",
|
"starting": "Starting",
|
||||||
"stop": "Stop",
|
"stop": "Stop",
|
||||||
@@ -81,7 +78,6 @@
|
|||||||
"contact_us": "Contact us",
|
"contact_us": "Contact us",
|
||||||
"cookies": "Cookies",
|
"cookies": "Cookies",
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
"copy_interface_type": "Copy interface type",
|
|
||||||
"copy_user_id": "Copy User Auth Token",
|
"copy_user_id": "Copy User Auth Token",
|
||||||
"developer_option": "Developer options",
|
"developer_option": "Developer options",
|
||||||
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
|
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
|
||||||
@@ -97,7 +93,6 @@
|
|||||||
"keyboard_shortcuts": "Keyboard shortcuts",
|
"keyboard_shortcuts": "Keyboard shortcuts",
|
||||||
"name": "Hoppscotch",
|
"name": "Hoppscotch",
|
||||||
"new_version_found": "New version found. Refresh to update.",
|
"new_version_found": "New version found. Refresh to update.",
|
||||||
"open_in_hoppscotch": "Open in Hoppscotch",
|
|
||||||
"options": "Options",
|
"options": "Options",
|
||||||
"proxy_privacy_policy": "Proxy privacy policy",
|
"proxy_privacy_policy": "Proxy privacy policy",
|
||||||
"reload": "Reload",
|
"reload": "Reload",
|
||||||
@@ -173,8 +168,6 @@
|
|||||||
"name_length_insufficient": "Collection name should be at least 3 characters long",
|
"name_length_insufficient": "Collection name should be at least 3 characters long",
|
||||||
"new": "New Collection",
|
"new": "New Collection",
|
||||||
"order_changed": "Collection Order Updated",
|
"order_changed": "Collection Order Updated",
|
||||||
"properties":"Colection Properties",
|
|
||||||
"properties_updated": "Collection Properties Updated",
|
|
||||||
"renamed": "Collection renamed",
|
"renamed": "Collection renamed",
|
||||||
"request_in_use": "Request in use",
|
"request_in_use": "Request in use",
|
||||||
"save_as": "Save as",
|
"save_as": "Save as",
|
||||||
@@ -194,7 +187,6 @@
|
|||||||
"remove_folder": "Are you sure you want to permanently delete this folder?",
|
"remove_folder": "Are you sure you want to permanently delete this folder?",
|
||||||
"remove_history": "Are you sure you want to permanently delete all history?",
|
"remove_history": "Are you sure you want to permanently delete all history?",
|
||||||
"remove_request": "Are you sure you want to permanently delete this request?",
|
"remove_request": "Are you sure you want to permanently delete this request?",
|
||||||
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
|
|
||||||
"remove_team": "Are you sure you want to delete this team?",
|
"remove_team": "Are you sure you want to delete this team?",
|
||||||
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
|
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
|
||||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
||||||
@@ -236,8 +228,7 @@
|
|||||||
"profile": "Login to view your profile",
|
"profile": "Login to view your profile",
|
||||||
"protocols": "Protocols are empty",
|
"protocols": "Protocols are empty",
|
||||||
"schema": "Connect to a GraphQL endpoint to view schema",
|
"schema": "Connect to a GraphQL endpoint to view schema",
|
||||||
"shared_requests_logout": "Login to view your shared requests or create a new one",
|
"shortcodes": "Shortcodes are empty",
|
||||||
"shared_requests": "Shared requests are empty",
|
|
||||||
"subscription": "Subscriptions are empty",
|
"subscription": "Subscriptions are empty",
|
||||||
"team_name": "Team name empty",
|
"team_name": "Team name empty",
|
||||||
"teams": "You don't belong to any teams",
|
"teams": "You don't belong to any teams",
|
||||||
@@ -277,9 +268,6 @@
|
|||||||
"variable": "Variable",
|
"variable": "Variable",
|
||||||
"variable_list": "Variable List"
|
"variable_list": "Variable List"
|
||||||
},
|
},
|
||||||
"graphql_collections": {
|
|
||||||
"title": "GraphQL Collections"
|
|
||||||
},
|
|
||||||
"error": {
|
"error": {
|
||||||
"browser_support_sse": "This browser doesn't seems to have Server Sent Events support.",
|
"browser_support_sse": "This browser doesn't seems to have Server Sent Events support.",
|
||||||
"check_console_details": "Check console log for details.",
|
"check_console_details": "Check console log for details.",
|
||||||
@@ -315,8 +303,7 @@
|
|||||||
"create_secret_gist": "Create secret Gist",
|
"create_secret_gist": "Create secret Gist",
|
||||||
"gist_created": "Gist created",
|
"gist_created": "Gist created",
|
||||||
"require_github": "Login with GitHub to create secret gist",
|
"require_github": "Login with GitHub to create secret gist",
|
||||||
"title": "Export",
|
"title": "Export"
|
||||||
"failed": "Something went wrong while exporting"
|
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"all": "All",
|
"all": "All",
|
||||||
@@ -345,20 +332,22 @@
|
|||||||
"url": "URL"
|
"url": "URL"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"install_pwa": "Install app",
|
"install_pwa": "Add to Home Screen",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"save_workspace": "Save My Workspace"
|
"save_workspace": "Save My Workspace",
|
||||||
|
"download_app": "Download app",
|
||||||
|
"menu": "Menu",
|
||||||
|
"go_back": "Go back",
|
||||||
|
"go_forward": "Go forward"
|
||||||
},
|
},
|
||||||
"helpers": {
|
"helpers": {
|
||||||
"authorization": "The authorization header will be automatically generated when you send the request.",
|
"authorization": "The authorization header will be automatically generated when you send the request.",
|
||||||
"generate_documentation_first": "Generate documentation first",
|
"generate_documentation_first": "Generate documentation first",
|
||||||
"network_fail": "Unable to reach the API endpoint. Check your network connection or select a different Interceptor and try again.",
|
"network_fail": "Unable to reach the API endpoint. Check your network connection or select a different Interceptor and try again.",
|
||||||
"offline": "You're using Hoppscotch offline. Updates will sync when you're online, based on workspace settings.",
|
"offline": "You seem to be offline. Data in this workspace might not be up to date.",
|
||||||
"offline_short": "You're using Hoppscotch offline.",
|
"offline_short": "You seem to be offline.",
|
||||||
"post_request_tests": "Test scripts are written in JavaScript, and are run after the response is received.",
|
"post_request_tests": "Test scripts are written in JavaScript, and are run after the response is received.",
|
||||||
"pre_request_script": "Pre-request scripts are written in JavaScript, and are run before the request is sent.",
|
"pre_request_script": "Pre-request scripts are written in JavaScript, and are run before the request is sent.",
|
||||||
"collection_properties_authorization": " This authorization will be set for every request in this collection.",
|
|
||||||
"collection_properties_header": "This header will be set for every request in this collection.",
|
|
||||||
"script_fail": "It seems there is a glitch in the pre-request script. Check the error below and fix the script accordingly.",
|
"script_fail": "It seems there is a glitch in the pre-request script. Check the error below and fix the script accordingly.",
|
||||||
"test_script_fail": "There seems to be an error with test script. Please fix the errors and run tests again",
|
"test_script_fail": "There seems to be an error with test script. Please fix the errors and run tests again",
|
||||||
"tests": "Write a test script to automate debugging."
|
"tests": "Write a test script to automate debugging."
|
||||||
@@ -385,7 +374,6 @@
|
|||||||
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
|
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
|
||||||
"from_postman": "Import from Postman",
|
"from_postman": "Import from Postman",
|
||||||
"from_postman_description": "Import from Postman collection",
|
"from_postman_description": "Import from Postman collection",
|
||||||
"from_file": "Import from File",
|
|
||||||
"from_url": "Import from URL",
|
"from_url": "Import from URL",
|
||||||
"gist_url": "Enter Gist URL",
|
"gist_url": "Enter Gist URL",
|
||||||
"import_from_url_invalid_fetch": "Couldn't get data from the url",
|
"import_from_url_invalid_fetch": "Couldn't get data from the url",
|
||||||
@@ -393,15 +381,7 @@
|
|||||||
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
|
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
|
||||||
"import_from_url_success": "Collections Imported",
|
"import_from_url_success": "Collections Imported",
|
||||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||||
"title": "Import",
|
"title": "Import"
|
||||||
"hoppscotch_environment": "Hoppscotch Environment",
|
|
||||||
"hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
|
|
||||||
"postman_environment": "Postman Environment",
|
|
||||||
"postman_environment_description": "Import Postman Environment from a JSON file",
|
|
||||||
"insomnia_environment_description": "Import Insomnia Environment from a JSON/YAML file",
|
|
||||||
"environments_from_gist": "Import From Gist",
|
|
||||||
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
|
|
||||||
"gql_collections_from_gist_description": "Import GraphQL Collections From Gist"
|
|
||||||
},
|
},
|
||||||
"inspections": {
|
"inspections": {
|
||||||
"description": "Inspect possible errors",
|
"description": "Inspect possible errors",
|
||||||
@@ -438,9 +418,7 @@
|
|||||||
"close_unsaved_tab": "You have unsaved changes",
|
"close_unsaved_tab": "You have unsaved changes",
|
||||||
"collections": "Collections",
|
"collections": "Collections",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"customize_request": "Customize Request",
|
|
||||||
"edit_request": "Edit Request",
|
"edit_request": "Edit Request",
|
||||||
"share_request": "Share Request",
|
|
||||||
"import_export": "Import / Export"
|
"import_export": "Import / Export"
|
||||||
},
|
},
|
||||||
"mqtt": {
|
"mqtt": {
|
||||||
@@ -516,6 +494,7 @@
|
|||||||
"structured": "Structured",
|
"structured": "Structured",
|
||||||
"text": "Text"
|
"text": "Text"
|
||||||
},
|
},
|
||||||
|
"copy_link": "Copy link",
|
||||||
"different_collection": "Cannot reorder requests from different collections",
|
"different_collection": "Cannot reorder requests from different collections",
|
||||||
"duplicated": "Request duplicated",
|
"duplicated": "Request duplicated",
|
||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
@@ -548,7 +527,6 @@
|
|||||||
"saved": "Request saved",
|
"saved": "Request saved",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
"share_description": "Share Hoppscotch with your friends",
|
"share_description": "Share Hoppscotch with your friends",
|
||||||
"share_request": "Share Request",
|
|
||||||
"stop": "Stop",
|
"stop": "Stop",
|
||||||
"title": "Request",
|
"title": "Request",
|
||||||
"type": "Request type",
|
"type": "Request type",
|
||||||
@@ -629,31 +607,14 @@
|
|||||||
"additional": "Additional Settings",
|
"additional": "Additional Settings",
|
||||||
"verify_email": "Verify email"
|
"verify_email": "Verify email"
|
||||||
},
|
},
|
||||||
"shared_requests": {
|
"shortcodes": {
|
||||||
"button": "Button",
|
"actions": "Actions",
|
||||||
"button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.",
|
"created_on": "Created on",
|
||||||
"customize": "Customize",
|
"deleted": "Shortcode deleted",
|
||||||
"creating_widget": "Creating widget",
|
"method": "Method",
|
||||||
"copy_html": "Copy HTML",
|
"not_found": "Shortcode not found",
|
||||||
"copy_link": "Copy Link",
|
"short_code": "Short code",
|
||||||
"copy_markdown": "Copy Markdown",
|
"url": "URL"
|
||||||
"deleted": "Shared request deleted",
|
|
||||||
"description": "Select a widget, you can change and customize this later",
|
|
||||||
"embed": "Embed",
|
|
||||||
"embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.",
|
|
||||||
"link": "Link",
|
|
||||||
"link_info": "Create a shareable link to share with anyone on the internet with view access.",
|
|
||||||
"modified": "Shared request modified",
|
|
||||||
"not_found": "Shared request not found",
|
|
||||||
"open_new_tab": "Open in new tab",
|
|
||||||
"preview": "Preview",
|
|
||||||
"run_in_hoppscotch": "Run in Hoppscotch",
|
|
||||||
"theme": {
|
|
||||||
"dark": "Dark",
|
|
||||||
"light": "Light",
|
|
||||||
"system": "System",
|
|
||||||
"title": "Theme"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"shortcut": {
|
"shortcut": {
|
||||||
"general": {
|
"general": {
|
||||||
@@ -683,6 +644,7 @@
|
|||||||
"title": "Others"
|
"title": "Others"
|
||||||
},
|
},
|
||||||
"request": {
|
"request": {
|
||||||
|
"copy_request_link": "Copy Request Link",
|
||||||
"delete_method": "Select DELETE method",
|
"delete_method": "Select DELETE method",
|
||||||
"get_method": "Select GET method",
|
"get_method": "Select GET method",
|
||||||
"head_method": "Select HEAD method",
|
"head_method": "Select HEAD method",
|
||||||
@@ -698,7 +660,6 @@
|
|||||||
"save_to_collections": "Save to Collections",
|
"save_to_collections": "Save to Collections",
|
||||||
"send_request": "Send Request",
|
"send_request": "Send Request",
|
||||||
"show_code": "Generate code snippet",
|
"show_code": "Generate code snippet",
|
||||||
"share_request": "Share Request",
|
|
||||||
"title": "Request"
|
"title": "Request"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
@@ -823,7 +784,6 @@
|
|||||||
"connection_failed": "Connection failed",
|
"connection_failed": "Connection failed",
|
||||||
"connection_lost": "Connection lost",
|
"connection_lost": "Connection lost",
|
||||||
"copied_to_clipboard": "Copied to clipboard",
|
"copied_to_clipboard": "Copied to clipboard",
|
||||||
"copied_interface_to_clipboard": "Copied {language} interface type to clipboard",
|
|
||||||
"deleted": "Deleted",
|
"deleted": "Deleted",
|
||||||
"deprecated": "DEPRECATED",
|
"deprecated": "DEPRECATED",
|
||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
@@ -882,7 +842,6 @@
|
|||||||
"queries": "Queries",
|
"queries": "Queries",
|
||||||
"query": "Query",
|
"query": "Query",
|
||||||
"schema": "Schema",
|
"schema": "Schema",
|
||||||
"shared_requests": "Shared Requests",
|
|
||||||
"socketio": "Socket.IO",
|
"socketio": "Socket.IO",
|
||||||
"sse": "SSE",
|
"sse": "SSE",
|
||||||
"tests": "Tests",
|
"tests": "Tests",
|
||||||
|
|||||||
@@ -22,41 +22,45 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "^10.1.0",
|
"@apidevtools/swagger-parser": "^10.1.0",
|
||||||
"@codemirror/autocomplete": "^6.11.0",
|
"@codemirror/autocomplete": "^6.10.2",
|
||||||
"@codemirror/commands": "^6.3.0",
|
"@codemirror/commands": "^6.3.0",
|
||||||
"@codemirror/lang-javascript": "^6.2.1",
|
"@codemirror/lang-javascript": "^6.2.1",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-xml": "^6.0.2",
|
"@codemirror/lang-xml": "^6.0.2",
|
||||||
"@codemirror/language": "6.9.2",
|
"@codemirror/language": "6.9.0",
|
||||||
"@codemirror/legacy-modes": "^6.3.3",
|
"@codemirror/legacy-modes": "^6.3.3",
|
||||||
"@codemirror/lint": "^6.4.2",
|
"@codemirror/lint": "^6.4.2",
|
||||||
"@codemirror/search": "^6.5.4",
|
"@codemirror/search": "^6.5.4",
|
||||||
"@codemirror/state": "^6.3.1",
|
"@codemirror/state": "^6.3.1",
|
||||||
"@codemirror/view": "^6.22.0",
|
"@codemirror/view": "^6.22.0",
|
||||||
|
"@fontsource-variable/inter": "^5.0.8",
|
||||||
|
"@fontsource-variable/material-symbols-rounded": "^5.0.7",
|
||||||
|
"@fontsource-variable/roboto-mono": "^5.0.9",
|
||||||
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
|
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
|
||||||
"@hoppscotch/data": "workspace:^",
|
"@hoppscotch/data": "workspace:^",
|
||||||
"@hoppscotch/js-sandbox": "workspace:^",
|
"@hoppscotch/js-sandbox": "workspace:^",
|
||||||
"@hoppscotch/ui": "workspace:^",
|
"@hoppscotch/ui": "workspace:^",
|
||||||
"@hoppscotch/vue-toasted": "^0.1.0",
|
"@hoppscotch/vue-toasted": "^0.1.0",
|
||||||
"@lezer/highlight": "1.2.0",
|
"@lezer/highlight": "1.1.4",
|
||||||
"@unhead/vue": "^1.8.8",
|
"@urql/core": "^4.1.1",
|
||||||
"@urql/core": "^4.2.0",
|
|
||||||
"@urql/devtools": "^2.0.3",
|
"@urql/devtools": "^2.0.3",
|
||||||
"@urql/exchange-auth": "^2.1.6",
|
"@urql/exchange-auth": "^2.1.6",
|
||||||
"@urql/exchange-graphcache": "^6.3.3",
|
"@urql/exchange-graphcache": "^6.3.2",
|
||||||
"@vitejs/plugin-legacy": "^4.1.1",
|
"@vitejs/plugin-legacy": "^4.1.1",
|
||||||
"@vueuse/core": "^10.6.1",
|
"@vueuse/core": "^10.3.0",
|
||||||
"acorn-walk": "^8.3.0",
|
"@vueuse/head": "^1.3.1",
|
||||||
"axios": "^1.6.2",
|
"acorn-walk": "^8.2.0",
|
||||||
|
"axios": "^1.4.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"cookie-es": "^1.0.0",
|
"cookie-es": "^1.0.0",
|
||||||
"dioc": "^1.0.1",
|
"dioc": "workspace:^",
|
||||||
"esprima": "^4.0.1",
|
"esprima": "^4.0.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"fp-ts": "^2.16.1",
|
"fp-ts": "^2.16.1",
|
||||||
|
"fuse.js": "^6.6.2",
|
||||||
"globalthis": "^1.0.3",
|
"globalthis": "^1.0.3",
|
||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.0",
|
||||||
"graphql-language-service-interface": "^2.10.2",
|
"graphql-language-service-interface": "^2.9.1",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"httpsnippet": "^3.0.1",
|
"httpsnippet": "^3.0.1",
|
||||||
"insomnia-importers": "^3.6.0",
|
"insomnia-importers": "^3.6.0",
|
||||||
@@ -64,15 +68,14 @@
|
|||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsonpath-plus": "^7.2.0",
|
"jsonpath-plus": "^7.2.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lossless-json": "^3.0.2",
|
"lossless-json": "^2.0.11",
|
||||||
"minisearch": "^6.3.0",
|
"minisearch": "^6.1.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"paho-mqtt": "^1.1.0",
|
"paho-mqtt": "^1.1.0",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"postman-collection": "^4.3.0",
|
"postman-collection": "^4.2.0",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"qs": "^6.11.2",
|
"qs": "^6.11.2",
|
||||||
"quicktype-core": "^23.0.79",
|
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"set-cookie-parser": "^2.6.0",
|
"set-cookie-parser": "^2.6.0",
|
||||||
"set-cookie-parser-es": "^1.0.5",
|
"set-cookie-parser-es": "^1.0.5",
|
||||||
@@ -86,19 +89,19 @@
|
|||||||
"tern": "^0.24.3",
|
"tern": "^0.24.3",
|
||||||
"timers": "^0.1.1",
|
"timers": "^0.1.1",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"url": "^0.11.3",
|
"url": "^0.11.1",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
"verzod": "^0.2.0",
|
"verzod": "^0.2.0",
|
||||||
"uuid": "^9.0.1",
|
"vue": "^3.3.4",
|
||||||
"vue": "^3.3.8",
|
"vue-i18n": "^9.2.2",
|
||||||
"vue-i18n": "^9.7.1",
|
"vue-pdf-embed": "^1.1.6",
|
||||||
"vue-pdf-embed": "^1.2.1",
|
"vue-router": "^4.2.4",
|
||||||
"vue-router": "^4.2.5",
|
|
||||||
"vue-tippy": "6.3.1",
|
"vue-tippy": "6.3.1",
|
||||||
"vuedraggable-es": "^4.1.1",
|
"vuedraggable-es": "^4.1.1",
|
||||||
"wonka": "^6.3.4",
|
"wonka": "^6.3.4",
|
||||||
"workbox-window": "^7.0.0",
|
"workbox-window": "^7.0.0",
|
||||||
"xml-formatter": "^3.6.0",
|
"xml-formatter": "^3.5.0",
|
||||||
"yargs-parser": "^21.1.1",
|
"yargs-parser": "^21.1.1",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
@@ -110,58 +113,57 @@
|
|||||||
"@graphql-codegen/typed-document-node": "^5.0.1",
|
"@graphql-codegen/typed-document-node": "^5.0.1",
|
||||||
"@graphql-codegen/typescript": "^4.0.1",
|
"@graphql-codegen/typescript": "^4.0.1",
|
||||||
"@graphql-codegen/typescript-operations": "^4.0.1",
|
"@graphql-codegen/typescript-operations": "^4.0.1",
|
||||||
"@graphql-codegen/typescript-urql-graphcache": "^3.0.0",
|
"@graphql-codegen/typescript-urql-graphcache": "^2.4.5",
|
||||||
"@graphql-codegen/urql-introspection": "^3.0.0",
|
"@graphql-codegen/urql-introspection": "^2.2.1",
|
||||||
"@graphql-typed-document-node/core": "^3.2.0",
|
"@graphql-typed-document-node/core": "^3.2.0",
|
||||||
"@iconify-json/lucide": "^1.1.141",
|
"@iconify-json/lucide": "^1.1.119",
|
||||||
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
||||||
"@relmify/jest-fp-ts": "^2.1.1",
|
"@relmify/jest-fp-ts": "^2.1.1",
|
||||||
"@rushstack/eslint-patch": "^1.6.0",
|
"@rushstack/eslint-patch": "^1.3.3",
|
||||||
"@types/har-format": "^1.2.15",
|
"@types/har-format": "^1.2.12",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.5",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.8",
|
||||||
"@types/lossless-json": "^1.0.4",
|
"@types/lossless-json": "^1.0.1",
|
||||||
"@types/nprogress": "^0.2.3",
|
"@types/nprogress": "^0.2.0",
|
||||||
"@types/paho-mqtt": "^1.0.10",
|
"@types/paho-mqtt": "^1.0.7",
|
||||||
"@types/postman-collection": "^3.5.10",
|
"@types/postman-collection": "^3.5.7",
|
||||||
"@types/splitpanes": "^2.2.6",
|
"@types/splitpanes": "^2.2.1",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.2",
|
||||||
"@types/yargs-parser": "^21.0.3",
|
"@types/yargs-parser": "^21.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
"@typescript-eslint/eslint-plugin": "^6.4.0",
|
||||||
"@typescript-eslint/parser": "^6.12.0",
|
"@typescript-eslint/parser": "^6.4.0",
|
||||||
"@vitejs/plugin-vue": "^4.5.0",
|
"@vitejs/plugin-vue": "^4.3.1",
|
||||||
"@vue/compiler-sfc": "^3.3.8",
|
"@vue/compiler-sfc": "^3.3.4",
|
||||||
"@vue/eslint-config-typescript": "^12.0.0",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"@vue/runtime-core": "^3.3.8",
|
"@vue/runtime-core": "^3.3.4",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"eslint": "^8.54.0",
|
"eslint": "^8.47.0",
|
||||||
"eslint-plugin-prettier": "^5.0.1",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"eslint-plugin-vue": "^9.18.1",
|
"eslint-plugin-vue": "^9.17.0",
|
||||||
"glob": "^10.3.10",
|
"glob": "^10.3.10",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
"postcss": "^8.4.23",
|
"postcss": "^8.4.23",
|
||||||
"prettier": "^3.1.0",
|
"prettier-plugin-tailwindcss": "^0.5.6",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.7",
|
"rollup-plugin-polyfill-node": "^0.12.0",
|
||||||
"rollup-plugin-polyfill-node": "^0.13.0",
|
"sass": "^1.66.0",
|
||||||
"sass": "^1.69.5",
|
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
"typescript": "^5.3.2",
|
"typescript": "^5.1.6",
|
||||||
"unplugin-fonts": "^1.1.1",
|
"unplugin-fonts": "^1.0.3",
|
||||||
"unplugin-icons": "^0.17.4",
|
"unplugin-icons": "^0.16.5",
|
||||||
"unplugin-vue-components": "^0.25.2",
|
"unplugin-vue-components": "^0.25.1",
|
||||||
"vite": "^4.5.0",
|
"vite": "^4.4.9",
|
||||||
"vite-plugin-checker": "^0.6.2",
|
"vite-plugin-checker": "^0.6.1",
|
||||||
"vite-plugin-fonts": "^0.7.0",
|
"vite-plugin-fonts": "^0.6.0",
|
||||||
"vite-plugin-html-config": "^1.0.11",
|
"vite-plugin-html-config": "^1.0.11",
|
||||||
"vite-plugin-inspect": "^0.7.42",
|
"vite-plugin-inspect": "^0.7.38",
|
||||||
"vite-plugin-pages": "^0.31.0",
|
"vite-plugin-pages": "^0.31.0",
|
||||||
"vite-plugin-pages-sitemap": "^1.6.1",
|
"vite-plugin-pages-sitemap": "^1.6.1",
|
||||||
"vite-plugin-pwa": "^0.17.0",
|
"vite-plugin-pwa": "^0.16.4",
|
||||||
"vite-plugin-vue-layouts": "^0.8.0",
|
"vite-plugin-vue-layouts": "^0.8.0",
|
||||||
"vitest": "^0.34.6",
|
"vitest": "^0.34.2",
|
||||||
"vue-tsc": "^1.8.22"
|
"vue-tsc": "^1.8.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
packages/hoppscotch-common/src/components.d.ts
vendored
22
packages/hoppscotch-common/src/components.d.ts
vendored
@@ -56,13 +56,11 @@ declare module 'vue' {
|
|||||||
CollectionsGraphqlRequest: typeof import('./components/collections/graphql/Request.vue')['default']
|
CollectionsGraphqlRequest: typeof import('./components/collections/graphql/Request.vue')['default']
|
||||||
CollectionsImportExport: typeof import('./components/collections/ImportExport.vue')['default']
|
CollectionsImportExport: typeof import('./components/collections/ImportExport.vue')['default']
|
||||||
CollectionsMyCollections: typeof import('./components/collections/MyCollections.vue')['default']
|
CollectionsMyCollections: typeof import('./components/collections/MyCollections.vue')['default']
|
||||||
CollectionsProperties: typeof import('./components/collections/Properties.vue')['default']
|
|
||||||
CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
|
CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
|
||||||
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
|
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
|
||||||
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
||||||
CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
|
CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
|
||||||
CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default']
|
CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default']
|
||||||
Embeds: typeof import('./components/embeds/index.vue')['default']
|
|
||||||
Environments: typeof import('./components/environments/index.vue')['default']
|
Environments: typeof import('./components/environments/index.vue')['default']
|
||||||
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
|
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
|
||||||
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
|
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
|
||||||
@@ -110,7 +108,6 @@ declare module 'vue' {
|
|||||||
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
|
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
|
||||||
HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
|
HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
|
||||||
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
|
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
|
||||||
HoppSmartSelectWrapper: typeof import('@hoppscotch/ui')['HoppSmartSelectWrapper']
|
|
||||||
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
|
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
|
||||||
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
||||||
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
||||||
@@ -163,14 +160,6 @@ declare module 'vue' {
|
|||||||
IconLucideRss: typeof import('~icons/lucide/rss')['default']
|
IconLucideRss: typeof import('~icons/lucide/rss')['default']
|
||||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||||
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
|
|
||||||
IconLucideX: typeof import('~icons/lucide/x')['default']
|
|
||||||
ImportExportBase: typeof import('./components/importExport/Base.vue')['default']
|
|
||||||
ImportExportImportExportList: typeof import('./components/importExport/ImportExportList.vue')['default']
|
|
||||||
ImportExportImportExportSourcesList: typeof import('./components/importExport/ImportExportSourcesList.vue')['default']
|
|
||||||
ImportExportImportExportStepsFileImport: typeof import('./components/importExport/ImportExportSteps/FileImport.vue')['default']
|
|
||||||
ImportExportImportExportStepsMyCollectionImport: typeof import('./components/importExport/ImportExportSteps/MyCollectionImport.vue')['default']
|
|
||||||
ImportExportImportExportStepsUrlImport: typeof import('./components/importExport/ImportExportSteps/UrlImport.vue')['default']
|
|
||||||
InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default']
|
InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default']
|
||||||
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
|
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
|
||||||
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
||||||
@@ -194,16 +183,6 @@ declare module 'vue' {
|
|||||||
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
|
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
|
||||||
SettingsExtension: typeof import('./components/settings/Extension.vue')['default']
|
SettingsExtension: typeof import('./components/settings/Extension.vue')['default']
|
||||||
SettingsProxy: typeof import('./components/settings/Proxy.vue')['default']
|
SettingsProxy: typeof import('./components/settings/Proxy.vue')['default']
|
||||||
Share: typeof import('./components/share/index.vue')['default']
|
|
||||||
ShareCreateModal: typeof import('./components/share/CreateModal.vue')['default']
|
|
||||||
ShareCustomizeModal: typeof import('./components/share/CustomizeModal.vue')['default']
|
|
||||||
ShareModal: typeof import('./components/share/Modal.vue')['default']
|
|
||||||
ShareRequest: typeof import('./components/share/Request.vue')['default']
|
|
||||||
ShareRequestModal: typeof import('./components/share/RequestModal.vue')['default']
|
|
||||||
ShareShareRequestModal: typeof import('./components/share/ShareRequestModal.vue')['default']
|
|
||||||
ShareTemplatesButton: typeof import('./components/share/templates/Button.vue')['default']
|
|
||||||
ShareTemplatesEmbeds: typeof import('./components/share/templates/Embeds.vue')['default']
|
|
||||||
ShareTemplatesLink: typeof import('./components/share/templates/Link.vue')['default']
|
|
||||||
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
|
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
|
||||||
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
|
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
|
||||||
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
|
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
|
||||||
@@ -224,7 +203,6 @@ declare module 'vue' {
|
|||||||
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
|
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
|
||||||
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
||||||
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
||||||
SmartSelectWrapper: typeof import('./../../hoppscotch-ui/src/components/smart/SelectWrapper.vue')['default']
|
|
||||||
SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default']
|
SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default']
|
||||||
SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.vue')['default']
|
SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.vue')['default']
|
||||||
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
|
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
|
||||||
|
|||||||
@@ -1,23 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:role="bannerRole"
|
:role="bannerRole"
|
||||||
class="flex items-center justify-between px-4 py-2 text-tiny text-secondaryDark"
|
class="flex items-center px-4 py-2 text-tiny"
|
||||||
:class="bannerColor"
|
:class="bannerColor"
|
||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<component :is="bannerIcon" class="mr-2 text-white" />
|
||||||
<component :is="bannerIcon" class="mr-2" />
|
|
||||||
<span :class="{ 'hidden sm:inline-flex': banner.alternateText }">
|
<span class="text-white">
|
||||||
{{ banner.text(t) }}
|
<span v-if="banner.alternateText" class="md:hidden">
|
||||||
</span>
|
|
||||||
<span v-if="banner.alternateText" class="inline-flex sm:hidden">
|
|
||||||
{{ banner.alternateText(t) }}
|
{{ banner.alternateText(t) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<span :class="banner.alternateText ? '<md:hidden' : ''">
|
||||||
<icon-lucide-x
|
{{ banner.text(t) }}
|
||||||
v-if="dismissible"
|
</span>
|
||||||
class="opacity-50 hover:cursor-pointer hover:opacity-100"
|
</span>
|
||||||
@click="emit('dismiss')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -30,32 +26,22 @@ import IconAlertCircle from "~icons/lucide/alert-circle"
|
|||||||
import IconAlertTriangle from "~icons/lucide/alert-triangle"
|
import IconAlertTriangle from "~icons/lucide/alert-triangle"
|
||||||
import IconInfo from "~icons/lucide/info"
|
import IconInfo from "~icons/lucide/info"
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = defineProps<{
|
||||||
defineProps<{
|
|
||||||
banner: BannerContent
|
banner: BannerContent
|
||||||
dismissible?: boolean
|
}>()
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
dismissible: false,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "dismiss"): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const ariaRoles: Record<BannerType, string> = {
|
const ariaRoles: Record<BannerType, string> = {
|
||||||
info: "status",
|
|
||||||
warning: "status",
|
|
||||||
error: "alert",
|
error: "alert",
|
||||||
|
warning: "status",
|
||||||
|
info: "status",
|
||||||
}
|
}
|
||||||
|
|
||||||
const bgColors: Record<BannerType, string> = {
|
const bgColors: Record<BannerType, string> = {
|
||||||
info: "bg-bannerInfo",
|
error: "bg-red-700",
|
||||||
warning: "bg-bannerWarning",
|
warning: "bg-yellow-700",
|
||||||
error: "bg-bannerError",
|
info: "bg-stone-800",
|
||||||
}
|
}
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
|
|||||||
@@ -13,16 +13,38 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('header.menu')"
|
||||||
|
:icon="IconMenu"
|
||||||
|
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
|
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
:label="t('app.name')"
|
:label="t('app.name')"
|
||||||
to="/"
|
to="/"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('header.go_back')"
|
||||||
|
:icon="IconArrowLeft"
|
||||||
|
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
|
@click="router.back()"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('header.go_forward')"
|
||||||
|
:icon="IconArrowRight"
|
||||||
|
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
|
@click="router.forward()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-1 flex items-center justify-between space-x-2">
|
<div class="col-span-1 flex items-center justify-between space-x-2">
|
||||||
<button
|
<button
|
||||||
class="flex h-full flex-1 cursor-text items-center justify-between self-stretch rounded border border-dividerDark bg-primaryDark px-2 text-secondaryLight transition hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
|
class="flex h-full flex-1 cursor-text items-center justify-between rounded border border-dividerDark bg-primaryDark px-2 text-secondaryLight transition hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
|
||||||
@click="invokeAction('modals.search.toggle')"
|
@click="invokeAction('modals.search.toggle')"
|
||||||
>
|
>
|
||||||
<span class="inline-flex flex-1 items-center">
|
<span class="inline-flex flex-1 items-center">
|
||||||
@@ -37,14 +59,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-span-2 flex items-center justify-between space-x-2">
|
<div class="col-span-2 flex items-center justify-between space-x-2">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<HoppButtonSecondary
|
|
||||||
v-if="showInstallButton"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('header.install_pwa')"
|
|
||||||
:icon="IconDownload"
|
|
||||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
|
||||||
@click="installPWA()"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||||
:title="`${
|
:title="`${
|
||||||
@@ -54,6 +68,41 @@
|
|||||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
@click="invokeAction('modals.support.toggle')"
|
@click="invokeAction('modals.support.toggle')"
|
||||||
/>
|
/>
|
||||||
|
<span>
|
||||||
|
<tippy
|
||||||
|
interactive
|
||||||
|
trigger="click"
|
||||||
|
theme="popover"
|
||||||
|
:on-shown="() => downloadActions.focus()"
|
||||||
|
>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('header.download_app')"
|
||||||
|
:icon="IconDownload"
|
||||||
|
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
|
/>
|
||||||
|
<template #content="{ hide }">
|
||||||
|
<div
|
||||||
|
ref="downloadActions"
|
||||||
|
class="flex flex-col focus:outline-none"
|
||||||
|
tabindex="0"
|
||||||
|
@keyup.escape="hide()"
|
||||||
|
@click="hide()"
|
||||||
|
>
|
||||||
|
<HoppSmartItem
|
||||||
|
:label="t('header.download_app')"
|
||||||
|
:icon="IconDownload"
|
||||||
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
v-if="showInstallButton"
|
||||||
|
:label="t('header.install_pwa')"
|
||||||
|
:icon="IconPlusSquare"
|
||||||
|
@click="installPWA()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</tippy>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div
|
<div
|
||||||
@@ -112,18 +161,14 @@
|
|||||||
trigger="click"
|
trigger="click"
|
||||||
theme="popover"
|
theme="popover"
|
||||||
:on-shown="() => accountActions.focus()"
|
:on-shown="() => accountActions.focus()"
|
||||||
>
|
|
||||||
<HoppSmartSelectWrapper
|
|
||||||
class="!text-blue-500 !focus-visible:text-blue-600 !hover:text-blue-600"
|
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('workspace.change')"
|
:title="t('workspace.change')"
|
||||||
:label="mdAndLarger ? workspaceName : ``"
|
:label="mdAndLarger ? workspaceName : ``"
|
||||||
:icon="workspace.type === 'personal' ? IconUser : IconUsers"
|
:icon="workspace.type === 'personal' ? IconUser : IconUsers"
|
||||||
class="!focus-visible:text-blue-600 !hover:text-blue-600 h-8 rounded border border-blue-600/25 bg-blue-500/10 pr-8 !text-blue-500 hover:border-blue-600/20 hover:bg-blue-600/20 focus-visible:border-blue-600/20 focus-visible:bg-blue-600/20"
|
class="select-wrapper !focus-visible:text-blue-600 !hover:text-blue-600 h-8 rounded border border-blue-600/25 bg-blue-500/10 pr-8 !text-blue-500 hover:border-blue-600/20 hover:bg-blue-600/20 focus-visible:border-blue-600/20 focus-visible:bg-blue-600/20"
|
||||||
/>
|
/>
|
||||||
</HoppSmartSelectWrapper>
|
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
<div
|
<div
|
||||||
ref="accountActions"
|
ref="accountActions"
|
||||||
@@ -144,10 +189,15 @@
|
|||||||
:on-shown="() => tippyActions.focus()"
|
:on-shown="() => tippyActions.focus()"
|
||||||
>
|
>
|
||||||
<HoppSmartPicture
|
<HoppSmartPicture
|
||||||
|
v-if="currentUser.photoURL"
|
||||||
v-tippy="{
|
v-tippy="{
|
||||||
theme: 'tooltip',
|
theme: 'tooltip',
|
||||||
}"
|
}"
|
||||||
:name="currentUser.uid"
|
:url="currentUser.photoURL"
|
||||||
|
:alt="
|
||||||
|
currentUser.displayName ||
|
||||||
|
t('profile.default_hopp_displayname')
|
||||||
|
"
|
||||||
:title="
|
:title="
|
||||||
currentUser.displayName ||
|
currentUser.displayName ||
|
||||||
currentUser.email ||
|
currentUser.email ||
|
||||||
@@ -155,7 +205,21 @@
|
|||||||
"
|
"
|
||||||
indicator
|
indicator
|
||||||
:indicator-styles="
|
:indicator-styles="
|
||||||
network.isOnline ? 'bg-green-500' : 'bg-red-500'
|
network.isOnline ? 'bg-emerald-500' : 'bg-red-500'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<HoppSmartPicture
|
||||||
|
v-else
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="
|
||||||
|
currentUser.displayName ||
|
||||||
|
currentUser.email ||
|
||||||
|
t('profile.default_hopp_displayname')
|
||||||
|
"
|
||||||
|
:initial="currentUser.displayName || currentUser.email"
|
||||||
|
indicator
|
||||||
|
:indicator-styles="
|
||||||
|
network.isOnline ? 'bg-emerald-500' : 'bg-red-500'
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
@@ -168,16 +232,14 @@
|
|||||||
@keyup.l="logout.$el.click()"
|
@keyup.l="logout.$el.click()"
|
||||||
@keyup.escape="hide()"
|
@keyup.escape="hide()"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col px-2">
|
<div class="flex flex-col px-2 text-tiny">
|
||||||
<span class="inline-flex truncate font-semibold">
|
<span class="inline-flex truncate font-semibold">
|
||||||
{{
|
{{
|
||||||
currentUser.displayName ||
|
currentUser.displayName ||
|
||||||
t("profile.default_hopp_displayname")
|
t("profile.default_hopp_displayname")
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span class="inline-flex truncate text-secondaryLight">
|
||||||
class="inline-flex truncate text-secondaryLight text-tiny"
|
|
||||||
>
|
|
||||||
{{ currentUser.email }}
|
{{ currentUser.email }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,12 +273,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<AppBanner
|
<AppBanner v-if="bannerContent" :banner="bannerContent" />
|
||||||
v-if="bannerContent"
|
|
||||||
:banner="bannerContent"
|
|
||||||
:dismissible="true"
|
|
||||||
@dismiss="dismissOfflineBanner"
|
|
||||||
/>
|
|
||||||
<TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" />
|
<TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" />
|
||||||
<TeamsInvite
|
<TeamsInvite
|
||||||
v-if="workspace.type === 'team' && workspace.teamID"
|
v-if="workspace.type === 'team' && workspace.teamID"
|
||||||
@@ -261,6 +318,10 @@ import IconUploadCloud from "~icons/lucide/upload-cloud"
|
|||||||
import IconUser from "~icons/lucide/user"
|
import IconUser from "~icons/lucide/user"
|
||||||
import IconUserPlus from "~icons/lucide/user-plus"
|
import IconUserPlus from "~icons/lucide/user-plus"
|
||||||
import IconUsers from "~icons/lucide/users"
|
import IconUsers from "~icons/lucide/users"
|
||||||
|
import IconPlusSquare from "~icons/lucide/plus-square"
|
||||||
|
import IconArrowLeft from "~icons/lucide/arrow-left"
|
||||||
|
import IconArrowRight from "~icons/lucide/arrow-right"
|
||||||
|
import IconMenu from "~icons/lucide/align-left"
|
||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
import * as TE from "fp-ts/TaskEither"
|
import * as TE from "fp-ts/TaskEither"
|
||||||
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
|
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
|
||||||
@@ -269,9 +330,11 @@ import {
|
|||||||
BannerContent,
|
BannerContent,
|
||||||
BANNER_PRIORITY_HIGH,
|
BANNER_PRIORITY_HIGH,
|
||||||
} from "~/services/banner.service"
|
} from "~/services/banner.service"
|
||||||
|
import { useRouter } from "vue-router"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Once the PWA code is initialized, this holds a method
|
* Once the PWA code is initialized, this holds a method
|
||||||
@@ -291,7 +354,7 @@ const bannerContent = computed(() => banner.content.value?.content)
|
|||||||
let bannerID: number | null = null
|
let bannerID: number | null = null
|
||||||
|
|
||||||
const offlineBanner: BannerContent = {
|
const offlineBanner: BannerContent = {
|
||||||
type: "warning",
|
type: "info",
|
||||||
text: (t) => t("helpers.offline"),
|
text: (t) => t("helpers.offline"),
|
||||||
alternateText: (t) => t("helpers.offline_short"),
|
alternateText: (t) => t("helpers.offline_short"),
|
||||||
score: BANNER_PRIORITY_HIGH,
|
score: BANNER_PRIORITY_HIGH,
|
||||||
@@ -305,14 +368,13 @@ watch(isOnline, () => {
|
|||||||
if (!isOnline.value) {
|
if (!isOnline.value) {
|
||||||
bannerID = banner.showBanner(offlineBanner)
|
bannerID = banner.showBanner(offlineBanner)
|
||||||
return
|
return
|
||||||
}
|
} else {
|
||||||
if (banner.content && bannerID) {
|
if (banner.content && bannerID) {
|
||||||
banner.removeBanner(bannerID)
|
banner.removeBanner(bannerID)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const dismissOfflineBanner = () => banner.removeBanner(bannerID!)
|
|
||||||
|
|
||||||
const currentUser = useReadonlyStream(
|
const currentUser = useReadonlyStream(
|
||||||
platform.auth.getProbableUserStream(),
|
platform.auth.getProbableUserStream(),
|
||||||
platform.auth.getProbableUser()
|
platform.auth.getProbableUser()
|
||||||
@@ -444,6 +506,7 @@ const profile = ref<any | null>(null)
|
|||||||
const settings = ref<any | null>(null)
|
const settings = ref<any | null>(null)
|
||||||
const logout = ref<any | null>(null)
|
const logout = ref<any | null>(null)
|
||||||
const accountActions = ref<any | null>(null)
|
const accountActions = ref<any | null>(null)
|
||||||
|
const downloadActions = ref<any | null>(null)
|
||||||
|
|
||||||
defineActionHandler("modals.team.edit", handleTeamEdit)
|
defineActionHandler("modals.team.edit", handleTeamEdit)
|
||||||
|
|
||||||
|
|||||||
@@ -92,8 +92,9 @@ const getHighestSeverity = computed(() => {
|
|||||||
},
|
},
|
||||||
{ severity: 0 }
|
{ severity: 0 }
|
||||||
)
|
)
|
||||||
}
|
} else {
|
||||||
return { severity: 0 }
|
return { severity: 0 }
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const severityColor = (severity: number) => {
|
const severityColor = (severity: number) => {
|
||||||
|
|||||||
@@ -17,10 +17,9 @@
|
|||||||
v-if="isEmpty(shortcutsResults)"
|
v-if="isEmpty(shortcutsResults)"
|
||||||
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
||||||
<icon-lucide-search class="svg-icons opacity-75" />
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
|
|
||||||
<details
|
<details
|
||||||
v-for="(sectionResults, sectionTitle) in shortcutsResults"
|
v-for="(sectionResults, sectionTitle) in shortcutsResults"
|
||||||
v-else
|
v-else
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
:is="entry.icon"
|
:is="entry.icon"
|
||||||
class="svg-icons opacity-80"
|
class="svg-icons opacity-50"
|
||||||
:class="{ 'opacity-25': active }"
|
:class="{ 'opacity-100': active }"
|
||||||
/>
|
/>
|
||||||
<template
|
<template
|
||||||
v-if="entry.text.type === 'text' && typeof entry.text.text === 'string'"
|
v-if="entry.text.type === 'text' && typeof entry.text.text === 'string'"
|
||||||
@@ -82,9 +82,9 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const formattedShortcutKeys = computed(
|
const formattedShortcutKeys = computed(
|
||||||
() =>
|
() =>
|
||||||
props.entry.meta?.keyboardShortcut?.map(
|
props.entry.meta?.keyboardShortcut?.map((key) => {
|
||||||
(key) => SPECIAL_KEY_CHARS[key] ?? capitalize(key)
|
return SPECIAL_KEY_CHARS[key] ?? capitalize(key)
|
||||||
)
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { HoppCollection } from "@hoppscotch/data"
|
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data"
|
||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
import { graphqlCollectionStore } from "~/newstore/collections"
|
import { graphqlCollectionStore } from "~/newstore/collections"
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ const pathFolders = computed(() => {
|
|||||||
.slice(0, -1)
|
.slice(0, -1)
|
||||||
.map((x) => parseInt(x))
|
.map((x) => parseInt(x))
|
||||||
|
|
||||||
const pathItems: HoppCollection[] = []
|
const pathItems: HoppCollection<HoppGQLRequest>[] = []
|
||||||
|
|
||||||
let currentFolder =
|
let currentFolder =
|
||||||
graphqlCollectionStore.value.state[folderIndicies.shift()!]
|
graphqlCollectionStore.value.state[folderIndicies.shift()!]
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { HoppCollection } from "@hoppscotch/data"
|
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
import { restCollectionStore } from "~/newstore/collections"
|
import { restCollectionStore } from "~/newstore/collections"
|
||||||
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
|
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
|
||||||
@@ -36,7 +36,7 @@ const pathFolders = computed(() => {
|
|||||||
.slice(0, -1)
|
.slice(0, -1)
|
||||||
.map((x) => parseInt(x))
|
.map((x) => parseInt(x))
|
||||||
|
|
||||||
const pathItems: HoppCollection[] = []
|
const pathItems: HoppCollection<HoppRESTRequest>[] = []
|
||||||
|
|
||||||
let currentFolder = restCollectionStore.value.state[folderIndicies.shift()!]
|
let currentFolder = restCollectionStore.value.state[folderIndicies.shift()!]
|
||||||
pathItems.push(currentFolder)
|
pathItems.push(currentFolder)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user