Skip to content

Commit 0ddb69b

Browse files
authored
Add article CRUD endpoints and some others (#129)
### Description - Article models and related migrations - Article endpoints - create - update - delete - get by slug - list - feed - Fix drop utility by dropping migrations table as well - Swagger id token auth. This closes #107 as well. That PR didn't work out of the box (it uses an invalid token format, Bearer instead of Token as per specified in the [docs](https://realworld-docs.netlify.app/specifications/backend/endpoints/)), but it gave the inspiration. ### PR Checklist (Please do not remove) - [x] Read the [CONTRIBUTING]( https://github.com/agnyz/bedstack/blob/main/CONTRIBUTING.md) guide - [x] Title this PR according to the `type(scope): description` or `type: description` format - [x] Provide description sufficient to understand the changes introduced in this PR, and, if necessary, some screenshots - [x] Reference an issue or discussion where the feature or changes have been previously discussed - [x] Add a failing test that passes with the changes introduced in this PR, or explain why it's not feasible - [x] Add documentation for the feature or changes introduced in this PR to the docs; you can run them with `bun docs` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced full article management capabilities, including endpoints for listing, creating, updating, deleting articles, and fetching personalized feeds. - Enhanced API security with token authentication and persistent authorization. - Expanded the database to support new content types and user interactions, such as published and favorited articles. - Improved profile response handling to better reflect user following status. - Added a utility function for generating URL-friendly slugs. - Added a new configuration for database migrations and expanded the database schema to include articles and favorite articles. - **Bug Fixes** - Adjusted error handling in profile response generation to accommodate cases with no current user ID. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 9c4b567 commit 0ddb69b

20 files changed

+1068
-11
lines changed

db/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,9 @@ export default defineConfig({
1919
dialect: 'postgresql',
2020
dbCredentials: dbCredentials,
2121
strict: true,
22+
// Redefine default migrations table and schema for the sake of clarity
23+
migrations: {
24+
table: '__drizzle_migrations',
25+
schema: 'drizzle',
26+
},
2227
});

db/drop.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { exit } from 'node:process';
22
import { db } from '@/database.providers';
3+
import { articles, favoriteArticles } from '@articles/articles.model';
4+
import dbConfig from '@db/config';
35
import { userFollows, users } from '@users/users.model';
46
import { getTableName, sql } from 'drizzle-orm';
57

6-
const tables = [users, userFollows];
8+
const tables = [userFollows, favoriteArticles, articles, users];
79
console.log('Dropping all tables from the database');
810

911
try {
@@ -17,6 +19,13 @@ try {
1719
);
1820
console.log(`Dropped ${name}`);
1921
}
22+
if (dbConfig.migrations?.table) {
23+
// Clean up migrations
24+
console.log('Dropping migrations table: ', dbConfig.migrations.table);
25+
await tx.execute(
26+
sql`DROP TABLE IF EXISTS ${sql.identifier(dbConfig.migrations.schema ?? 'public')}.${sql.identifier(dbConfig.migrations.table)} CASCADE;`,
27+
);
28+
}
2029
});
2130

