mirror of
https://github.com/papra-hq/papra.git
synced 2025-12-30 08:59:39 -06:00
chore(lint): enabled type-aware linting (#398)
This commit is contained in:
committed by
GitHub
parent
f28d8245bf
commit
a188af1f88
@@ -5,6 +5,11 @@ export default antfu({
|
||||
semi: true,
|
||||
},
|
||||
|
||||
ignores: [
|
||||
// Generated file
|
||||
'src/modules/i18n/locales.types.ts',
|
||||
],
|
||||
|
||||
rules: {
|
||||
// To allow export on top of files
|
||||
'ts/no-use-before-define': ['error', { allowNamedExports: true, functions: false }],
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import type { HttpClientOptions, ResponseType } from '../shared/http/http-client';
|
||||
import { joinUrlPaths } from '@corentinth/chisels';
|
||||
|
||||
type ExtractRouteParams<Path extends string> =
|
||||
Path extends `${infer _Start}:${infer Param}/${infer Rest}`
|
||||
type ExtractRouteParams<Path extends string>
|
||||
= Path extends `${infer _Start}:${infer Param}/${infer Rest}`
|
||||
? { [k in Param | keyof ExtractRouteParams<`/${Rest}`>]: string }
|
||||
: Path extends `${infer _Start}:${infer Param}`
|
||||
? { [k in Param]: string }
|
||||
|
||||
@@ -24,8 +24,8 @@ export const alertVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
type alertProps<T extends ValidComponent = 'div'> = AlertRootProps<T> &
|
||||
VariantProps<typeof alertVariants> & {
|
||||
type alertProps<T extends ValidComponent = 'div'> = AlertRootProps<T>
|
||||
& VariantProps<typeof alertVariants> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ export const buttonVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
type buttonProps<T extends ValidComponent = 'button'> = ButtonRootProps<T> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
type buttonProps<T extends ValidComponent = 'button'> = ButtonRootProps<T>
|
||||
& VariantProps<typeof buttonVariants> & {
|
||||
class?: string;
|
||||
isLoading?: boolean;
|
||||
children?: JSX.Element;
|
||||
|
||||
@@ -88,8 +88,8 @@ export function ComboboxTrigger<T extends ValidComponent = 'button'>(props: Poly
|
||||
);
|
||||
}
|
||||
|
||||
type comboboxContentProps<T extends ValidComponent = 'div'> =
|
||||
ComboboxContentProps<T> & {
|
||||
type comboboxContentProps<T extends ValidComponent = 'div'>
|
||||
= ComboboxContentProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -77,8 +77,8 @@ export function DialogTitle<T extends ValidComponent = 'h2'>(props: PolymorphicP
|
||||
);
|
||||
}
|
||||
|
||||
type dialogDescriptionProps<T extends ValidComponent = 'p'> =
|
||||
DialogDescriptionProps<T> & {
|
||||
type dialogDescriptionProps<T extends ValidComponent = 'p'>
|
||||
= DialogDescriptionProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ export function DropdownMenu(props: DropdownMenuRootProps) {
|
||||
return <DropdownMenuPrimitive {...merge} />;
|
||||
}
|
||||
|
||||
type dropdownMenuContentProps<T extends ValidComponent = 'div'> =
|
||||
DropdownMenuContentProps<T> & {
|
||||
type dropdownMenuContentProps<T extends ValidComponent = 'div'>
|
||||
= DropdownMenuContentProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
@@ -49,8 +49,8 @@ export function DropdownMenuContent<T extends ValidComponent = 'div'>(props: Pol
|
||||
);
|
||||
}
|
||||
|
||||
type dropdownMenuItemProps<T extends ValidComponent = 'div'> =
|
||||
DropdownMenuItemProps<T> & {
|
||||
type dropdownMenuItemProps<T extends ValidComponent = 'div'>
|
||||
= DropdownMenuItemProps<T> & {
|
||||
class?: string;
|
||||
inset?: boolean;
|
||||
};
|
||||
@@ -73,8 +73,8 @@ export function DropdownMenuItem<T extends ValidComponent = 'div'>(props: Polymo
|
||||
);
|
||||
}
|
||||
|
||||
type dropdownMenuGroupLabelProps<T extends ValidComponent = 'span'> =
|
||||
DropdownMenuGroupLabelProps<T> & {
|
||||
type dropdownMenuGroupLabelProps<T extends ValidComponent = 'span'>
|
||||
= DropdownMenuGroupLabelProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
@@ -92,8 +92,8 @@ export function DropdownMenuGroupLabel<T extends ValidComponent = 'span'>(props:
|
||||
);
|
||||
}
|
||||
|
||||
type dropdownMenuItemLabelProps<T extends ValidComponent = 'div'> =
|
||||
DropdownMenuItemLabelProps<T> & {
|
||||
type dropdownMenuItemLabelProps<T extends ValidComponent = 'div'>
|
||||
= DropdownMenuItemLabelProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
@@ -111,8 +111,8 @@ export function DropdownMenuItemLabel<T extends ValidComponent = 'div'>(props: P
|
||||
);
|
||||
}
|
||||
|
||||
type dropdownMenuSeparatorProps<T extends ValidComponent = 'hr'> =
|
||||
DropdownMenuSeparatorProps<T> & {
|
||||
type dropdownMenuSeparatorProps<T extends ValidComponent = 'hr'>
|
||||
= DropdownMenuSeparatorProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
@@ -178,8 +178,8 @@ export function DropdownMenuSubTrigger<T extends ValidComponent = 'div'>(props:
|
||||
);
|
||||
}
|
||||
|
||||
type dropdownMenuSubContentProps<T extends ValidComponent = 'div'> =
|
||||
DropdownMenuSubTriggerProps<T> & {
|
||||
type dropdownMenuSubContentProps<T extends ValidComponent = 'div'>
|
||||
= DropdownMenuSubTriggerProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -60,8 +60,8 @@ export function NumberFieldErrorMessage<T extends ValidComponent = 'div'>(props:
|
||||
);
|
||||
}
|
||||
|
||||
type numberFieldProps<T extends ValidComponent = 'div'> =
|
||||
NumberFieldRootProps<T> & {
|
||||
type numberFieldProps<T extends ValidComponent = 'div'>
|
||||
= NumberFieldRootProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
@@ -87,8 +87,8 @@ export function NumberFieldGroup(props: ComponentProps<'div'>) {
|
||||
);
|
||||
}
|
||||
|
||||
type numberFieldInputProps<T extends ValidComponent = 'input'> =
|
||||
NumberFieldInputProps<T> & {
|
||||
type numberFieldInputProps<T extends ValidComponent = 'input'>
|
||||
= NumberFieldInputProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -55,8 +55,8 @@ export function SelectTrigger<T extends ValidComponent = 'button'>(props: Polymo
|
||||
);
|
||||
}
|
||||
|
||||
type selectContentProps<T extends ValidComponent = 'div'> =
|
||||
SelectContentProps<T> & {
|
||||
type selectContentProps<T extends ValidComponent = 'div'>
|
||||
= SelectContentProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ export const sheetVariants = cva(
|
||||
);
|
||||
|
||||
type sheetContentProps<T extends ValidComponent = 'div'> = ParentProps<
|
||||
DialogContentProps<T> &
|
||||
VariantProps<typeof sheetVariants> & {
|
||||
DialogContentProps<T>
|
||||
& VariantProps<typeof sheetVariants> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
@@ -96,8 +96,8 @@ export function SheetTitle<T extends ValidComponent = 'h2'>(props: PolymorphicPr
|
||||
);
|
||||
}
|
||||
|
||||
type sheetDescriptionProps<T extends ValidComponent = 'p'> =
|
||||
DialogDescriptionProps<T> & {
|
||||
type sheetDescriptionProps<T extends ValidComponent = 'p'>
|
||||
= DialogDescriptionProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -46,8 +46,8 @@ export function TabsList<T extends ValidComponent = 'div'>(props: PolymorphicPro
|
||||
);
|
||||
}
|
||||
|
||||
type tabsContentProps<T extends ValidComponent = 'div'> =
|
||||
TabsContentProps<T> & {
|
||||
type tabsContentProps<T extends ValidComponent = 'div'>
|
||||
= TabsContentProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
@@ -65,8 +65,8 @@ export function TabsContent<T extends ValidComponent = 'div'>(props: Polymorphic
|
||||
);
|
||||
}
|
||||
|
||||
type tabsTriggerProps<T extends ValidComponent = 'button'> =
|
||||
TabsTriggerProps<T> & {
|
||||
type tabsTriggerProps<T extends ValidComponent = 'button'>
|
||||
= TabsTriggerProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
@@ -100,8 +100,8 @@ const tabsIndicatorVariants = cva(
|
||||
);
|
||||
|
||||
type tabsIndicatorProps<T extends ValidComponent = 'div'> = VoidProps<
|
||||
TabsIndicatorProps<T> &
|
||||
VariantProps<typeof tabsIndicatorVariants> & {
|
||||
TabsIndicatorProps<T>
|
||||
& VariantProps<typeof tabsIndicatorVariants> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -12,8 +12,8 @@ import { cva } from 'class-variance-authority';
|
||||
import { splitProps } from 'solid-js';
|
||||
import { cn } from '@/modules/shared/style/cn';
|
||||
|
||||
type textFieldProps<T extends ValidComponent = 'div'> =
|
||||
TextFieldRootProps<T> & {
|
||||
type textFieldProps<T extends ValidComponent = 'div'>
|
||||
= TextFieldRootProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
@@ -43,8 +43,8 @@ export const textfieldLabel = cva(
|
||||
},
|
||||
);
|
||||
|
||||
type textFieldLabelProps<T extends ValidComponent = 'label'> =
|
||||
TextFieldLabelProps<T> & {
|
||||
type textFieldLabelProps<T extends ValidComponent = 'label'>
|
||||
= TextFieldLabelProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
@@ -59,8 +59,8 @@ export function TextFieldLabel<T extends ValidComponent = 'label'>(props: Polymo
|
||||
);
|
||||
}
|
||||
|
||||
type textFieldErrorMessageProps<T extends ValidComponent = 'div'> =
|
||||
TextFieldErrorMessageProps<T> & {
|
||||
type textFieldErrorMessageProps<T extends ValidComponent = 'div'>
|
||||
= TextFieldErrorMessageProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
@@ -77,8 +77,8 @@ export function TextFieldErrorMessage<T extends ValidComponent = 'div'>(props: P
|
||||
);
|
||||
}
|
||||
|
||||
type textFieldDescriptionProps<T extends ValidComponent = 'div'> =
|
||||
TextFieldDescriptionProps<T> & {
|
||||
type textFieldDescriptionProps<T extends ValidComponent = 'div'>
|
||||
= TextFieldDescriptionProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ function useToggleGroup() {
|
||||
}
|
||||
|
||||
type toggleGroupProps<T extends ValidComponent = 'div'> = ParentProps<
|
||||
ToggleGroupRootProps<T> &
|
||||
VariantProps<typeof toggleVariants> & {
|
||||
ToggleGroupRootProps<T>
|
||||
& VariantProps<typeof toggleVariants> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
@@ -56,8 +56,8 @@ export function ToggleGroup<T extends ValidComponent = 'div'>(props: Polymorphic
|
||||
);
|
||||
}
|
||||
|
||||
type toggleGroupItemProps<T extends ValidComponent = 'button'> =
|
||||
ToggleGroupItemProps<T> & {
|
||||
type toggleGroupItemProps<T extends ValidComponent = 'button'>
|
||||
= ToggleGroupItemProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -28,11 +28,11 @@ export const toggleVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
type toggleButtonProps<T extends ValidComponent = 'button'> =
|
||||
ToggleButtonRootProps<T> &
|
||||
VariantProps<typeof toggleVariants> & {
|
||||
class?: string;
|
||||
};
|
||||
type toggleButtonProps<T extends ValidComponent = 'button'>
|
||||
= ToggleButtonRootProps<T>
|
||||
& VariantProps<typeof toggleVariants> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export function ToggleButton<T extends ValidComponent = 'button'>(props: PolymorphicProps<T, toggleButtonProps<T>>) {
|
||||
const [local, rest] = splitProps(props as toggleButtonProps, [
|
||||
|
||||
@@ -22,8 +22,8 @@ export function Tooltip(props: TooltipRootProps) {
|
||||
return <TooltipPrimitive {...merge} />;
|
||||
}
|
||||
|
||||
type tooltipContentProps<T extends ValidComponent = 'div'> =
|
||||
TooltipContentProps<T> & {
|
||||
type tooltipContentProps<T extends ValidComponent = 'div'>
|
||||
= TooltipContentProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { cwd as getCwd } from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parse } from 'yaml';
|
||||
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
|
||||
export async function generateI18nTypes({ cwd = getCwd() }: { cwd?: string } = {}) {
|
||||
try {
|
||||
const yamlPath = path.join(cwd, 'src/locales/en.yml');
|
||||
@@ -17,7 +20,7 @@ export async function generateI18nTypes({ cwd = getCwd() }: { cwd?: string } = {
|
||||
// Do not manually edit this file.
|
||||
// This file is dynamically generated when the dev server runs (or using the \`pnpm script:generate-i18n-types\` command).
|
||||
// Keys are extracted from the en.yml file.
|
||||
// Source code : ${path.relative(cwd, __filename)}
|
||||
// Source code : ${path.relative(cwd, filename)}
|
||||
|
||||
export type LocaleKeys =\n${localKeys.map(key => ` | '${key}'`).join('\n')};
|
||||
`.trimStart();
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import antfu from '@antfu/eslint-config';
|
||||
|
||||
export default antfu({
|
||||
typescript: {
|
||||
tsconfigPath: './tsconfig.json',
|
||||
overridesTypeAware: {
|
||||
'ts/no-misused-promises': ['error', { checksVoidReturn: false }],
|
||||
'ts/strict-boolean-expressions': ['error', { allowNullableObject: true }],
|
||||
},
|
||||
|
||||
},
|
||||
stylistic: {
|
||||
semi: true,
|
||||
},
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { createPrefixedIdRegex } from '../shared/random/ids';
|
||||
|
||||
export const API_KEY_ID_PREFIX = 'ak';
|
||||
export const API_KEY_ID_REGEX = createPrefixedIdRegex({ prefix: API_KEY_ID_PREFIX });
|
||||
|
||||
export const API_KEY_PREFIX = 'ppapi';
|
||||
export const API_KEY_TOKEN_LENGTH = 64;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Context } from '../app/server.types';
|
||||
import { createMiddleware } from 'hono/factory';
|
||||
import { createUnauthorizedError } from '../app/auth/auth.errors';
|
||||
import { getAuthorizationHeader } from '../shared/headers/headers.models';
|
||||
import { isNil } from '../shared/utils';
|
||||
import { createApiKeysRepository } from './api-keys.repository';
|
||||
import { getApiKey } from './api-keys.usecases';
|
||||
|
||||
@@ -14,7 +15,7 @@ export function createApiKeyMiddleware({ db }: { db: Database }) {
|
||||
return createMiddleware(async (context: Context, next) => {
|
||||
const { authorizationHeader } = getAuthorizationHeader({ context });
|
||||
|
||||
if (!authorizationHeader) {
|
||||
if (isNil(authorizationHeader)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
@@ -30,6 +31,11 @@ export function createApiKeyMiddleware({ db }: { db: Database }) {
|
||||
throw createUnauthorizedError();
|
||||
}
|
||||
|
||||
if (isNil(token)) {
|
||||
// For type safety
|
||||
throw createUnauthorizedError();
|
||||
}
|
||||
|
||||
const { apiKey } = await getApiKey({ token, apiKeyRepository });
|
||||
|
||||
if (apiKey) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { injectArguments } from '@corentinth/chisels';
|
||||
import { and, eq, getTableColumns, inArray } from 'drizzle-orm';
|
||||
import { omit, pick } from 'lodash-es';
|
||||
import { organizationMembersTable, organizationsTable } from '../organizations/organizations.table';
|
||||
import { createError } from '../shared/errors/errors';
|
||||
import { createLogger } from '../shared/logger/logger';
|
||||
import { apiKeyOrganizationsTable, apiKeysTable } from './api-keys.tables';
|
||||
|
||||
@@ -58,6 +59,16 @@ async function saveApiKey({
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!apiKey) {
|
||||
// Very unlikely to happen as the insertion should throw an issue, it's for type safety
|
||||
throw createError({
|
||||
message: 'Error while creating api key',
|
||||
code: 'api-keys.create_error',
|
||||
statusCode: 500,
|
||||
isInternal: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (organizationIds && organizationIds.length > 0) {
|
||||
const apiKeyId = apiKey.id;
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ import { z } from 'zod';
|
||||
import { requireAuthentication } from '../app/auth/auth.middleware';
|
||||
import { getUser } from '../app/auth/auth.models';
|
||||
import { createError } from '../shared/errors/errors';
|
||||
import { validateJsonBody } from '../shared/validation/validation';
|
||||
import { validateJsonBody, validateParams } from '../shared/validation/validation';
|
||||
import { API_KEY_PERMISSIONS_VALUES } from './api-keys.constants';
|
||||
import { createApiKeysRepository } from './api-keys.repository';
|
||||
import { apiKeyIdSchema } from './api-keys.schemas';
|
||||
import { createApiKey } from './api-keys.usecases';
|
||||
|
||||
export function registerApiKeysRoutes(context: RouteDefinitionContext) {
|
||||
@@ -85,11 +86,14 @@ function setupDeleteApiKeyRoute({ app, db }: RouteDefinitionContext) {
|
||||
app.delete(
|
||||
'/api/api-keys/:apiKeyId',
|
||||
requireAuthentication(),
|
||||
validateParams(z.object({
|
||||
apiKeyId: apiKeyIdSchema,
|
||||
})),
|
||||
async (context) => {
|
||||
const { userId } = getUser({ context });
|
||||
const apiKeyRepository = createApiKeysRepository({ db });
|
||||
|
||||
const { apiKeyId } = context.req.param();
|
||||
const { apiKeyId } = context.req.valid('param');
|
||||
|
||||
await apiKeyRepository.deleteUserApiKey({ apiKeyId, userId });
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import { API_KEY_ID_REGEX } from './api-keys.constants';
|
||||
|
||||
export const apiKeyIdSchema = z.string().regex(API_KEY_ID_REGEX);
|
||||
@@ -4,11 +4,12 @@ import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import { organizationMembersTable } from '../organizations/organizations.table';
|
||||
import { createPrimaryKeyField, createTimestampColumns } from '../shared/db/columns.helpers';
|
||||
import { usersTable } from '../users/users.table';
|
||||
import { API_KEY_ID_PREFIX } from './api-keys.constants';
|
||||
|
||||
export const apiKeysTable = sqliteTable(
|
||||
'api_keys',
|
||||
{
|
||||
...createPrimaryKeyField({ prefix: 'ak' }),
|
||||
...createPrimaryKeyField({ prefix: API_KEY_ID_PREFIX }),
|
||||
...createTimestampColumns(),
|
||||
|
||||
name: text('name').notNull(),
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('api-keys usecases', () => {
|
||||
|
||||
const { apiKey } = await getApiKey({ token: 'ppapi_HT2Hj5V8A3WHMQtVcMDB9UucqUxPU15o1aI6qOc1Oy5qBvbSEr4jZzsjuFYPqCP0', apiKeyRepository });
|
||||
|
||||
expect(apiKey.id).to.eql('api_key_1');
|
||||
expect(apiKey?.id).to.eql('api_key_1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,11 +4,12 @@ import type { Context } from '../server.types';
|
||||
import type { Session } from './auth.types';
|
||||
import { uniq } from 'lodash-es';
|
||||
import { createError } from '../../shared/errors/errors';
|
||||
import { isNil } from '../../shared/utils';
|
||||
|
||||
export function getUser({ context }: { context: Context }) {
|
||||
const userId = context.get('userId');
|
||||
|
||||
if (!userId) {
|
||||
if (isNil(userId)) {
|
||||
// This should never happen as getUser is called in authenticated routes
|
||||
// just for proper type safety
|
||||
throw createError({
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { Context, RouteDefinitionContext } from '../server.types';
|
||||
import type { Session } from './auth.types';
|
||||
import { get } from 'lodash-es';
|
||||
import { isDefined } from '../../shared/utils';
|
||||
|
||||
export function registerAuthRoutes({ app, auth, config }: RouteDefinitionContext) {
|
||||
app.on(
|
||||
['POST', 'GET'],
|
||||
'/api/auth/*',
|
||||
context => auth.handler(context.req.raw),
|
||||
async context => auth.handler(context.req.raw),
|
||||
);
|
||||
|
||||
app.use('*', async (context: Context, next) => {
|
||||
@@ -23,9 +24,9 @@ export function registerAuthRoutes({ app, auth, config }: RouteDefinitionContext
|
||||
|
||||
if (config.env === 'test') {
|
||||
app.use('*', async (context: Context, next) => {
|
||||
const overrideUserId = get(context.env, 'loggedInUserId') as string | undefined;
|
||||
const overrideUserId: unknown = get(context.env, 'loggedInUserId');
|
||||
|
||||
if (overrideUserId) {
|
||||
if (isDefined(overrideUserId) && typeof overrideUserId === 'string') {
|
||||
context.set('userId', overrideUserId);
|
||||
context.set('session', {} as Session);
|
||||
context.set('authType', 'session');
|
||||
|
||||
@@ -36,9 +36,7 @@ export function getAuth({
|
||||
logger: {
|
||||
disabled: false,
|
||||
log: (baseLevel, message) => {
|
||||
const level = (baseLevel in logger ? baseLevel : 'info') as keyof typeof logger;
|
||||
|
||||
logger[level](message);
|
||||
logger[baseLevel ?? 'info'](message);
|
||||
},
|
||||
},
|
||||
emailAndPassword: {
|
||||
|
||||
@@ -53,7 +53,7 @@ async function seedDatabase({ db, ...seedRows }: { db: Database } & SeedTablesRo
|
||||
await Promise.all(
|
||||
Object
|
||||
.entries(seedRows)
|
||||
.map(([table, rows]) => db
|
||||
.map(async ([table, rows]) => db
|
||||
.insert(seedTables[table as keyof typeof seedTables])
|
||||
.values(rows)
|
||||
.execute(),
|
||||
|
||||
@@ -5,5 +5,5 @@ import { sql } from 'drizzle-orm';
|
||||
export async function isDatabaseHealthy({ db }: { db: Database }) {
|
||||
const [result, error] = await safely(db.run(sql`SELECT 1;`));
|
||||
|
||||
return error === null && result.rows.length > 0 && result.rows[0]['1'] === 1;
|
||||
return error === null && result.rows.length > 0 && result.rows[0]?.['1'] === 1;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ServerInstanceGenerics } from '../server.types';
|
||||
import { Hono } from 'hono';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { createError } from '../../shared/errors/errors';
|
||||
@@ -6,8 +7,8 @@ import { registerErrorMiddleware } from './errors.middleware';
|
||||
describe('errors middleware', () => {
|
||||
describe('registerErrorMiddleware', () => {
|
||||
test('when a non-internal custom error is thrown with a status code, the error is returned', async () => {
|
||||
const app = new Hono();
|
||||
registerErrorMiddleware({ app: app as any });
|
||||
const app = new Hono<ServerInstanceGenerics>();
|
||||
registerErrorMiddleware({ app });
|
||||
|
||||
app.get('/error', async () => {
|
||||
throw createError({
|
||||
@@ -29,8 +30,8 @@ describe('errors middleware', () => {
|
||||
});
|
||||
|
||||
test('when an unknown error is thrown, a 500 error is returned with a generic message', async () => {
|
||||
const app = new Hono();
|
||||
registerErrorMiddleware({ app: app as any });
|
||||
const app = new Hono<ServerInstanceGenerics>();
|
||||
registerErrorMiddleware({ app });
|
||||
|
||||
app.get('/error', async () => {
|
||||
throw new Error('Unknown error');
|
||||
@@ -48,8 +49,8 @@ describe('errors middleware', () => {
|
||||
});
|
||||
|
||||
test('when a custom error is marked as internal, a 500 error is returned with a generic message', async () => {
|
||||
const app = new Hono();
|
||||
registerErrorMiddleware({ app: app as any });
|
||||
const app = new Hono<ServerInstanceGenerics>();
|
||||
registerErrorMiddleware({ app });
|
||||
|
||||
app.get('/error', async () => {
|
||||
throw createError({
|
||||
@@ -72,8 +73,8 @@ describe('errors middleware', () => {
|
||||
});
|
||||
|
||||
test('when querying an unknown route, a 404 error is returned', async () => {
|
||||
const app = new Hono();
|
||||
registerErrorMiddleware({ app: app as any });
|
||||
const app = new Hono<ServerInstanceGenerics>();
|
||||
registerErrorMiddleware({ app });
|
||||
|
||||
const response = await app.request('/unknown-route', { method: 'GET' });
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ServerInstanceGenerics } from '../server.types';
|
||||
import { Hono } from 'hono';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { overrideConfig } from '../../config/config.test-utils';
|
||||
@@ -9,8 +10,8 @@ describe('middlewares', () => {
|
||||
test('when a request last longer than the config timeout, a 504 error is raised', async () => {
|
||||
const config = overrideConfig({ server: { routeTimeoutMs: 50 } });
|
||||
|
||||
const app = new Hono<{ Variables: { config: any } }>();
|
||||
registerErrorMiddleware({ app: app as any });
|
||||
const app = new Hono<ServerInstanceGenerics>();
|
||||
registerErrorMiddleware({ app });
|
||||
|
||||
app.get(
|
||||
'/should-timeout',
|
||||
|
||||
@@ -17,7 +17,7 @@ function setValidParams(path: string) {
|
||||
.replaceAll(':invitationId', 'inv_101010101010101010101010');
|
||||
|
||||
// throw if there are any remaining params
|
||||
if (newPath.match(/:(\w+)/g)) {
|
||||
if (newPath.match(/:\w+/g)) {
|
||||
throw new Error(`Add a dummy value for the params in ${path}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { RouteDefinitionContext } from '../app/server.types';
|
||||
import { getPublicConfig } from './config.models';
|
||||
|
||||
export async function registerConfigRoutes(context: RouteDefinitionContext) {
|
||||
export function registerConfigRoutes(context: RouteDefinitionContext) {
|
||||
setupGetPublicConfigRoute(context);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getExtension } from '../shared/files/file-names';
|
||||
import { generateId } from '../shared/random/ids';
|
||||
import { isDefined } from '../shared/utils';
|
||||
import { ORIGINAL_DOCUMENTS_STORAGE_KEY } from './documents.constants';
|
||||
|
||||
export function joinStorageKeyParts(...parts: string[]) {
|
||||
@@ -9,7 +10,7 @@ export function joinStorageKeyParts(...parts: string[]) {
|
||||
export function buildOriginalDocumentKey({ documentId, organizationId, fileName }: { documentId: string; organizationId: string; fileName: string }) {
|
||||
const { extension } = getExtension({ fileName });
|
||||
|
||||
const newFileName = extension ? `${documentId}.${extension}` : documentId;
|
||||
const newFileName = isDefined(extension) ? `${documentId}.${extension}` : documentId;
|
||||
|
||||
const originalDocumentStorageKey = joinStorageKeyParts(organizationId, ORIGINAL_DOCUMENTS_STORAGE_KEY, newFileName);
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@ import { injectArguments, safely } from '@corentinth/chisels';
|
||||
import { subDays } from 'date-fns';
|
||||
import { and, count, desc, eq, getTableColumns, lt, sql, sum } from 'drizzle-orm';
|
||||
import { omit } from 'lodash-es';
|
||||
import { createOrganizationNotFoundError } from '../organizations/organizations.errors';
|
||||
import { isUniqueConstraintError } from '../shared/db/constraints.models';
|
||||
import { withPagination } from '../shared/db/pagination';
|
||||
import { omitUndefined } from '../shared/utils';
|
||||
import { createError } from '../shared/errors/errors';
|
||||
import { isDefined, isNil, omitUndefined } from '../shared/utils';
|
||||
import { documentsTagsTable, tagsTable } from '../tags/tags.table';
|
||||
import { createDocumentAlreadyExistsError } from './documents.errors';
|
||||
import { createDocumentAlreadyExistsError, createDocumentNotFoundError } from './documents.errors';
|
||||
import { documentsTable } from './documents.table';
|
||||
|
||||
export type DocumentsRepository = ReturnType<typeof createDocumentsRepository>;
|
||||
@@ -57,13 +59,27 @@ async function saveOrganizationDocument({ db, ...documentToInsert }: { db: Datab
|
||||
throw createDocumentAlreadyExistsError();
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const [document] = documents ?? [];
|
||||
|
||||
if (isNil(document)) {
|
||||
// Very unlikely to happen as the insertion throws an issue, it's for type safety
|
||||
throw createError({
|
||||
message: 'Error while saving document',
|
||||
code: 'documents.save_error',
|
||||
statusCode: 500,
|
||||
isInternal: true,
|
||||
});
|
||||
}
|
||||
|
||||
return { document };
|
||||
}
|
||||
|
||||
async function getOrganizationDocumentsCount({ organizationId, filters, db }: { organizationId: string; filters?: { tags?: string[] }; db: Database }) {
|
||||
const [{ documentsCount }] = await db
|
||||
const [record] = await db
|
||||
.select({
|
||||
documentsCount: count(documentsTable.id),
|
||||
})
|
||||
@@ -77,11 +93,17 @@ async function getOrganizationDocumentsCount({ organizationId, filters, db }: {
|
||||
),
|
||||
);
|
||||
|
||||
if (isNil(record)) {
|
||||
throw createOrganizationNotFoundError();
|
||||
}
|
||||
|
||||
const { documentsCount } = record;
|
||||
|
||||
return { documentsCount };
|
||||
}
|
||||
|
||||
async function getOrganizationDeletedDocumentsCount({ organizationId, db }: { organizationId: string; db: Database }) {
|
||||
const [{ documentsCount }] = await db
|
||||
const [record] = await db
|
||||
.select({
|
||||
documentsCount: count(documentsTable.id),
|
||||
})
|
||||
@@ -93,6 +115,12 @@ async function getOrganizationDeletedDocumentsCount({ organizationId, db }: { or
|
||||
),
|
||||
);
|
||||
|
||||
if (isNil(record)) {
|
||||
throw createOrganizationNotFoundError();
|
||||
}
|
||||
|
||||
const { documentsCount } = record;
|
||||
|
||||
return { documentsCount };
|
||||
}
|
||||
|
||||
@@ -145,7 +173,7 @@ async function getOrganizationDocuments({
|
||||
}
|
||||
|
||||
if (tag) {
|
||||
acc[document.id].tags.push(tag);
|
||||
acc[document.id]!.tags.push(tag);
|
||||
}
|
||||
|
||||
return acc;
|
||||
@@ -235,8 +263,8 @@ async function restoreDocument({ documentId, organizationId, name, userId, db }:
|
||||
isDeleted: false,
|
||||
deletedBy: null,
|
||||
deletedAt: null,
|
||||
...(name ? { name, originalName: name } : {}),
|
||||
...(userId ? { createdBy: userId } : {}),
|
||||
...(isDefined(name) ? { name, originalName: name } : {}),
|
||||
...(isDefined(userId) ? { createdBy: userId } : {}),
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
@@ -246,6 +274,10 @@ async function restoreDocument({ documentId, organizationId, name, userId, db }:
|
||||
)
|
||||
.returning();
|
||||
|
||||
if (isNil(document)) {
|
||||
throw createDocumentNotFoundError();
|
||||
}
|
||||
|
||||
return { document };
|
||||
}
|
||||
|
||||
@@ -294,7 +326,7 @@ async function searchOrganizationDocuments({ organizationId, searchQuery, pageIn
|
||||
}
|
||||
|
||||
async function getOrganizationStats({ organizationId, db }: { organizationId: string; db: Database }) {
|
||||
const [{ documentsCount, documentsSize }] = await db
|
||||
const [record] = await db
|
||||
.select({
|
||||
documentsCount: count(documentsTable.id),
|
||||
documentsSize: sum(documentsTable.originalSize),
|
||||
@@ -307,6 +339,12 @@ async function getOrganizationStats({ organizationId, db }: { organizationId: st
|
||||
),
|
||||
);
|
||||
|
||||
if (isNil(record)) {
|
||||
throw createOrganizationNotFoundError();
|
||||
}
|
||||
|
||||
const { documentsCount, documentsSize } = record;
|
||||
|
||||
return {
|
||||
documentsCount,
|
||||
documentsSize: Number(documentsSize ?? 0),
|
||||
|
||||
@@ -7,6 +7,7 @@ import { organizationIdSchema } from '../organizations/organization.schemas';
|
||||
import { createOrganizationsRepository } from '../organizations/organizations.repository';
|
||||
import { ensureUserIsInOrganization } from '../organizations/organizations.usecases';
|
||||
import { createError } from '../shared/errors/errors';
|
||||
import { isNil } from '../shared/utils';
|
||||
import { validateFormData, validateJsonBody, validateParams, validateQuery } from '../shared/validation/validation';
|
||||
import { createWebhookRepository } from '../webhooks/webhook.repository';
|
||||
import { triggerWebhooks } from '../webhooks/webhook.usecases';
|
||||
@@ -38,7 +39,7 @@ function setupCreateDocumentRoute({ app, config, db, trackingServices }: RouteDe
|
||||
app.post(
|
||||
'/api/organizations/:organizationId/documents',
|
||||
requireAuthentication({ apiKeyPermissions: ['documents:create'] }),
|
||||
(context, next) => {
|
||||
async (context, next) => {
|
||||
const { maxUploadSize } = config.documentsStorage;
|
||||
|
||||
if (!isDocumentSizeLimitEnabled({ maxUploadSize })) {
|
||||
@@ -56,6 +57,7 @@ function setupCreateDocumentRoute({ app, config, db, trackingServices }: RouteDe
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line ts/no-unsafe-argument
|
||||
return middleware(context, next);
|
||||
},
|
||||
|
||||
@@ -72,7 +74,7 @@ function setupCreateDocumentRoute({ app, config, db, trackingServices }: RouteDe
|
||||
const { file, ocrLanguages } = context.req.valid('form');
|
||||
const { organizationId } = context.req.valid('param');
|
||||
|
||||
if (!file) {
|
||||
if (isNil(file)) {
|
||||
throw createError({
|
||||
message: 'No file provided, please upload a file using the "file" key.',
|
||||
code: 'document.no_file',
|
||||
|
||||
@@ -17,6 +17,7 @@ import pLimit from 'p-limit';
|
||||
import { checkIfOrganizationCanCreateNewDocument } from '../organizations/organizations.usecases';
|
||||
import { createPlansRepository } from '../plans/plans.repository';
|
||||
import { createLogger } from '../shared/logger/logger';
|
||||
import { isDefined } from '../shared/utils';
|
||||
import { createSubscriptionsRepository } from '../subscriptions/subscriptions.repository';
|
||||
import { createTaggingRulesRepository } from '../tagging-rules/tagging-rules.repository';
|
||||
import { applyTaggingRules } from '../tagging-rules/tagging-rules.usecases';
|
||||
@@ -100,28 +101,28 @@ export async function createDocument({
|
||||
|
||||
const { document } = existingDocument
|
||||
? await handleExistingDocument({
|
||||
existingDocument,
|
||||
fileName,
|
||||
organizationId,
|
||||
documentsRepository,
|
||||
tagsRepository,
|
||||
logger,
|
||||
})
|
||||
existingDocument,
|
||||
fileName,
|
||||
organizationId,
|
||||
documentsRepository,
|
||||
tagsRepository,
|
||||
logger,
|
||||
})
|
||||
: await createNewDocument({
|
||||
file,
|
||||
fileName,
|
||||
size,
|
||||
mimeType,
|
||||
hash,
|
||||
userId,
|
||||
organizationId,
|
||||
documentsRepository,
|
||||
documentsStorageService,
|
||||
generateDocumentId,
|
||||
trackingServices,
|
||||
ocrLanguages,
|
||||
logger,
|
||||
});
|
||||
file,
|
||||
fileName,
|
||||
size,
|
||||
mimeType,
|
||||
hash,
|
||||
userId,
|
||||
organizationId,
|
||||
documentsRepository,
|
||||
documentsStorageService,
|
||||
generateDocumentId,
|
||||
trackingServices,
|
||||
ocrLanguages,
|
||||
logger,
|
||||
});
|
||||
|
||||
deferRegisterDocumentActivityLog({
|
||||
documentId: document.id,
|
||||
@@ -195,7 +196,7 @@ async function handleExistingDocument({
|
||||
tagsRepository: TagsRepository;
|
||||
logger: Logger;
|
||||
}) {
|
||||
if (existingDocument && !existingDocument.isDeleted) {
|
||||
if (!existingDocument.isDeleted) {
|
||||
throw createDocumentAlreadyExistsError();
|
||||
}
|
||||
|
||||
@@ -277,7 +278,7 @@ async function createNewDocument({
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
if (isDefined(userId)) {
|
||||
trackingServices.captureUserEvent({ userId, event: 'Document created' });
|
||||
}
|
||||
|
||||
@@ -354,7 +355,7 @@ export async function deleteExpiredDocuments({
|
||||
const limit = pLimit(10);
|
||||
|
||||
await Promise.all(
|
||||
documents.map(document => limit(async () => {
|
||||
documents.map(async document => limit(async () => {
|
||||
const [, error] = await safely(hardDeleteDocument({ document, documentsRepository, documentsStorageService }));
|
||||
|
||||
if (error) {
|
||||
@@ -408,6 +409,6 @@ export async function deleteAllTrashDocuments({
|
||||
const limit = pLimit(10);
|
||||
|
||||
await Promise.all(
|
||||
documents.map(document => limit(() => hardDeleteDocument({ document, documentsRepository, documentsStorageService }))),
|
||||
documents.map(async document => limit(async () => hardDeleteDocument({ document, documentsRepository, documentsStorageService }))),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Config } from '../../config/config.types';
|
||||
import { createError } from '../../shared/errors/errors';
|
||||
import { isNil } from '../../shared/utils';
|
||||
import { AZ_BLOB_STORAGE_DRIVER_NAME, azBlobStorageDriverFactory } from './drivers/az-blob/az-blob.storage-driver';
|
||||
import { B2_STORAGE_DRIVER_NAME, b2StorageDriverFactory } from './drivers/b2/b2.storage-driver';
|
||||
import { FS_STORAGE_DRIVER_NAME, fsStorageDriverFactory } from './drivers/fs/fs.storage-driver';
|
||||
@@ -21,7 +22,7 @@ export async function createDocumentStorageService({ config }: { config: Config
|
||||
|
||||
const storageDriverFactory = storageDriverFactories[storageDriverName];
|
||||
|
||||
if (!storageDriverFactory) {
|
||||
if (isNil(storageDriverFactory)) {
|
||||
throw createError({
|
||||
message: `Unknown storage driver: ${storageDriverName}`,
|
||||
code: 'storage_driver.unknown_driver',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
import B2 from 'backblaze-b2';
|
||||
|
||||
import { isNil } from '../../../../shared/utils';
|
||||
import { createFileNotFoundError } from '../../document-storage.errors';
|
||||
import { defineStorageDriver } from '../drivers.models';
|
||||
|
||||
@@ -22,8 +23,10 @@ export const b2StorageDriverFactory = defineStorageDriver(async ({ config }) =>
|
||||
bucketId,
|
||||
});
|
||||
const upload = await b2Client.uploadFile({
|
||||
uploadUrl: getUploadUrl.data.uploadUrl,
|
||||
uploadAuthToken: getUploadUrl.data.authorizationToken,
|
||||
// eslint-disable-next-line ts/no-unsafe-member-access
|
||||
uploadUrl: getUploadUrl.data?.uploadUrl as string,
|
||||
// eslint-disable-next-line ts/no-unsafe-member-access
|
||||
uploadAuthToken: getUploadUrl.data?.authorizationToken as string,
|
||||
fileName: storageKey,
|
||||
data: Buffer.from(await file.arrayBuffer()),
|
||||
});
|
||||
@@ -34,15 +37,18 @@ export const b2StorageDriverFactory = defineStorageDriver(async ({ config }) =>
|
||||
},
|
||||
getFileStream: async ({ storageKey }) => {
|
||||
await b2Client.authorize();
|
||||
|
||||
const response = await b2Client.downloadFileByName({
|
||||
bucketName,
|
||||
fileName: storageKey,
|
||||
responseType: 'stream',
|
||||
});
|
||||
if (!response.data) {
|
||||
|
||||
if (isNil(response.data)) {
|
||||
throw createFileNotFoundError();
|
||||
}
|
||||
return { fileStream: response.data };
|
||||
|
||||
return { fileStream: response.data as ReadableStream };
|
||||
},
|
||||
deleteFile: async ({ storageKey }) => {
|
||||
await b2Client.hideFile({
|
||||
|
||||
@@ -94,7 +94,7 @@ describe('storage driver', () => {
|
||||
|
||||
const { fileStream } = await fsStorageDriver.getFileStream({ storageKey: 'org_1/text-file.txt' });
|
||||
|
||||
const chunks: Uint8Array[] = [];
|
||||
const chunks: unknown[] = [];
|
||||
for await (const chunk of fileStream) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('memory storage-driver', () => {
|
||||
const entries = Array.from(storage.entries());
|
||||
|
||||
expect(entries).to.have.length(1);
|
||||
const [key, file] = entries[0];
|
||||
const [key, file] = entries[0] as [string, File];
|
||||
|
||||
expect(key).to.eql('org_1/text-file.txt');
|
||||
expect(file).to.be.a('File');
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { Config } from '../config/config.types';
|
||||
import type { EmailDriverName } from './drivers/email-driver';
|
||||
import type { EmailDriverFactory } from './emails.types';
|
||||
import { createError } from '../shared/errors/errors';
|
||||
import { createLogger } from '../shared/logger/logger';
|
||||
import { isNil } from '../shared/utils';
|
||||
import { emailDrivers } from './drivers/email-driver';
|
||||
|
||||
export type EmailsServices = ReturnType<typeof createEmailsServices>;
|
||||
@@ -9,9 +11,9 @@ export type EmailsServices = ReturnType<typeof createEmailsServices>;
|
||||
export function createEmailsServices({ config }: { config: Config }) {
|
||||
const { driverName } = config.emails;
|
||||
|
||||
const emailDriver = emailDrivers[driverName as EmailDriverName];
|
||||
const emailDriver: EmailDriverFactory | undefined = emailDrivers[driverName as EmailDriverName];
|
||||
|
||||
if (!emailDriver) {
|
||||
if (isNil(emailDriver)) {
|
||||
throw createError({
|
||||
message: `Invalid email driver ${driverName}`,
|
||||
code: 'emails.invalid_driver',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { isAbsolute, join, parse, sep as pathSeparator, relative } from 'node:path';
|
||||
import { ORGANIZATION_ID_REGEX } from '../organizations/organizations.constants';
|
||||
import { isNil } from '../shared/utils';
|
||||
|
||||
export function normalizeFilePathToIngestionFolder({
|
||||
filePath,
|
||||
@@ -16,7 +17,7 @@ export function normalizeFilePathToIngestionFolder({
|
||||
export function getOrganizationIdFromFilePath({ relativeFilePath }: { relativeFilePath: string }) {
|
||||
const [maybeOrganizationId] = relativeFilePath.split(pathSeparator);
|
||||
|
||||
if (!maybeOrganizationId || !ORGANIZATION_ID_REGEX.test(maybeOrganizationId)) {
|
||||
if (isNil(maybeOrganizationId) || !ORGANIZATION_ID_REGEX.test(maybeOrganizationId)) {
|
||||
return { organizationId: undefined };
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,10 @@ export async function getFile({
|
||||
fs?: Pick<FsServices, 'readFile'>;
|
||||
}) {
|
||||
const buffer = await fs.readFile({ filePath });
|
||||
// OR pipes since lookup returns false if the mime type is not found
|
||||
const mimeType = mime.lookup(filePath) || 'application/octet-stream';
|
||||
// lookup returns false if the mime type is not found
|
||||
const lookedUpMimeType = mime.lookup(filePath);
|
||||
const mimeType = lookedUpMimeType === false ? 'application/octet-stream' : lookedUpMimeType;
|
||||
|
||||
const { base: fileName } = parse(filePath);
|
||||
|
||||
const file = new File([buffer], fileName, { type: mimeType });
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('ingestion-folders usecases', () => {
|
||||
|
||||
expect(files).to.have.length(1);
|
||||
|
||||
const [file] = files;
|
||||
const [file] = files as [File];
|
||||
|
||||
expect(file.name).to.equal('hello.md');
|
||||
expect(file.size).to.equal(11);
|
||||
@@ -175,7 +175,7 @@ describe('ingestion-folders usecases', () => {
|
||||
|
||||
expect(files).to.have.length(1);
|
||||
|
||||
const [file] = files;
|
||||
const [file] = files as [File];
|
||||
|
||||
expect(file.name).to.equal('hello.md');
|
||||
expect(file.size).to.equal(11);
|
||||
@@ -232,6 +232,7 @@ describe('ingestion-folders usecases', () => {
|
||||
ingestionFolder: {
|
||||
folderRootPath: '/apps/papra/ingestion',
|
||||
postProcessing: {
|
||||
// eslint-disable-next-line ts/no-unsafe-assignment
|
||||
strategy: 'unknown' as any,
|
||||
},
|
||||
},
|
||||
@@ -278,7 +279,7 @@ describe('ingestion-folders usecases', () => {
|
||||
|
||||
expect(files).to.have.length(1);
|
||||
|
||||
const [file] = files;
|
||||
const [file] = files as [File];
|
||||
|
||||
expect(file.name).to.equal('hello.md');
|
||||
expect(file.size).to.equal(11);
|
||||
@@ -303,6 +304,7 @@ describe('ingestion-folders usecases', () => {
|
||||
ingestionFolder: {
|
||||
folderRootPath: '/apps/papra/ingestion',
|
||||
postProcessing: {
|
||||
// eslint-disable-next-line ts/no-unsafe-assignment
|
||||
strategy: 'unknown' as any,
|
||||
},
|
||||
},
|
||||
@@ -477,7 +479,7 @@ describe('ingestion-folders usecases', () => {
|
||||
const documents = await db.select().from(documentsTable);
|
||||
|
||||
expect(documents).to.have.length(1);
|
||||
expect(documents[0].id).to.equal('doc_1');
|
||||
expect(documents[0]?.id).to.equal('doc_1');
|
||||
|
||||
// Check fs
|
||||
expect(vol.toJSON()).to.deep.equal({
|
||||
@@ -553,7 +555,7 @@ describe('ingestion-folders usecases', () => {
|
||||
const documents = await db.select().from(documentsTable);
|
||||
|
||||
expect(documents).to.have.length(1);
|
||||
expect(documents[0].id).to.equal('doc_1');
|
||||
expect(documents[0]?.id).to.equal('doc_1');
|
||||
|
||||
// Check fs
|
||||
expect(vol.toJSON()).to.deep.equal({
|
||||
|
||||
@@ -18,6 +18,7 @@ import { isErrorWithCode } from '../shared/errors/errors';
|
||||
import { createFsServices } from '../shared/fs/fs.services';
|
||||
import { createLogger } from '../shared/logger/logger';
|
||||
import { getRootDirPath } from '../shared/path';
|
||||
import { isNil } from '../shared/utils';
|
||||
import { addTimestampToFilename, getAbsolutePathFromFolderRelativeToOrganizationIngestionFolder, getOrganizationIdFromFilePath, isFileInDoneFolder, isFileInErrorFolder, normalizeFilePathToIngestionFolder } from './ingestion-folder.models';
|
||||
import { createInvalidPostProcessingStrategyError } from './ingestion-folders.errors';
|
||||
import { getFile } from './ingestion-folders.services';
|
||||
@@ -58,8 +59,8 @@ export function createIngestionFolderWatcher({
|
||||
ignored,
|
||||
},
|
||||
)
|
||||
.on('add', (fileMaybeCwdRelativePath) => {
|
||||
processingQueue.add(async () => {
|
||||
.on('add', async (fileMaybeCwdRelativePath) => {
|
||||
await processingQueue.add(async () => {
|
||||
const filePath = isAbsolute(fileMaybeCwdRelativePath) ? fileMaybeCwdRelativePath : join(cwd, fileMaybeCwdRelativePath);
|
||||
|
||||
logger.info({ filePath }, 'Processing file');
|
||||
@@ -116,7 +117,7 @@ export async function processFile({
|
||||
|
||||
const { organizationId } = await getFileOrganizationId({ filePath, ingestionFolderPath, organizationsRepository });
|
||||
|
||||
if (!organizationId) {
|
||||
if (isNil(organizationId)) {
|
||||
logger.warn({ filePath }, 'A file in the ingestion folder is not located in an organization ingestion folder, skipping');
|
||||
return;
|
||||
}
|
||||
@@ -149,7 +150,7 @@ export async function processFile({
|
||||
logger.info({ filePath }, 'Document not inserted because it already exists');
|
||||
}
|
||||
|
||||
if (result) {
|
||||
if (result?.document) {
|
||||
const { document } = result;
|
||||
|
||||
logger.info({ documentId: document.id }, 'Document imported from ingestion folder');
|
||||
@@ -195,12 +196,16 @@ async function getFileOrganizationId({ filePath, ingestionFolderPath, organizati
|
||||
|
||||
const { organizationId } = getOrganizationIdFromFilePath({ relativeFilePath });
|
||||
|
||||
if (!organizationId) {
|
||||
if (isNil(organizationId)) {
|
||||
return { organizationId: undefined };
|
||||
}
|
||||
|
||||
const { organization } = await organizationsRepository.getOrganizationById({ organizationId });
|
||||
|
||||
if (isNil(organization)) {
|
||||
return { organizationId: undefined };
|
||||
}
|
||||
|
||||
return { organizationId: organization.id };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { createError } from '../shared/errors/errors';
|
||||
import { isDefined, isNil } from '../shared/utils';
|
||||
|
||||
export function buildEmailAddress({
|
||||
username,
|
||||
domain,
|
||||
@@ -7,11 +10,20 @@ export function buildEmailAddress({
|
||||
domain: string;
|
||||
plusPart?: string;
|
||||
}) {
|
||||
return `${username}${plusPart ? `+${plusPart}` : ''}@${domain}`;
|
||||
return `${username}${isDefined(plusPart) ? `+${plusPart}` : ''}@${domain}`;
|
||||
}
|
||||
|
||||
export function parseEmailAddress({ email }: { email: string }) {
|
||||
const [fullUsername, domain] = email.split('@');
|
||||
|
||||
if (isNil(fullUsername) || isNil(domain)) {
|
||||
throw createError({
|
||||
message: 'Invalid email address',
|
||||
code: 'intake_emails.invalid_email_address',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const [username, ...plusParts] = fullUsername.split('+');
|
||||
const plusPart = plusParts.length > 0 ? plusParts.join('+') : undefined;
|
||||
|
||||
@@ -19,7 +31,7 @@ export function parseEmailAddress({ email }: { email: string }) {
|
||||
}
|
||||
|
||||
export function getEmailUsername({ email }: { email: string | undefined }) {
|
||||
if (!email) {
|
||||
if (isNil(email)) {
|
||||
return { username: undefined };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { Database } from '../app/database/database.types';
|
||||
import { injectArguments } from '@corentinth/chisels';
|
||||
import { and, count, eq } from 'drizzle-orm';
|
||||
import { createError } from '../shared/errors/errors';
|
||||
import { omitUndefined } from '../shared/utils';
|
||||
import { createIntakeEmailNotFoundError } from './intake-emails.errors';
|
||||
import { intakeEmailsTable } from './intake-emails.tables';
|
||||
|
||||
export type IntakeEmailsRepository = ReturnType<typeof createIntakeEmailsRepository>;
|
||||
@@ -24,6 +26,16 @@ export function createIntakeEmailsRepository({ db }: { db: Database }) {
|
||||
async function createIntakeEmail({ organizationId, emailAddress, db }: { organizationId: string; emailAddress: string; db: Database }) {
|
||||
const [intakeEmail] = await db.insert(intakeEmailsTable).values({ organizationId, emailAddress }).returning();
|
||||
|
||||
if (!intakeEmail) {
|
||||
// Very unlikely to happen as the insertion should throw an issue, it's for type safety
|
||||
throw createError({
|
||||
message: 'Error while creating intake email',
|
||||
code: 'intake-emails.create_error',
|
||||
statusCode: 500,
|
||||
isInternal: true,
|
||||
});
|
||||
}
|
||||
|
||||
return { intakeEmail };
|
||||
}
|
||||
|
||||
@@ -44,6 +56,10 @@ async function updateIntakeEmail({ intakeEmailId, organizationId, isEnabled, all
|
||||
)
|
||||
.returning();
|
||||
|
||||
if (!intakeEmail) {
|
||||
throw createIntakeEmailNotFoundError();
|
||||
}
|
||||
|
||||
return { intakeEmail };
|
||||
}
|
||||
|
||||
@@ -93,12 +109,18 @@ async function deleteIntakeEmail({ intakeEmailId, organizationId, db }: { intake
|
||||
}
|
||||
|
||||
async function getOrganizationIntakeEmailsCount({ organizationId, db }: { organizationId: string; db: Database }) {
|
||||
const [{ intakeEmailCount }] = await db
|
||||
const [record] = await db
|
||||
.select({ intakeEmailCount: count() })
|
||||
.from(intakeEmailsTable)
|
||||
.where(
|
||||
eq(intakeEmailsTable.organizationId, organizationId),
|
||||
);
|
||||
|
||||
if (!record) {
|
||||
throw createIntakeEmailNotFoundError();
|
||||
}
|
||||
|
||||
const { intakeEmailCount } = record;
|
||||
|
||||
return { intakeEmailCount };
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { createPlansRepository } from '../plans/plans.repository';
|
||||
import { createError } from '../shared/errors/errors';
|
||||
import { getHeader } from '../shared/headers/headers.models';
|
||||
import { createLogger } from '../shared/logger/logger';
|
||||
import { isNil } from '../shared/utils';
|
||||
import { validateFormData, validateJsonBody, validateParams } from '../shared/validation/validation';
|
||||
import { createSubscriptionsRepository } from '../subscriptions/subscriptions.repository';
|
||||
import { INTAKE_EMAILS_INGEST_ROUTE } from './intake-emails.constants';
|
||||
@@ -166,7 +167,7 @@ function setupIngestIntakeEmailRoute({ app, db, config, trackingServices }: Rout
|
||||
const bodyBuffer = await context.req.arrayBuffer();
|
||||
const signature = getHeader({ context, name: 'X-Signature' });
|
||||
|
||||
if (!signature) {
|
||||
if (isNil(signature)) {
|
||||
throw createError({
|
||||
message: 'Signature header is required',
|
||||
code: 'intake_emails.signature_header_required',
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { Config } from '../config/config.types';
|
||||
import type { IntakeEmailDriverName } from './drivers/intake-emails.drivers';
|
||||
import type { IntakeEmailDriverFactory } from './drivers/intake-emails.drivers.models';
|
||||
import { createError } from '../shared/errors/errors';
|
||||
import { isNil } from '../shared/utils';
|
||||
import { intakeEmailDrivers } from './drivers/intake-emails.drivers';
|
||||
|
||||
export function createIntakeEmailsServices({ config }: { config: Config }) {
|
||||
const intakeEmailDriver = intakeEmailDrivers[config.intakeEmails.driver as IntakeEmailDriverName];
|
||||
const intakeEmailDriver: IntakeEmailDriverFactory | undefined = intakeEmailDrivers[config.intakeEmails.driver as IntakeEmailDriverName];
|
||||
|
||||
if (!intakeEmailDriver) {
|
||||
if (isNil(intakeEmailDriver)) {
|
||||
throw createError({
|
||||
message: `Invalid intake email driver ${config.intakeEmails.driver}`,
|
||||
code: 'intake-emails.invalid_driver',
|
||||
|
||||
@@ -37,7 +37,7 @@ export async function createIntakeEmail({
|
||||
return { intakeEmail };
|
||||
}
|
||||
|
||||
export function processIntakeEmailIngestion({
|
||||
export async function processIntakeEmailIngestion({
|
||||
fromAddress,
|
||||
recipientsAddresses,
|
||||
attachments,
|
||||
@@ -51,7 +51,7 @@ export function processIntakeEmailIngestion({
|
||||
createDocument: CreateDocumentUsecase;
|
||||
}) {
|
||||
return Promise.all(
|
||||
recipientsAddresses.map(recipientAddress => safely(
|
||||
recipientsAddresses.map(async recipientAddress => safely(
|
||||
ingestEmailForRecipient({
|
||||
fromAddress,
|
||||
recipientAddress,
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import type { RouteDefinitionContext } from '../app/server.types';
|
||||
import z from 'zod';
|
||||
import { createForbiddenError } from '../app/auth/auth.errors';
|
||||
import { requireAuthentication } from '../app/auth/auth.middleware';
|
||||
import { getUser } from '../app/auth/auth.models';
|
||||
import { invitationIdSchema } from '../organizations/organization.schemas';
|
||||
import { ORGANIZATION_INVITATION_STATUS, ORGANIZATION_ROLES } from '../organizations/organizations.constants';
|
||||
import { createOrganizationsRepository } from '../organizations/organizations.repository';
|
||||
import { resendOrganizationInvitation } from '../organizations/organizations.usecases';
|
||||
import { createError } from '../shared/errors/errors';
|
||||
import { createLogger } from '../shared/logger/logger';
|
||||
import { validateParams } from '../shared/validation/validation';
|
||||
import { createUsersRepository } from '../users/users.repository';
|
||||
|
||||
const logger = createLogger({ namespace: 'invitations' });
|
||||
@@ -62,8 +65,11 @@ function setupAcceptInvitationRoute({ app, db }: RouteDefinitionContext) {
|
||||
app.post(
|
||||
'/api/invitations/:invitationId/accept',
|
||||
requireAuthentication(),
|
||||
validateParams(z.object({
|
||||
invitationId: invitationIdSchema,
|
||||
})),
|
||||
async (context) => {
|
||||
const { invitationId } = context.req.param();
|
||||
const { invitationId } = context.req.valid('param');
|
||||
const { userId } = getUser({ context });
|
||||
|
||||
const organizationsRepository = createOrganizationsRepository({ db });
|
||||
@@ -111,8 +117,11 @@ function setupRejectInvitationRoute({ app, db }: RouteDefinitionContext) {
|
||||
app.post(
|
||||
'/api/invitations/:invitationId/reject',
|
||||
requireAuthentication(),
|
||||
validateParams(z.object({
|
||||
invitationId: invitationIdSchema,
|
||||
})),
|
||||
async (context) => {
|
||||
const { invitationId } = context.req.param();
|
||||
const { invitationId } = context.req.valid('param');
|
||||
const { userId } = getUser({ context });
|
||||
|
||||
const organizationsRepository = createOrganizationsRepository({ db });
|
||||
@@ -146,8 +155,11 @@ function setupCancelInvitationRoute({ app, db }: RouteDefinitionContext) {
|
||||
app.post(
|
||||
'/api/invitations/:invitationId/cancel',
|
||||
requireAuthentication(),
|
||||
validateParams(z.object({
|
||||
invitationId: invitationIdSchema,
|
||||
})),
|
||||
async (context) => {
|
||||
const { invitationId } = context.req.param();
|
||||
const { invitationId } = context.req.valid('param');
|
||||
const { userId } = getUser({ context });
|
||||
|
||||
const organizationsRepository = createOrganizationsRepository({ db });
|
||||
@@ -179,8 +191,11 @@ function setupResendInvitationRoute({ app, db, config, emailsServices }: RouteDe
|
||||
app.post(
|
||||
'/api/invitations/:invitationId/resend',
|
||||
requireAuthentication(),
|
||||
validateParams(z.object({
|
||||
invitationId: invitationIdSchema,
|
||||
})),
|
||||
async (context) => {
|
||||
const { invitationId } = context.req.param();
|
||||
const { invitationId } = context.req.valid('param');
|
||||
const { userId } = getUser({ context });
|
||||
|
||||
const organizationsRepository = createOrganizationsRepository({ db });
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import { ORGANIZATION_ID_REGEX, ORGANIZATION_MEMBER_ID_REGEX } from './organizations.constants';
|
||||
import { ORGANIZATION_ID_REGEX, ORGANIZATION_INVITATION_ID_REGEX, ORGANIZATION_MEMBER_ID_REGEX } from './organizations.constants';
|
||||
|
||||
export const organizationIdSchema = z.string().regex(ORGANIZATION_ID_REGEX);
|
||||
export const memberIdSchema = z.string().regex(ORGANIZATION_MEMBER_ID_REGEX);
|
||||
export const invitationIdSchema = z.string().regex(ORGANIZATION_INVITATION_ID_REGEX);
|
||||
|
||||
@@ -6,6 +6,9 @@ export const ORGANIZATION_ID_REGEX = createPrefixedIdRegex({ prefix: ORGANIZATIO
|
||||
export const ORGANIZATION_MEMBER_ID_PREFIX = 'org_mem';
|
||||
export const ORGANIZATION_MEMBER_ID_REGEX = createPrefixedIdRegex({ prefix: ORGANIZATION_MEMBER_ID_PREFIX });
|
||||
|
||||
export const ORGANIZATION_INVITATION_ID_PREFIX = 'org_inv';
|
||||
export const ORGANIZATION_INVITATION_ID_REGEX = createPrefixedIdRegex({ prefix: ORGANIZATION_INVITATION_ID_PREFIX });
|
||||
|
||||
export const ORGANIZATION_ROLES = {
|
||||
MEMBER: 'member',
|
||||
OWNER: 'owner',
|
||||
|
||||
@@ -11,7 +11,7 @@ export function ensureInvitationStatus({ invitation, now = new Date() }: { invit
|
||||
return invitation;
|
||||
}
|
||||
|
||||
if (invitation.expiresAt && isAfter(invitation.expiresAt, now)) {
|
||||
if (isAfter(invitation.expiresAt, now)) {
|
||||
return invitation;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { omit } from 'lodash-es';
|
||||
import { omitUndefined } from '../shared/utils';
|
||||
import { usersTable } from '../users/users.table';
|
||||
import { ORGANIZATION_INVITATION_STATUS, ORGANIZATION_ROLES } from './organizations.constants';
|
||||
import { createOrganizationNotFoundError } from './organizations.errors';
|
||||
import { ensureInvitationStatus } from './organizations.repository.models';
|
||||
import { organizationInvitationsTable, organizationMembersTable, organizationsTable } from './organizations.table';
|
||||
|
||||
@@ -49,6 +50,12 @@ export function createOrganizationsRepository({ db }: { db: Database }) {
|
||||
async function saveOrganization({ organization: organizationToInsert, db }: { organization: DbInsertableOrganization; db: Database }) {
|
||||
const [organization] = await db.insert(organizationsTable).values(organizationToInsert).returning();
|
||||
|
||||
if (!organization) {
|
||||
// This should never happen, as the database should always return the inserted organization
|
||||
// guard for type safety
|
||||
throw new Error('Failed to save organization');
|
||||
}
|
||||
|
||||
return { organization };
|
||||
}
|
||||
|
||||
@@ -110,7 +117,7 @@ async function getOrganizationById({ organizationId, db }: { organizationId: str
|
||||
}
|
||||
|
||||
async function getUserOwnedOrganizationCount({ userId, db }: { userId: string; db: Database }) {
|
||||
const [{ organizationCount }] = await db
|
||||
const [record] = await db
|
||||
.select({
|
||||
organizationCount: count(organizationMembersTable.id),
|
||||
})
|
||||
@@ -122,13 +129,19 @@ async function getUserOwnedOrganizationCount({ userId, db }: { userId: string; d
|
||||
),
|
||||
);
|
||||
|
||||
if (!record) {
|
||||
throw createOrganizationNotFoundError();
|
||||
}
|
||||
|
||||
const { organizationCount } = record;
|
||||
|
||||
return {
|
||||
organizationCount,
|
||||
};
|
||||
}
|
||||
|
||||
async function getOrganizationOwner({ organizationId, db }: { organizationId: string; db: Database }) {
|
||||
const [{ organizationOwner }] = await db
|
||||
const [record] = await db
|
||||
.select({
|
||||
organizationOwner: getTableColumns(usersTable),
|
||||
})
|
||||
@@ -141,11 +154,17 @@ async function getOrganizationOwner({ organizationId, db }: { organizationId: st
|
||||
),
|
||||
);
|
||||
|
||||
if (!record) {
|
||||
throw createOrganizationNotFoundError();
|
||||
}
|
||||
|
||||
const { organizationOwner } = record;
|
||||
|
||||
return { organizationOwner };
|
||||
}
|
||||
|
||||
async function getOrganizationMembersCount({ organizationId, db }: { organizationId: string; db: Database }) {
|
||||
const [{ membersCount }] = await db
|
||||
const [record] = await db
|
||||
.select({
|
||||
membersCount: count(organizationMembersTable.id),
|
||||
})
|
||||
@@ -154,6 +173,12 @@ async function getOrganizationMembersCount({ organizationId, db }: { organizatio
|
||||
eq(organizationMembersTable.organizationId, organizationId),
|
||||
);
|
||||
|
||||
if (!record) {
|
||||
throw createOrganizationNotFoundError();
|
||||
}
|
||||
|
||||
const { membersCount } = record;
|
||||
|
||||
return {
|
||||
membersCount,
|
||||
};
|
||||
@@ -268,7 +293,7 @@ async function saveOrganizationInvitation({
|
||||
}
|
||||
|
||||
async function getTodayUserInvitationCount({ userId, db, now = new Date() }: { userId: string; db: Database; now?: Date }) {
|
||||
const [{ userInvitationCount }] = await db
|
||||
const [record] = await db
|
||||
.select({
|
||||
userInvitationCount: count(organizationInvitationsTable.id),
|
||||
})
|
||||
@@ -280,6 +305,12 @@ async function getTodayUserInvitationCount({ userId, db, now = new Date() }: { u
|
||||
),
|
||||
);
|
||||
|
||||
if (!record) {
|
||||
throw createOrganizationNotFoundError();
|
||||
}
|
||||
|
||||
const { userInvitationCount } = record;
|
||||
|
||||
return {
|
||||
userInvitationCount,
|
||||
};
|
||||
@@ -335,7 +366,7 @@ async function updateOrganizationInvitation({ invitationId, status, expiresAt, d
|
||||
}
|
||||
|
||||
async function getPendingInvitationsCount({ email, db, now = new Date() }: { email: string; db: Database; now?: Date }) {
|
||||
const [{ pendingInvitationsCount }] = await db
|
||||
const [record] = await db
|
||||
.select({
|
||||
pendingInvitationsCount: count(organizationInvitationsTable.id),
|
||||
})
|
||||
@@ -349,6 +380,12 @@ async function getPendingInvitationsCount({ email, db, now = new Date() }: { ema
|
||||
),
|
||||
);
|
||||
|
||||
if (!record) {
|
||||
throw createOrganizationNotFoundError();
|
||||
}
|
||||
|
||||
const { pendingInvitationsCount } = record;
|
||||
|
||||
return {
|
||||
pendingInvitationsCount,
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ORGANIZATION_ROLES } from './organizations.constants';
|
||||
import { createOrganizationsRepository } from './organizations.repository';
|
||||
import { checkIfUserCanCreateNewOrganization, createOrganization, ensureUserIsInOrganization, inviteMemberToOrganization, removeMemberFromOrganization, updateOrganizationMemberRole } from './organizations.usecases';
|
||||
|
||||
export async function registerOrganizationsRoutes(context: RouteDefinitionContext) {
|
||||
export function registerOrganizationsRoutes(context: RouteDefinitionContext) {
|
||||
setupGetOrganizationsRoute(context);
|
||||
setupCreateOrganizationRoute(context);
|
||||
setupGetOrganizationRoute(context);
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { OrganizationInvitationStatus, OrganizationRole } from './organizat
|
||||
import { integer, sqliteTable, text, unique } from 'drizzle-orm/sqlite-core';
|
||||
import { createPrimaryKeyField, createTimestampColumns } from '../shared/db/columns.helpers';
|
||||
import { usersTable } from '../users/users.table';
|
||||
import { ORGANIZATION_ID_PREFIX, ORGANIZATION_INVITATION_STATUS, ORGANIZATION_INVITATION_STATUS_LIST, ORGANIZATION_MEMBER_ID_PREFIX, ORGANIZATION_ROLES_LIST } from './organizations.constants';
|
||||
import { ORGANIZATION_ID_PREFIX, ORGANIZATION_INVITATION_ID_PREFIX, ORGANIZATION_INVITATION_STATUS, ORGANIZATION_INVITATION_STATUS_LIST, ORGANIZATION_MEMBER_ID_PREFIX, ORGANIZATION_ROLES_LIST } from './organizations.constants';
|
||||
|
||||
export const organizationsTable = sqliteTable('organizations', {
|
||||
...createPrimaryKeyField({ prefix: ORGANIZATION_ID_PREFIX }),
|
||||
@@ -31,7 +31,7 @@ export const organizationMembersTable = sqliteTable('organization_members', {
|
||||
]);
|
||||
|
||||
export const organizationInvitationsTable = sqliteTable('organization_invitations', {
|
||||
...createPrimaryKeyField({ prefix: 'org_inv' }),
|
||||
...createPrimaryKeyField({ prefix: ORGANIZATION_INVITATION_ID_PREFIX }),
|
||||
...createTimestampColumns(),
|
||||
|
||||
organizationId: text('organization_id').notNull().references(() => organizationsTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
|
||||
@@ -379,7 +379,7 @@ describe('organizations usecases', () => {
|
||||
const remainingMembers = await db.select().from(organizationMembersTable);
|
||||
|
||||
expect(remainingMembers.length).to.equal(1);
|
||||
expect(remainingMembers[0].id).to.equal('member-1');
|
||||
expect(remainingMembers[0]?.id).to.equal('member-1');
|
||||
});
|
||||
|
||||
test('a member (not admin nor owner) cannot remove anyone from the organization', async () => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { createForbiddenError } from '../app/auth/auth.errors';
|
||||
import { getOrganizationPlan } from '../plans/plans.usecases';
|
||||
import { sanitize } from '../shared/html/html';
|
||||
import { createLogger } from '../shared/logger/logger';
|
||||
import { isDefined } from '../shared/utils';
|
||||
import { ORGANIZATION_INVITATION_STATUS, ORGANIZATION_ROLES } from './organizations.constants';
|
||||
import {
|
||||
createOrganizationDocumentStorageLimitReachedError,
|
||||
@@ -113,7 +114,7 @@ export async function getOrCreateOrganizationCustomerId({
|
||||
throw createOrganizationNotFoundError();
|
||||
}
|
||||
|
||||
if (organization.customerId) {
|
||||
if (isDefined(organization.customerId)) {
|
||||
return { customerId: organization.customerId };
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('plans repository', () => {
|
||||
|
||||
const { organizationPlans } = getOrganizationPlansRecords({ config });
|
||||
|
||||
expect(organizationPlans[FREE_PLAN_ID].limits).to.deep.equal({
|
||||
expect(organizationPlans[FREE_PLAN_ID]!.limits).to.deep.equal({
|
||||
maxDocumentStorageBytes: Number.POSITIVE_INFINITY,
|
||||
maxIntakeEmailsCount: Number.POSITIVE_INFINITY,
|
||||
maxOrganizationsMembersCount: Number.POSITIVE_INFINITY,
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { areSomeRolesInJwtPayload } from './roles.models';
|
||||
|
||||
describe('roles models', () => {
|
||||
describe('areSomeRolesInJwtPayload', () => {
|
||||
test('check if at least one roles in the jwtPayload match at least one of the expected roles', () => {
|
||||
expect(areSomeRolesInJwtPayload({
|
||||
roles: ['admin'],
|
||||
jwtPayload: {
|
||||
roles: ['admin', 'user'],
|
||||
},
|
||||
})).to.eql(true);
|
||||
|
||||
expect(areSomeRolesInJwtPayload({
|
||||
roles: ['admin'],
|
||||
jwtPayload: {
|
||||
roles: [],
|
||||
},
|
||||
})).to.eql(false);
|
||||
});
|
||||
|
||||
test('when the roles field in the jwtPayload is not a string array, it should return false', () => {
|
||||
expect(areSomeRolesInJwtPayload({
|
||||
roles: ['admin'],
|
||||
jwtPayload: {
|
||||
roles: 'admin',
|
||||
},
|
||||
})).to.eql(false);
|
||||
|
||||
expect(areSomeRolesInJwtPayload({
|
||||
roles: ['admin'],
|
||||
jwtPayload: {
|
||||
roles: undefined,
|
||||
},
|
||||
})).to.eql(false);
|
||||
|
||||
expect(areSomeRolesInJwtPayload({
|
||||
roles: ['admin'],
|
||||
jwtPayload: {
|
||||
roles: ['admin', 123],
|
||||
},
|
||||
})).to.eql(false);
|
||||
|
||||
expect(areSomeRolesInJwtPayload({
|
||||
roles: ['admin'],
|
||||
jwtPayload: {},
|
||||
})).to.eql(false);
|
||||
|
||||
expect(areSomeRolesInJwtPayload({
|
||||
roles: ['admin'],
|
||||
jwtPayload: undefined,
|
||||
})).to.eql(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import { get, isArray, isString } from 'lodash-es';
|
||||
|
||||
export function areSomeRolesInJwtPayload({ roles, jwtPayload }: { roles: string[]; jwtPayload?: Record<string, unknown> }) {
|
||||
const rolesInJwt = get(jwtPayload, 'roles');
|
||||
|
||||
if (!rolesInJwt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isArray(rolesInJwt) || !rolesInJwt.every(isString)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return rolesInJwt.some(role => roles.includes(role));
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
export const nextTick = () => new Promise(resolve => setImmediate(resolve));
|
||||
export const nextTick = async () => new Promise(resolve => setImmediate(resolve));
|
||||
|
||||
@@ -13,5 +13,5 @@ export function safelyDefer(fn: () => Promise<void>, { logger = createLogger({ n
|
||||
}
|
||||
|
||||
export function createDeferable<Args extends unknown[]>(fn: (...args: Args) => Promise<void>) {
|
||||
return (...args: Args) => safelyDefer(() => fn(...args));
|
||||
return (...args: Args) => safelyDefer(async () => fn(...args));
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ describe('errors', () => {
|
||||
expect(error.message).to.eql('foo');
|
||||
expect(error.code).to.eql('bar');
|
||||
expect(error.statusCode).to.eql(500);
|
||||
expect(error.isInternal).to.eql(false);
|
||||
});
|
||||
|
||||
test('accepts an optional cause property to attach the original error that caused the custom error', () => {
|
||||
|
||||
@@ -16,9 +16,9 @@ class CustomError extends Error {
|
||||
cause?: Error | null;
|
||||
statusCode: ContentfulStatusCode;
|
||||
isCustomError = true;
|
||||
isInternal?: boolean;
|
||||
isInternal: boolean = false;
|
||||
|
||||
constructor({ message, code, cause, statusCode, isInternal }: ErrorOptions) {
|
||||
constructor({ message, code, cause, statusCode, isInternal = false }: ErrorOptions) {
|
||||
super(message);
|
||||
|
||||
this.code = code;
|
||||
|
||||
@@ -51,7 +51,7 @@ export async function moveFile({ sourceFilePath, destinationFilePath, fs = fsNat
|
||||
}
|
||||
|
||||
export async function readFile({ filePath, fs = fsNative }: { filePath: string; fs?: FsNative }) {
|
||||
return await fs.readFile(filePath);
|
||||
return fs.readFile(filePath);
|
||||
}
|
||||
|
||||
export async function areFilesContentIdentical({ file1, file2, fs = fsNative }: { file1: string; file2: string; fs?: FsNative }): Promise<boolean> {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Context } from '../../app/server.types';
|
||||
import { createMiddleware } from 'hono/factory';
|
||||
import { getHeader } from '../headers/headers.models';
|
||||
import { generateId } from '../random/ids';
|
||||
@@ -6,7 +7,7 @@ import { createLogger, wrapWithLoggerContext } from './logger';
|
||||
const logger = createLogger({ namespace: 'app' });
|
||||
|
||||
export function createLoggerMiddleware() {
|
||||
return createMiddleware(async (context, next) => {
|
||||
return createMiddleware(async (context: Context, next) => {
|
||||
const requestId = getHeader({ context, name: 'x-request-id' });
|
||||
|
||||
await wrapWithLoggerContext(
|
||||
|
||||
@@ -6,7 +6,7 @@ export const getRootDirPath = memoize(() => process.cwd());
|
||||
// Working with libsql, file url can be relative, which is not correct according to RFC 8089, which standardizes the `file` scheme
|
||||
// see https://github.com/tursodatabase/libsql-client-ts/blob/ee036574f5c23335c8b2d6d0c0e117cbe14bf376/packages/libsql-core/src/uri.ts
|
||||
export function fileUrlToPath({ fileUrl }: { fileUrl: string }) {
|
||||
const rawPath = fileUrl.replace(/^file:(\/\/)?/, '');
|
||||
const rawPath = fileUrl.replace(/^file:(?:\/\/)?/, '');
|
||||
|
||||
return decodeURIComponent(rawPath);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ const createId = init({ length: ID_RANDOM_PART_LENGTH });
|
||||
export function generateId({ prefix, getRandomPart = createId }: { prefix?: string; getRandomPart?: () => string } = {}) {
|
||||
const id = getRandomPart();
|
||||
|
||||
return prefix ? `${prefix}_${id}` : id;
|
||||
return prefix !== undefined ? `${prefix}_${id}` : id;
|
||||
}
|
||||
|
||||
export function createPrefixedIdRegex({ prefix }: { prefix: string }) {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export async function collectReadableStreamToString({ stream }: { stream: ReadableStream }) {
|
||||
return await new Response(stream).text();
|
||||
return new Response(stream).text();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { omitUndefined } from './utils';
|
||||
import { isDefined, isNil, omitUndefined } from './utils';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('omitUndefined', () => {
|
||||
@@ -22,4 +22,29 @@ describe('utils', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNil', () => {
|
||||
test('a value is considered nil if it is either undefined or null', () => {
|
||||
expect(isNil(undefined)).toBe(true);
|
||||
expect(isNil(null)).toBe(true);
|
||||
|
||||
expect(isNil(0)).toBe(false);
|
||||
expect(isNil('')).toBe(false);
|
||||
expect(isNil(false)).toBe(false);
|
||||
expect(isNil({})).toBe(false);
|
||||
expect(isNil([])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDefined', () => {
|
||||
test('a value is considered defined if it is not undefined or null', () => {
|
||||
expect(isDefined(undefined)).toBe(false);
|
||||
expect(isDefined(null)).toBe(false);
|
||||
|
||||
expect(isDefined(0)).toBe(true);
|
||||
expect(isDefined('')).toBe(true);
|
||||
expect(isDefined(false)).toBe(true);
|
||||
expect(isDefined({})).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,3 +7,11 @@ type OmitUndefined<T> = {
|
||||
export function omitUndefined<T extends Record<string, any>>(obj: T): OmitUndefined<T> {
|
||||
return omitBy(obj, isUndefined) as OmitUndefined<T>;
|
||||
}
|
||||
|
||||
export function isNil(value: unknown): value is undefined | null {
|
||||
return value === undefined || value === null;
|
||||
}
|
||||
|
||||
export function isDefined<T>(value: T): value is Exclude<T, undefined | null> {
|
||||
return !isNil(value);
|
||||
}
|
||||
|
||||
@@ -17,11 +17,13 @@ function buildValidator<Target extends keyof ValidationTargets>({ target, error
|
||||
return <Schema extends z.ZodTypeAny>(schema: Schema, { allowAdditionalFields = false }: { allowAdditionalFields?: boolean } = {}) => {
|
||||
return validator(target, (value, context) => {
|
||||
// @ts-expect-error try to enforce strict mode
|
||||
const refinedSchema = allowAdditionalFields ? schema : (schema.strict?.() ?? schema);
|
||||
// eslint-disable-next-line ts/no-unsafe-assignment, ts/no-unsafe-call
|
||||
const refinedSchema: Schema = allowAdditionalFields ? schema : (schema.strict?.() ?? schema);
|
||||
|
||||
const result = refinedSchema.safeParse(value);
|
||||
|
||||
if (result.success) {
|
||||
// eslint-disable-next-line ts/no-unsafe-return
|
||||
return result.data as z.infer<Schema>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { coerceStripeTimestampToDate } from './subscriptions.models';
|
||||
import { coerceStripeTimestampToDate, isSignatureHeaderFormatValid } from './subscriptions.models';
|
||||
|
||||
describe('subscriptions models', () => {
|
||||
describe('coerceStripeTimestampToDate', () => {
|
||||
@@ -10,4 +10,13 @@ describe('subscriptions models', () => {
|
||||
expect(date).to.deep.equal(new Date('2024-05-19T20:26:23.000Z'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSignatureHeaderFormatValid', () => {
|
||||
test('the signature value should be a non empty string', () => {
|
||||
expect(isSignatureHeaderFormatValid(undefined)).toBe(false);
|
||||
expect(isSignatureHeaderFormatValid('')).toBe(false);
|
||||
|
||||
expect(isSignatureHeaderFormatValid('v1_1234567890')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
import { isNil } from '../shared/utils';
|
||||
|
||||
export function coerceStripeTimestampToDate(timestamp: number) {
|
||||
return new Date(timestamp * 1000);
|
||||
}
|
||||
|
||||
export function isSignatureHeaderFormatValid(signature: string | undefined): signature is string {
|
||||
if (isNil(signature)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return typeof signature === 'string' && signature.length > 0;
|
||||
}
|
||||
|
||||
@@ -13,8 +13,10 @@ import { getOrganizationPlan } from '../plans/plans.usecases';
|
||||
import { createError } from '../shared/errors/errors';
|
||||
import { getHeader } from '../shared/headers/headers.models';
|
||||
import { createLogger } from '../shared/logger/logger';
|
||||
import { isNil } from '../shared/utils';
|
||||
import { validateJsonBody, validateParams } from '../shared/validation/validation';
|
||||
import { createInvalidWebhookPayloadError, createOrganizationAlreadyHasSubscriptionError } from './subscriptions.errors';
|
||||
import { isSignatureHeaderFormatValid } from './subscriptions.models';
|
||||
import { createSubscriptionsRepository } from './subscriptions.repository';
|
||||
import { handleStripeWebhookEvent } from './subscriptions.usecases';
|
||||
|
||||
@@ -31,7 +33,7 @@ function setupStripeWebhookRoute({ app, config, db, subscriptionsServices }: Rou
|
||||
app.post('/api/stripe/webhook', async (context) => {
|
||||
const signature = getHeader({ context, name: 'stripe-signature' });
|
||||
|
||||
if (!signature) {
|
||||
if (!isSignatureHeaderFormatValid(signature)) {
|
||||
throw createInvalidWebhookPayloadError();
|
||||
}
|
||||
|
||||
@@ -59,7 +61,7 @@ function setupStripeWebhookRoute({ app, config, db, subscriptionsServices }: Rou
|
||||
});
|
||||
}
|
||||
|
||||
async function setupCreateCheckoutSessionRoute({ app, config, db, subscriptionsServices }: RouteDefinitionContext) {
|
||||
function setupCreateCheckoutSessionRoute({ app, config, db, subscriptionsServices }: RouteDefinitionContext) {
|
||||
app.post(
|
||||
'/api/organizations/:organizationId/checkout-session',
|
||||
requireAuthentication(),
|
||||
@@ -99,7 +101,7 @@ async function setupCreateCheckoutSessionRoute({ app, config, db, subscriptionsS
|
||||
|
||||
const { organizationPlan: organizationPlanToSubscribeTo } = await plansRepository.getOrganizationPlanById({ planId });
|
||||
|
||||
if (!organizationPlanToSubscribeTo.priceId) {
|
||||
if (isNil(organizationPlanToSubscribeTo.priceId)) {
|
||||
// Very unlikely to happen, as only the free plan does not have a price ID, and we check for the plans in the route validation
|
||||
// but for type safety, we assert that the price ID is set
|
||||
throw createError({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Buffer } from 'node:buffer';
|
||||
import type { Config } from '../config/config.types';
|
||||
import { buildUrl, injectArguments } from '@corentinth/chisels';
|
||||
import Stripe from 'stripe';
|
||||
@@ -69,7 +70,7 @@ export async function createCheckoutUrl({
|
||||
return { checkoutUrl: session.url };
|
||||
}
|
||||
|
||||
async function parseWebhookEvent({ stripeClient, payload, signature, config }: { stripeClient: Stripe; payload: any; signature: string; config: Config }) {
|
||||
async function parseWebhookEvent({ stripeClient, payload, signature, config }: { stripeClient: Stripe; payload: string | Buffer; signature: string; config: Config }) {
|
||||
const event = await stripeClient.webhooks.constructEventAsync(payload, signature, config.subscriptions.stripeWebhookSecret);
|
||||
|
||||
return { event };
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { PlansRepository } from '../plans/plans.repository';
|
||||
import type { SubscriptionsRepository } from './subscriptions.repository';
|
||||
import { get } from 'lodash-es';
|
||||
import { createOrganizationNotFoundError } from '../organizations/organizations.errors';
|
||||
import { isNil } from '../shared/utils';
|
||||
import { coerceStripeTimestampToDate } from './subscriptions.models';
|
||||
|
||||
export async function handleStripeWebhookEvent({
|
||||
@@ -23,7 +24,7 @@ export async function handleStripeWebhookEvent({
|
||||
const cancelAtPeriodEnd = get(event, 'data.object.cancel_at_period_end');
|
||||
const status = get(event, 'data.object.status');
|
||||
|
||||
if (!organizationId) {
|
||||
if (isNil(organizationId)) {
|
||||
throw createOrganizationNotFoundError();
|
||||
}
|
||||
|
||||
@@ -52,7 +53,7 @@ export async function handleStripeWebhookEvent({
|
||||
const cancelAtPeriodEnd = get(event, 'data.object.cancel_at_period_end');
|
||||
const status = get(event, 'data.object.status');
|
||||
|
||||
if (!organizationId) {
|
||||
if (isNil(organizationId)) {
|
||||
throw createOrganizationNotFoundError();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Document } from '../documents/documents.types';
|
||||
import { get } from 'lodash-es';
|
||||
|
||||
export function getDocumentFieldValue({ document, field }: { document: Document; field: string }) {
|
||||
const fieldValue = get(document, field);
|
||||
const fieldValue: unknown = get(document, field);
|
||||
|
||||
return { fieldValue };
|
||||
return { fieldValue: String(fieldValue ?? '') };
|
||||
}
|
||||
|
||||
@@ -69,6 +69,12 @@ async function getOrganizationEnabledTaggingRules({ organizationId, db }: { orga
|
||||
async function createTaggingRule({ taggingRule, db }: { taggingRule: DbInsertableTaggingRule; db: Database }) {
|
||||
const [createdTaggingRule] = await db.insert(taggingRulesTable).values(taggingRule).returning();
|
||||
|
||||
if (!createdTaggingRule) {
|
||||
// Very unlikely to happen as the query will throw an error if the tagging rule is not created
|
||||
// it's for type safety
|
||||
throw new Error('Failed to create tagging rule');
|
||||
}
|
||||
|
||||
return { taggingRule: createdTaggingRule };
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, test } from 'vitest';
|
||||
import { createInMemoryDatabase } from '../app/database/database.test-utils';
|
||||
import { documentsTable } from '../documents/documents.table';
|
||||
import { createTestLogger } from '../shared/logger/logger.test-utils';
|
||||
import { isNil } from '../shared/utils';
|
||||
import { createTagsRepository } from '../tags/tags.repository';
|
||||
import { documentsTagsTable } from '../tags/tags.table';
|
||||
import { createTaggingRulesRepository } from './tagging-rules.repository';
|
||||
@@ -25,6 +26,11 @@ describe('tagging-rules usecases', () => {
|
||||
|
||||
const [document] = await db.select().from(documentsTable).where(eq(documentsTable.id, 'doc_1'));
|
||||
|
||||
if (isNil(document)) {
|
||||
// type safety
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
const taggingRulesRepository = createTaggingRulesRepository({ db });
|
||||
const tagsRepository = createTagsRepository({ db });
|
||||
|
||||
@@ -60,6 +66,11 @@ describe('tagging-rules usecases', () => {
|
||||
|
||||
const [document] = await db.select().from(documentsTable).where(eq(documentsTable.id, 'doc_1'));
|
||||
|
||||
if (isNil(document)) {
|
||||
// type safety
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
const taggingRulesRepository = createTaggingRulesRepository({ db });
|
||||
const tagsRepository = createTagsRepository({ db });
|
||||
|
||||
@@ -78,6 +89,11 @@ describe('tagging-rules usecases', () => {
|
||||
|
||||
const [document] = await db.select().from(documentsTable).where(eq(documentsTable.id, 'doc_1'));
|
||||
|
||||
if (isNil(document)) {
|
||||
// type safety
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
const taggingRulesRepository = createTaggingRulesRepository({ db });
|
||||
const tagsRepository = createTagsRepository({ db });
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ export async function applyTaggingRules({
|
||||
const tagIdsToApply: string[] = uniq(taggingRulesToApplyActions.flatMap(taggingRule => taggingRule.actions.map(action => action.tagId)));
|
||||
|
||||
const appliedTagIds = await Promise.all(tagIdsToApply.map(async (tagId) => {
|
||||
const [, error] = await safely(() => tagsRepository.addTagToDocument({ tagId, documentId: document.id }));
|
||||
const [, error] = await safely(async () => tagsRepository.addTagToDocument({ tagId, documentId: document.id }));
|
||||
|
||||
if (error) {
|
||||
logger.error({ error, tagId, documentId: document.id }, 'Failed to add tag to document');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { createInMemoryDatabase } from '../app/database/database.test-utils';
|
||||
import { ORGANIZATION_ROLES } from '../organizations/organizations.constants';
|
||||
import { isNil } from '../shared/utils';
|
||||
import { createDocumentAlreadyHasTagError, createTagAlreadyExistsError } from './tags.errors';
|
||||
import { createTagsRepository } from './tags.repository';
|
||||
|
||||
@@ -17,6 +18,11 @@ describe('tags repository', () => {
|
||||
tag: { organizationId: 'organization-1', name: 'Tag 1', color: '#aa0000' },
|
||||
});
|
||||
|
||||
if (isNil(tag1)) {
|
||||
// type safety
|
||||
throw new Error('Tag 1 not found');
|
||||
}
|
||||
|
||||
expect(tag1).to.include({
|
||||
organizationId: 'organization-1',
|
||||
name: 'Tag 1',
|
||||
|
||||
@@ -29,12 +29,12 @@ function createTaskScheduler({
|
||||
|
||||
const task = cron.schedule(
|
||||
cronSchedule,
|
||||
() => wrapWithLoggerContext(
|
||||
async () => wrapWithLoggerContext(
|
||||
{
|
||||
taskId: generateId({ prefix: 'task' }),
|
||||
taskName: taskDefinition.taskName,
|
||||
},
|
||||
() => taskDefinition.run({ ...tasksArgs, config }),
|
||||
async () => taskDefinition.run({ ...tasksArgs, config }),
|
||||
),
|
||||
{
|
||||
scheduled: false,
|
||||
|
||||
@@ -14,7 +14,7 @@ export type TrackingServices = {
|
||||
export function createDummyTrackingServices(): TrackingServices {
|
||||
return {
|
||||
captureUserEvent: () => {},
|
||||
shutdown: () => Promise.resolve(),
|
||||
shutdown: async () => Promise.resolve(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,6 +37,6 @@ export function createTrackingServices({ config }: { config: Config }): Tracking
|
||||
captureUserEvent: ({ userId, event, properties }) => {
|
||||
trackingClient.capture({ distinctId: userId, event, properties });
|
||||
},
|
||||
shutdown: () => trackingClient.shutdown(),
|
||||
shutdown: async () => trackingClient.shutdown(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { createRolesRepository } from '../roles/roles.repository';
|
||||
import { validateJsonBody } from '../shared/validation/validation';
|
||||
import { createUsersRepository } from './users.repository';
|
||||
|
||||
export async function registerUsersRoutes(context: RouteDefinitionContext) {
|
||||
export function registerUsersRoutes(context: RouteDefinitionContext) {
|
||||
setupGetCurrentUserRoute(context);
|
||||
setupUpdateUserRoute(context);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,13 @@ export function createWebhookRepository({ db }: { db: Database }) {
|
||||
async function createOrganizationWebhook({ db, ...webhook }: { db: Database } & { name: string; url: string; secret?: string; enabled?: boolean; events?: EventName[]; organizationId: string; createdBy: string }) {
|
||||
const [createdWebhook] = await db.insert(webhooksTable).values(webhook).returning();
|
||||
|
||||
if (webhook.events?.length) {
|
||||
if (!createdWebhook) {
|
||||
// Very unlikely to happen as the query will throw an error if the webhook is not created
|
||||
// it's for type safety
|
||||
throw new Error('Failed to create webhook');
|
||||
}
|
||||
|
||||
if (webhook.events && webhook.events.length > 0) {
|
||||
await db
|
||||
.insert(webhookEventsTable)
|
||||
.values(
|
||||
@@ -87,6 +93,10 @@ async function getOrganizationWebhookById({ db, webhookId, organizationId }: { d
|
||||
),
|
||||
);
|
||||
|
||||
if (!records.length) {
|
||||
return { webhook: undefined };
|
||||
}
|
||||
|
||||
const [{ webhook } = {}] = records;
|
||||
const events = records.map(record => record.webhookEvents?.eventName);
|
||||
|
||||
|
||||
@@ -99,8 +99,8 @@ export async function triggerWebhooks({
|
||||
const limit = pLimit(10);
|
||||
|
||||
await Promise.all(
|
||||
webhooks.map(webhook =>
|
||||
limit(() =>
|
||||
webhooks.map(async webhook =>
|
||||
limit(async () =>
|
||||
triggerWebhook({ webhook, webhookRepository, now, ...webhookData, logger, triggerWebhookService }),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -15,7 +15,7 @@ async function runScript(
|
||||
) {
|
||||
const isDryRun = process.argv.includes('--dry-run');
|
||||
|
||||
wrapWithLoggerContext(
|
||||
await wrapWithLoggerContext(
|
||||
{
|
||||
scriptName,
|
||||
isDryRun,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { runMigrations } from '../modules/app/database/database.services';
|
||||
import { runScript } from './commons/run-script';
|
||||
|
||||
runScript(
|
||||
await runScript(
|
||||
{ scriptName: 'migrate-up' },
|
||||
async ({ db }) => {
|
||||
// Drizzle kit config don't support encryption yet so we cannot use npx drizzle-kit migrate
|
||||
|
||||
@@ -3,7 +3,7 @@ import { triggerWebhook } from '@owlrelay/webhook';
|
||||
import { INTAKE_EMAILS_INGEST_ROUTE } from '../modules/intake-emails/intake-emails.constants';
|
||||
import { runScript } from './commons/run-script';
|
||||
|
||||
runScript(
|
||||
await runScript(
|
||||
{ scriptName: 'simulate-intake-email' },
|
||||
async ({ config }) => {
|
||||
const { baseUrl } = config.server;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
|
||||
2037
pnpm-lock.yaml
generated
2037
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -3,9 +3,9 @@ packages:
|
||||
- apps/*
|
||||
|
||||
catalog:
|
||||
'@antfu/eslint-config': ^4.12.0
|
||||
'@antfu/eslint-config': ^4.16.2
|
||||
'@types/node': ^22.15.21
|
||||
eslint: ^9.25.1
|
||||
eslint: ^9.30.1
|
||||
tsx: ^4.17.0
|
||||
typescript: ^5.6.2
|
||||
vitest: ^3.0.5
|
||||
|
||||
Reference in New Issue
Block a user