Skip to content

Commit c8e74b2

Browse files
added slugs to projects
Signed-off-by: Benjamin Strasser <bp.strasser@gmail.com>
1 parent 9b1942d commit c8e74b2

File tree

20 files changed

+193
-62
lines changed

20 files changed

+193
-62
lines changed

pnpm-lock.yaml

Lines changed: 41 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
packages:
2-
- "services/*"
2+
- "services"

services/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"kysely": "^0.27.3",
88
"pino": "^9.0.0",
99
"pino-pretty": "^11.0.0",
10-
"zod": "^3.23.5"
10+
"zod": "^3.23.5",
11+
"slugify": "^1.6.6"
1112
},
1213
"devDependencies": {
1314
"vite": "^5.2.11",

services/src/kysely/migrations/2024-04-28T09_init.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ export async function up(db: Kysely<unknown>): Promise<void> {
1313

1414
await createTableMigration(tx, 'projects')
1515
.addColumn('name', 'text', (col) => col.unique().notNull())
16-
.addColumn('base_language', 'integer', (col) =>
16+
.addColumn('base_language_id', 'integer', (col) =>
1717
col
1818
.references('languages.id')
1919
.onDelete('restrict')
2020
.notNull()
2121
.modifyEnd(sql`DEFERRABLE INITIALLY DEFERRED`)
2222
)
23+
.addColumn('slug', 'text', (col) => col.unique().notNull())
2324
.execute()
2425

2526
await createTableMigration(tx, 'languages')

services/src/project/project-repository.integration.test.ts

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import { beforeEach, describe, expect, it } from 'vitest'
22
import { createProject, getAllProjects } from './project-repository'
33
import { runMigration } from '../db/database-migration-util'
44
import { db } from '../db/database'
5-
import type { CreateProjectFormSchema, SelectableProject } from './project'
5+
import type { ProjectCreationParams, SelectableProject } from './project'
66
import type { Languages } from 'kysely-codegen'
77
import type { Selectable } from 'kysely'
88

9-
const projectCreationObject: CreateProjectFormSchema = {
9+
const projectCreationObject: ProjectCreationParams = {
1010
name: 'Test Project',
11-
base_language: 'en'
11+
base_language_code: 'en',
12+
slug: 'test-project'
1213
}
1314

1415
beforeEach(async () => {
@@ -29,7 +30,7 @@ describe('Project Repository', () => {
2930
expect(project).toMatchObject({
3031
id: createdProject.id,
3132
name: projectCreationObject.name,
32-
base_language: createdProject.base_language
33+
base_language_id: createdProject.base_language_id
3334
})
3435

3536
expect(project.id).toBeTypeOf('number')
@@ -44,6 +45,26 @@ describe('Project Repository', () => {
4445
expect(projects).toHaveLength(1)
4546
})
4647

48+
it('should not allow creation of projects with duplicate slugs', async () => {
49+
const projectCreationObject1 = {
50+
name: 'Test Project',
51+
base_language_code: 'en',
52+
slug: 'test-project'
53+
}
54+
const projectCreationObject2 = {
55+
name: 'test-project',
56+
base_language_code: 'en',
57+
slug: 'test-project'
58+
}
59+
60+
await createProject(projectCreationObject1)
61+
62+
await expect(createProject(projectCreationObject2)).rejects.toThrow()
63+
64+
const projects = await db.selectFrom('projects').selectAll().execute()
65+
expect(projects).toHaveLength(1)
66+
})
67+
4768
it('should create a base language for the project', async () => {
4869
const createdProject = await createProject(projectCreationObject)
4970

@@ -53,26 +74,26 @@ describe('Project Repository', () => {
5374
const language = languages[0] as Selectable<Languages>
5475

5576
expect(language.project_id).toBe(createdProject.id)
56-
expect(language.code).toBe(projectCreationObject.base_language)
77+
expect(language.code).toBe(projectCreationObject.base_language_code)
5778
})
5879

5980
it('should link the base language to the project', async () => {
6081
const createdProject = await createProject(projectCreationObject)
6182

62-
expect(createdProject.base_language).not.toBe(0)
83+
expect(createdProject.base_language_id).not.toBe(0)
6384

6485
const language = await db
6586
.selectFrom('languages')
66-
.where('id', '==', createdProject.base_language)
87+
.where('id', '==', createdProject.base_language_id)
6788
.selectAll()
6889
.executeTakeFirstOrThrow()
6990

7091
expect(language.project_id).toBe(createdProject.id)
7192
})
7293

7394
it('should allow creation of multiple projects with the same base language code', async () => {
74-
const project1 = { name: 'Project 1', base_language: 'en' }
75-
const project2 = { name: 'Project 2', base_language: 'en' }
95+
const project1 = { name: 'Project 1', base_language_code: 'en', slug: 'project-1' }
96+
const project2 = { name: 'Project 2', base_language_code: 'en', slug: 'project-2' }
7697

7798
await createProject(project1)
7899
await createProject(project2)
@@ -95,8 +116,8 @@ describe('Project Repository', () => {
95116
})
96117

97118
it('should return all created projects', async () => {
98-
const project1 = { name: 'Project 1', base_language: 'en' }
99-
const project2 = { name: 'Project 2', base_language: 'fr' }
119+
const project1 = { name: 'Project 1', base_language_code: 'en', slug: 'project-1' }
120+
const project2 = { name: 'Project 2', base_language_code: 'fr', slug: 'project-2' }
100121

101122
await createProject(project1)
102123
await createProject(project2)
@@ -120,7 +141,7 @@ describe('Project Repository', () => {
120141
expect(project).toMatchObject({
121142
id: createdProject.id,
122143
name: projectCreationObject.name,
123-
base_language: createdProject.base_language
144+
base_language_id: createdProject.base_language_id
124145
})
125146

126147
expect(project.id).toBeTypeOf('number')

services/src/project/project-repository.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
1-
import type { CreateProjectFormSchema, SelectableProject } from './project'
1+
import type { ProjectCreationParams, SelectableProject } from './project'
22
import { db } from '../db/database'
33

4-
export function createProject(project: CreateProjectFormSchema): Promise<SelectableProject> {
4+
export function createProject(project: ProjectCreationParams): Promise<SelectableProject> {
55
return db.transaction().execute(async (tx) => {
66
const tempProject = await tx
77
.insertInto('projects')
8-
.values({ name: project.name, base_language: 0 })
8+
.values({ name: project.name, base_language_id: 0, slug: project.slug })
99
.returning('id')
1010
.executeTakeFirstOrThrow(() => new Error('Error Creating Project'))
1111

1212
const baseLanguage = await tx
1313
.insertInto('languages')
14-
.values({ code: project.base_language, project_id: tempProject.id })
14+
.values({ code: project.base_language_code, project_id: tempProject.id })
1515
.returning('id')
1616
.executeTakeFirstOrThrow(() => new Error('Error Creating Base Language'))
1717

1818
const createdProject = await tx
1919
.updateTable('projects')
20-
.set({ base_language: baseLanguage.id })
20+
.set({ base_language_id: baseLanguage.id })
2121
.where('id', '==', tempProject.id)
2222
.returningAll()
2323
.executeTakeFirstOrThrow(() => new Error('Error Updating Project'))

services/src/project/project-service.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import type { CreateProjectFormSchema } from '$components/container/projects/create-project-schema'
2+
import { createSlug } from '../util/slug/slug.service'
13
import { CreateProjectNameNotUniqueError } from '../error'
2-
import { type CreateProjectFormSchema } from './project'
34
import * as repository from './project-repository'
45
import { SqliteError } from 'better-sqlite3'
56

67
export async function createProject(project: CreateProjectFormSchema) {
78
try {
8-
return await repository.createProject(project)
9+
const slug = createSlug(project.name)
10+
11+
return await repository.createProject({ ...project, slug })
912
} catch (e: unknown) {
1013
if (e instanceof SqliteError && e.code === 'SQLITE_CONSTRAINT_UNIQUE') {
1114
throw new CreateProjectNameNotUniqueError()

services/src/project/project-service.unit.test.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,37 @@
11
import { beforeEach, describe, expect, it, vi } from 'vitest'
22
import { createProject, getAllProjects } from './project-service'
33
import * as repository from './project-repository'
4-
import type { CreateProjectFormSchema } from './project'
54
import { CreateProjectNameNotUniqueError } from '../error'
65
import { SqliteError } from 'better-sqlite3'
6+
import type { CreateProjectFormSchema } from '$components/container/projects/create-project-schema'
7+
import { createSlug } from '../util/slug/slug.service'
78

89
vi.mock('./project-repository', () => ({
910
createProject: vi.fn(),
1011
getAllProjects: vi.fn()
1112
}))
1213

14+
vi.mock('../util/slug/slug.service', () => ({
15+
createSlug: vi.fn()
16+
}))
17+
1318
const projectCreationObject: CreateProjectFormSchema = {
1419
name: 'Test Project',
15-
base_language: 'en'
20+
base_language_code: 'en'
1621
}
1722

1823
const mockSelectableProject = {
1924
id: 1,
2025
name: 'Test Project',
21-
base_language: 1,
26+
slug: 'test-project',
27+
base_language_id: 1,
2228
created_at: new Date().toISOString(),
2329
updated_at: new Date().toISOString()
2430
}
2531

2632
beforeEach(() => {
2733
vi.resetAllMocks()
34+
vi.mocked(createSlug).mockReturnValue('test-project')
2835
})
2936

3037
describe('Project Service', () => {
@@ -34,7 +41,11 @@ describe('Project Service', () => {
3441

3542
const project = await createProject(projectCreationObject)
3643

37-
expect(repository.createProject).toHaveBeenCalledWith(projectCreationObject)
44+
expect(repository.createProject).toHaveBeenCalledWith({
45+
...projectCreationObject,
46+
slug: 'test-project'
47+
})
48+
3849
expect(project).toEqual(mockSelectableProject)
3950
})
4051

@@ -55,6 +66,22 @@ describe('Project Service', () => {
5566
new CreateProjectNameNotUniqueError()
5667
)
5768
})
69+
70+
it('should call the slug service to create a slug and use it to call repository', async () => {
71+
const mockedSlug = 'ABCD'
72+
vi.mocked(createSlug).mockReturnValue(mockedSlug)
73+
vi.mocked(repository.createProject).mockResolvedValue(mockSelectableProject)
74+
75+
const project = await createProject(projectCreationObject)
76+
77+
expect(createSlug).toHaveBeenCalledWith(projectCreationObject.name)
78+
expect(repository.createProject).toHaveBeenCalledWith({
79+
...projectCreationObject,
80+
slug: mockedSlug
81+
})
82+
83+
expect(project).toEqual(mockSelectableProject)
84+
})
5885
})
5986

6087
describe('getAllProjects', () => {

services/src/project/project.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,12 @@
11
import type { Insertable, Selectable } from 'kysely'
22
import type { Projects } from 'kysely-codegen'
3-
import { z } from 'zod'
43

5-
export type ProjectCreationParams = Insertable<Omit<Projects, 'id' | 'created_at' | 'updated_at'>>
4+
export type ProjectCreationParams = {
5+
name: string
6+
slug: string
7+
base_language_code: string
8+
}
69
export type Project = SelectableProject
710

811
export type SelectableProject = Selectable<Projects>
912
export type InsertableProject = Insertable<Projects>
10-
11-
export const createProjectSchema = z.object({
12-
name: z
13-
.string({ required_error: 'Project name is required' })
14-
.min(1, 'Project name must have at least one character'),
15-
base_language: z
16-
.string({ required_error: 'Base language is required' })
17-
.min(1, 'Base language must have at least one character')
18-
})
19-
20-
export type CreateProjectFormSchema = z.infer<typeof createProjectSchema>

0 commit comments

Comments
 (0)