2231
console.log('All tables dropped');
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
CREATE TABLE "articles" (
2+
"id" serial PRIMARY KEY NOT NULL,
3+
"slug" text NOT NULL,
4+
"title" text NOT NULL,
5+
"description" text NOT NULL,
6+
"body" text NOT NULL,
7+
"tag_list" text[] DEFAULT '{}' NOT NULL,
8+
"created_at" timestamp DEFAULT now() NOT NULL,
9+
"updated_at" timestamp DEFAULT now() NOT NULL,
10+
"author_id" integer NOT NULL,
11+
CONSTRAINT "articles_slug_unique" UNIQUE("slug")
12+
);
13+
14+
CREATE TABLE "favorite_articles" (
15+
"article_id" integer NOT NULL,
16+
"user_id" integer NOT NULL,
17+
"created_at" timestamp DEFAULT now() NOT NULL,
18+
"updated_at" timestamp DEFAULT now() NOT NULL,
19+
CONSTRAINT "favorite_articles_article_id_user_id_pk" PRIMARY KEY("article_id","user_id")
20+
);
21+
22+
ALTER TABLE "articles" ADD CONSTRAINT "articles_author_id_users_id_fk" FOREIGN KEY ("author_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
23+
ALTER TABLE "favorite_articles" ADD CONSTRAINT "favorite_articles_article_id_articles_id_fk" FOREIGN KEY ("article_id") REFERENCES "public"."articles"("id") ON DELETE cascade ON UPDATE no action;
24+
ALTER TABLE "favorite_articles" ADD CONSTRAINT "favorite_articles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
{
2+
"id": "842ed756-50d5-4cf1-b4dd-cbd31e340af4",
3+
"prevId": "c621d843-d221-4084-a3e3-361c92f1c7bf",
4+
"version": "7",
5+
"dialect": "postgresql",
6+
"tables": {
7+
"public.articles": {
8+
"name": "articles",
9+
"schema": "",
10+
"columns": {
11+
"id": {
12+
"name": "id",
13+
"type": "serial",
14+
"primaryKey": true,
15+
"notNull": true
16+
},
17+
"slug": {
18+
"name": "slug",
19+
"type": "text",
20+
"primaryKey": false,
21+
"notNull": true
22+
},
23+
"title": {
24+
"name": "title",
25+
"type": "text",
26+
"primaryKey": false,
27+
"notNull": true
28+
},
29+
"description": {
30+
"name": "description",
31+
"type": "text",
32+
"primaryKey": false,
33+
"notNull": true
34+
},
35+
"body": {
36+
"name": "body",
37+
"type": "text",
38+
"primaryKey": false,
39+
"notNull": true
40+
},
41+
"tag_list": {
42+
"name": "tag_list",
43+
"type": "text[]",
44+
"primaryKey": false,
45+
"notNull": true,
46+
"default": "'{}'"
47+
},
48+
"created_at": {
49+
"name": "created_at",
50+
"type": "timestamp",
51+
"primaryKey": false,
52+
"notNull": true,
53+
"default": "now()"
54+
},
55+
"updated_at": {
56+
"name": "updated_at",
57+
"type": "timestamp",
58+
"primaryKey": false,
59+
"notNull": true,
60+
"default": "now()"
61+
},
62+
"author_id": {
63+
"name": "author_id",
64+
"type": "integer",
65+
"primaryKey": false,
66+
"notNull": true
67+
}
68+
},
69+
"indexes": {},
70+
"foreignKeys": {
71+
"articles_author_id_users_id_fk": {
72+
"name": "articles_author_id_users_id_fk",
73+
"tableFrom": "articles",
74+
"tableTo": "users",
75+
"columnsFrom": ["author_id"],
76+
"columnsTo": ["id"],
77+
"onDelete": "cascade",
78+
"onUpdate": "no action"
79+
}
80+
},
81+
"compositePrimaryKeys": {},
82+
"uniqueConstraints": {
83+
"articles_slug_unique": {
84+
"name": "articles_slug_unique",
85+
"nullsNotDistinct": false,
86+
"columns": ["slug"]
87+
}
88+
},
89+
"policies": {},
90+
"checkConstraints": {},
91+
"isRLSEnabled": false
92+
},
93+
"public.favorite_articles": {
94+
"name": "favorite_articles",
95+
"schema": "",
96+
"columns": {
97+
"article_id": {
98+
"name": "article_id",
99+
"type": "integer",
100+
"primaryKey": false,
101+
"notNull": true
102+
},
103+
"user_id": {
104+
"name": "user_id",
105+
"type": "integer",
106+
"primaryKey": false,
107+
"notNull": true
108+
},
109+
"created_at": {
110+
"name": "created_at",
111+
"type": "timestamp",
112+
"primaryKey": false,
113+
"notNull": true,
114+
"default": "now()"
115+
},
116+
"updated_at": {
117+
"name": "updated_at",
118+
"type": "timestamp",
119+
"primaryKey": false,
120+
"notNull": true,
121+
"default": "now()"
122+
}
123+
},
124+
"indexes": {},
125+
"foreignKeys": {
126+
"favorite_articles_article_id_articles_id_fk": {
127+
"name": "favorite_articles_article_id_articles_id_fk",
128+
"tableFrom": "favorite_articles",
129+
"tableTo": "articles",
130+
"columnsFrom": ["article_id"],
131+
"columnsTo": ["id"],
132+
"onDelete": "cascade",
133+
"onUpdate": "no action"
134+
},
135+
"favorite_articles_user_id_users_id_fk": {
136+
"name": "favorite_articles_user_id_users_id_fk",
137+
"tableFrom": "favorite_articles",
138+
"tableTo": "users",
139+
"columnsFrom": ["user_id"],
140+
"columnsTo": ["id"],
141+
"onDelete": "cascade",
142+
"onUpdate": "no action"
143+
}
144+
},
145+
"compositePrimaryKeys": {
146+
"favorite_articles_article_id_user_id_pk": {
147+
"name": "favorite_articles_article_id_user_id_pk",
148+
"columns": ["article_id", "user_id"]
149+
}
150+
},
151+
"uniqueConstraints": {},
152+
"policies": {},
153+
"checkConstraints": {},
154+
"isRLSEnabled": false
155+
},
156+
"public.user_follows": {
157+
"name": "user_follows",
158+
"schema": "",
159+
"columns": {
160+
"followed_id": {
161+
"name": "followed_id",
162+
"type": "integer",
163+
"primaryKey": false,
164+
"notNull": true
165+
},
166+
"follower_id": {
167+
"name": "follower_id",
168+
"type": "integer",
169+
"primaryKey": false,
170+
"notNull": true
171+
},
172+
"created_at": {
173+
"name": "created_at",
174+
"type": "date",
175+
"primaryKey": false,
176+
"notNull": true,
177+
"default": "CURRENT_DATE"
178+
},
179+
"updated_at": {
180+
"name": "updated_at",
181+
"type": "date",
182+
"primaryKey": false,
183+
"notNull": true,
184+
"default": "CURRENT_DATE"
185+
}
186+
},
187+
"indexes": {},
188+
"foreignKeys": {
189+
"user_follows_followed_id_users_id_fk": {
190+
"name": "user_follows_followed_id_users_id_fk",
191+
"tableFrom": "user_follows",
192+
"tableTo": "users",
193+
"columnsFrom": ["followed_id"],
194+
"columnsTo": ["id"],
195+
"onDelete": "cascade",
196+
"onUpdate": "no action"
197+
},
198+
"user_follows_follower_id_users_id_fk": {
199+
"name": "user_follows_follower_id_users_id_fk",
200+
"tableFrom": "user_follows",
201+
"tableTo": "users",
202+
"columnsFrom": ["follower_id"],
203+
"columnsTo": ["id"],
204+
"onDelete": "cascade",
205+
"onUpdate": "no action"
206+
}
207+
},
208+
"compositePrimaryKeys": {
209+
"user_follows_followed_id_follower_id_pk": {
210+
"name": "user_follows_followed_id_follower_id_pk",
211+
"columns": ["followed_id", "follower_id"]
212+
}
213+
},
214+
"uniqueConstraints": {},
215+
"policies": {},
216+
"checkConstraints": {},
217+
"isRLSEnabled": false
218+
},
219+
"public.users": {
220+
"name": "users",
221+
"schema": "",
222+
"columns": {
223+
"id": {
224+
"name": "id",
225+
"type": "serial",
226+
"primaryKey": true,
227+
"notNull": true
228+
},
229+
"email": {
230+
"name": "email",
231+
"type": "text",
232+
"primaryKey": false,
233+
"notNull": true
234+
},
235+
"bio": {
236+
"name": "bio",
237+
"type": "text",
238+
"primaryKey": false,
239+
"notNull": true,
240+
"default": "''"
241+
},
242+
"image": {
243+
"name": "image",
244+
"type": "text",
245+
"primaryKey": false,
246+
"notNull": true,
247+
"default": "'https://api.realworld.io/images/smiley-cyrus.jpg'"
248+
},
249+
"password": {
250+
"name": "password",
251+
"type": "text",
252+
"primaryKey": false,
253+
"notNull": true
254+
},
255+
"username": {
256+
"name": "username",
257+
"type": "text",
258+
"primaryKey": false,
259+
"notNull": true
260+
},
261+
"created_at": {
262+
"name": "created_at",
263+
"type": "date",
264+
"primaryKey": false,
265+
"notNull": true,
266+
"default": "CURRENT_DATE"
267+
},
268+
"updated_at": {
269+
"name": "updated_at",
270+
"type": "date",
271+
"primaryKey": false,
272+
"notNull": true,
273+
"default": "CURRENT_DATE"
274+
}
275+
},
276+
"indexes": {},
277+
"foreignKeys": {},
278+
"compositePrimaryKeys": {},
279+
"uniqueConstraints": {
280+
"users_email_unique": {
281+
"name": "users_email_unique",
282+
"nullsNotDistinct": false,
283+
"columns": ["email"]
284+
},
285+
"users_username_unique": {
286+
"name": "users_username_unique",
287+
"nullsNotDistinct": false,
288+
"columns": ["username"]
289+
}
290+
},
291+
"policies": {},
292+
"checkConstraints": {},
293+
"isRLSEnabled": false
294+
}
295+
},
296+
"enums": {},
297+
"schemas": {},
298+
"sequences": {},
299+
"roles": {},
300+
"policies": {},
301+
"views": {},
302+
"_meta": {
303+
"columns": {},
304+
"schemas": {},
305+
"tables": {}
306+
}
307+
}

db/migrations/meta/_journal.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@
5050
"when": 1742050755293,
5151
"tag": "0006_goofy_diamondback",
5252
"breakpoints": false
53+
},
54+
{
55+
"idx": 7,
56+
"version": "7",
57+
"when": 1742359518221,
58+
"tag": "0007_thin_eternals",
59+
"breakpoints": false
5360
}
5461
]
5562
}

0 commit comments

Comments
 (0)