@porulle/plugin-wishlist 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # @porulle/plugin-wishlist
2
+
3
+ Per-customer saved items for later purchase, with admin listing across users.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @porulle/plugin-wishlist
9
+ ```
10
+
11
+ Add to `commerce.config.ts`:
12
+
13
+ ```typescript
14
+ import { wishlistPlugin } from "@porulle/plugin-wishlist";
15
+
16
+ export default defineConfig({
17
+ plugins: [wishlistPlugin()],
18
+ });
19
+ ```
20
+
21
+ Add to `drizzle.config.ts`:
22
+
23
+ ```typescript
24
+ schema: [
25
+ "./node_modules/@porulle/plugin-wishlist/src/schema.ts",
26
+ // ...
27
+ ],
28
+ ```
29
+
30
+ ## What it does
31
+
32
+ Stores wishlist rows keyed to customers and catalog entities; authenticated shoppers manage their list while admins can query aggregate views.
33
+
34
+ ## Routes exposed
35
+
36
+ **`/wishlist`** — `GET /`, `POST /`, `DELETE /{id}` (authenticated customer); `GET /admin` (`wishlist:admin`)
37
+
38
+ ## Hooks
39
+
40
+ **Emitted:** none.
41
+
42
+ **Consumed:** none.
43
+
44
+ ## MCP tools
45
+
46
+ None.
47
+
48
+ ## Configuration options
49
+
50
+ None (`wishlistPlugin()` takes no options).
51
+
52
+ ## License
53
+
54
+ MIT
@@ -0,0 +1,4 @@
1
+ export type { Db } from "./types.js";
2
+ export { WishlistService } from "./services/wishlist-service.js";
3
+ export declare function wishlistPlugin(): import("@porulle/core").CommercePlugin;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,YAAY,EAAE,EAAE,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAEjE,wBAAgB,cAAc,2CAiB7B"}
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ import { defineCommercePlugin } from "@porulle/core";
2
+ import { wishlistItems } from "./schema.js";
3
+ import { WishlistService } from "./services/wishlist-service.js";
4
+ import { buildWishlistRoutes } from "./routes/wishlist.js";
5
+ export { WishlistService } from "./services/wishlist-service.js";
6
+ export function wishlistPlugin() {
7
+ return defineCommercePlugin({
8
+ id: "wishlist",
9
+ version: "1.0.0",
10
+ permissions: [
11
+ { scope: "wishlist:read", description: "View wishlist items." },
12
+ { scope: "wishlist:write", description: "Add/remove own wishlist items." },
13
+ { scope: "wishlist:admin", description: "Manage any user's wishlist." },
14
+ ],
15
+ schema: () => ({ wishlistItems }),
16
+ hooks: () => [],
17
+ routes: (ctx) => {
18
+ const db = ctx.database.db;
19
+ if (!db)
20
+ return [];
21
+ return buildWishlistRoutes(new WishlistService(db), ctx);
22
+ },
23
+ });
24
+ }
@@ -0,0 +1,9 @@
1
+ import type { WishlistService } from "../services/wishlist-service.js";
2
+ import type { PluginRouteRegistration } from "@porulle/core";
3
+ export declare function buildWishlistRoutes(service: WishlistService, ctx: {
4
+ services?: Record<string, unknown>;
5
+ database?: {
6
+ db: unknown;
7
+ };
8
+ }): PluginRouteRegistration[];
9
+ //# sourceMappingURL=wishlist.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wishlist.d.ts","sourceRoot":"","sources":["../../src/routes/wishlist.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAE7D,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,eAAe,EACxB,GAAG,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,QAAQ,CAAC,EAAE;QAAE,EAAE,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,GACtE,uBAAuB,EAAE,CAqC3B"}
@@ -0,0 +1,38 @@
1
+ import { router } from "@porulle/core";
2
+ import { z } from "@hono/zod-openapi";
3
+ export function buildWishlistRoutes(service, ctx) {
4
+ const r = router("Wishlist", "/wishlist", ctx);
5
+ r.get("/").summary("List my wishlist").auth()
6
+ .handler(async ({ actor, orgId }) => {
7
+ const result = await service.list(orgId, actor.userId);
8
+ if (!result.ok)
9
+ throw new Error(result.error);
10
+ return result.value;
11
+ });
12
+ r.post("/").summary("Add to wishlist").auth()
13
+ .input(z.object({ entityId: z.string().uuid(), note: z.string().max(500).optional() }))
14
+ .handler(async ({ input, actor, orgId }) => {
15
+ const body = input;
16
+ const result = await service.add(orgId, actor.userId, body);
17
+ if (!result.ok)
18
+ throw new Error(result.error);
19
+ return result.value;
20
+ });
21
+ r.delete("/{id}").summary("Remove from wishlist").auth()
22
+ .handler(async ({ params, actor, orgId }) => {
23
+ const result = await service.remove(orgId, actor.userId, params.id);
24
+ if (!result.ok)
25
+ throw new Error(result.error);
26
+ return result.value;
27
+ });
28
+ r.get("/admin").summary("List all wishlists (admin)").permission("wishlist:admin")
29
+ .handler(async ({ orgId, db }) => {
30
+ // Admin can see all wishlists in their org — use db directly
31
+ const { wishlistItems } = await import("../schema.js");
32
+ const { eq } = await import("@porulle/core/drizzle");
33
+ const rows = await db.select().from(wishlistItems)
34
+ .where(eq(wishlistItems.organizationId, orgId));
35
+ return rows;
36
+ });
37
+ return r.routes();
38
+ }
@@ -0,0 +1,110 @@
1
+ export declare const wishlistItems: import("drizzle-orm/pg-core/table").PgTableWithColumns<{
2
+ name: "wishlist_items";
3
+ schema: undefined;
4
+ columns: {
5
+ id: import("@porulle/core/drizzle").PgColumn<{
6
+ name: "id";
7
+ tableName: "wishlist_items";
8
+ dataType: "string";
9
+ columnType: "PgUUID";
10
+ data: string;
11
+ driverParam: string;
12
+ notNull: true;
13
+ hasDefault: true;
14
+ isPrimaryKey: true;
15
+ isAutoincrement: false;
16
+ hasRuntimeDefault: false;
17
+ enumValues: undefined;
18
+ baseColumn: never;
19
+ identity: undefined;
20
+ generated: undefined;
21
+ }, {}, {}>;
22
+ organizationId: import("@porulle/core/drizzle").PgColumn<{
23
+ name: "organization_id";
24
+ tableName: "wishlist_items";
25
+ dataType: "string";
26
+ columnType: "PgText";
27
+ data: string;
28
+ driverParam: string;
29
+ notNull: true;
30
+ hasDefault: false;
31
+ isPrimaryKey: false;
32
+ isAutoincrement: false;
33
+ hasRuntimeDefault: false;
34
+ enumValues: [string, ...string[]];
35
+ baseColumn: never;
36
+ identity: undefined;
37
+ generated: undefined;
38
+ }, {}, {}>;
39
+ userId: import("@porulle/core/drizzle").PgColumn<{
40
+ name: "user_id";
41
+ tableName: "wishlist_items";
42
+ dataType: "string";
43
+ columnType: "PgText";
44
+ data: string;
45
+ driverParam: string;
46
+ notNull: true;
47
+ hasDefault: false;
48
+ isPrimaryKey: false;
49
+ isAutoincrement: false;
50
+ hasRuntimeDefault: false;
51
+ enumValues: [string, ...string[]];
52
+ baseColumn: never;
53
+ identity: undefined;
54
+ generated: undefined;
55
+ }, {}, {}>;
56
+ entityId: import("@porulle/core/drizzle").PgColumn<{
57
+ name: "entity_id";
58
+ tableName: "wishlist_items";
59
+ dataType: "string";
60
+ columnType: "PgUUID";
61
+ data: string;
62
+ driverParam: string;
63
+ notNull: true;
64
+ hasDefault: false;
65
+ isPrimaryKey: false;
66
+ isAutoincrement: false;
67
+ hasRuntimeDefault: false;
68
+ enumValues: undefined;
69
+ baseColumn: never;
70
+ identity: undefined;
71
+ generated: undefined;
72
+ }, {}, {}>;
73
+ note: import("@porulle/core/drizzle").PgColumn<{
74
+ name: "note";
75
+ tableName: "wishlist_items";
76
+ dataType: "string";
77
+ columnType: "PgText";
78
+ data: string;
79
+ driverParam: string;
80
+ notNull: false;
81
+ hasDefault: false;
82
+ isPrimaryKey: false;
83
+ isAutoincrement: false;
84
+ hasRuntimeDefault: false;
85
+ enumValues: [string, ...string[]];
86
+ baseColumn: never;
87
+ identity: undefined;
88
+ generated: undefined;
89
+ }, {}, {}>;
90
+ addedAt: import("@porulle/core/drizzle").PgColumn<{
91
+ name: "added_at";
92
+ tableName: "wishlist_items";
93
+ dataType: "date";
94
+ columnType: "PgTimestamp";
95
+ data: Date;
96
+ driverParam: string;
97
+ notNull: true;
98
+ hasDefault: true;
99
+ isPrimaryKey: false;
100
+ isAutoincrement: false;
101
+ hasRuntimeDefault: false;
102
+ enumValues: undefined;
103
+ baseColumn: never;
104
+ identity: undefined;
105
+ generated: undefined;
106
+ }, {}, {}>;
107
+ };
108
+ dialect: "pg";
109
+ }>;
110
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgBvB,CAAC"}
package/dist/schema.js ADDED
@@ -0,0 +1,20 @@
1
+ import { pgTable, uuid, text, timestamp, index, uniqueIndex, } from "@porulle/core/drizzle";
2
+ import { organization } from "@porulle/core/auth-schema";
3
+ import { sellableEntities } from "@porulle/core/schema";
4
+ export const wishlistItems = pgTable("wishlist_items", {
5
+ id: uuid("id").defaultRandom().primaryKey(),
6
+ organizationId: text("organization_id")
7
+ .notNull()
8
+ .references(() => organization.id, { onDelete: "cascade" }),
9
+ userId: text("user_id").notNull(),
10
+ entityId: uuid("entity_id")
11
+ .notNull()
12
+ .references(() => sellableEntities.id, { onDelete: "cascade" }),
13
+ note: text("note"),
14
+ addedAt: timestamp("added_at", { withTimezone: true }).defaultNow().notNull(),
15
+ }, (table) => ({
16
+ orgIdx: index("idx_wishlist_items_org").on(table.organizationId),
17
+ userIdx: index("idx_wishlist_items_user").on(table.organizationId, table.userId),
18
+ orgUserEntityUnique: uniqueIndex("wishlist_items_org_user_entity_unique")
19
+ .on(table.organizationId, table.userId, table.entityId),
20
+ }));
@@ -0,0 +1,18 @@
1
+ import type { PluginResult } from "@porulle/core";
2
+ import type { Db, WishlistItem } from "../types.js";
3
+ export declare class WishlistService {
4
+ private db;
5
+ constructor(db: Db);
6
+ list(orgId: string, userId: string): Promise<PluginResult<WishlistItem[]>>;
7
+ add(orgId: string, userId: string, input: {
8
+ entityId: string;
9
+ note?: string;
10
+ }): Promise<PluginResult<WishlistItem>>;
11
+ remove(orgId: string, userId: string, itemId: string): Promise<PluginResult<{
12
+ deleted: boolean;
13
+ }>>;
14
+ removeByEntity(orgId: string, userId: string, entityId: string): Promise<PluginResult<{
15
+ deleted: boolean;
16
+ }>>;
17
+ }
18
+ //# sourceMappingURL=wishlist-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wishlist-service.d.ts","sourceRoot":"","sources":["../../src/services/wishlist-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,KAAK,EAAE,EAAE,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEpD,qBAAa,eAAe;IACd,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAEpB,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,YAAY,EAAE,CAAC,CAAC;IAM1E,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE;QAC9C,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;KACjC,GAAG,OAAO,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;IAgBjC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAWlG,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAUnH"}
@@ -0,0 +1,39 @@
1
+ import { eq, and } from "@porulle/core/drizzle";
2
+ import { Ok, Err } from "@porulle/core";
3
+ import { wishlistItems } from "../schema.js";
4
+ export class WishlistService {
5
+ db;
6
+ constructor(db) {
7
+ this.db = db;
8
+ }
9
+ async list(orgId, userId) {
10
+ const rows = await this.db.select().from(wishlistItems)
11
+ .where(and(eq(wishlistItems.organizationId, orgId), eq(wishlistItems.userId, userId)));
12
+ return Ok(rows);
13
+ }
14
+ async add(orgId, userId, input) {
15
+ // Check duplicate
16
+ const existing = await this.db.select().from(wishlistItems)
17
+ .where(and(eq(wishlistItems.organizationId, orgId), eq(wishlistItems.userId, userId), eq(wishlistItems.entityId, input.entityId)));
18
+ if (existing.length > 0)
19
+ return Err("Item already in wishlist");
20
+ const rows = await this.db.insert(wishlistItems).values({
21
+ organizationId: orgId, userId, entityId: input.entityId, note: input.note,
22
+ }).returning();
23
+ return Ok(rows[0]);
24
+ }
25
+ async remove(orgId, userId, itemId) {
26
+ const rows = await this.db.delete(wishlistItems)
27
+ .where(and(eq(wishlistItems.id, itemId), eq(wishlistItems.organizationId, orgId), eq(wishlistItems.userId, userId))).returning();
28
+ if (rows.length === 0)
29
+ return Err("Item not found");
30
+ return Ok({ deleted: true });
31
+ }
32
+ async removeByEntity(orgId, userId, entityId) {
33
+ const rows = await this.db.delete(wishlistItems)
34
+ .where(and(eq(wishlistItems.organizationId, orgId), eq(wishlistItems.userId, userId), eq(wishlistItems.entityId, entityId))).returning();
35
+ if (rows.length === 0)
36
+ return Err("Item not found");
37
+ return Ok({ deleted: true });
38
+ }
39
+ }
@@ -0,0 +1,4 @@
1
+ import type { wishlistItems } from "./schema.js";
2
+ export type { PluginDb as Db } from "@porulle/core";
3
+ export type WishlistItem = typeof wishlistItems.$inferSelect;
4
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,YAAY,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,eAAe,CAAC;AACpD,MAAM,MAAM,YAAY,GAAG,OAAO,aAAa,CAAC,YAAY,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@porulle/plugin-wishlist",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "bun": "./src/index.ts",
9
+ "import": "./dist/index.js",
10
+ "types": "./src/index.ts"
11
+ },
12
+ "./schema": {
13
+ "bun": "./src/schema.ts",
14
+ "import": "./dist/schema.js",
15
+ "require": "./dist/schema.js",
16
+ "types": "./src/schema.ts"
17
+ }
18
+ },
19
+ "scripts": {
20
+ "build": "rm -rf dist tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json",
21
+ "check-types": "tsc --noEmit",
22
+ "test": "vitest run"
23
+ },
24
+ "dependencies": {
25
+ "@hono/zod-openapi": "^1.2.2",
26
+ "@porulle/core": "workspace:*",
27
+ "hono": "^4.12.5"
28
+ },
29
+ "devDependencies": {
30
+ "@repo/eslint-config": "*",
31
+ "@repo/typescript-config": "*",
32
+ "@types/node": "^24.5.2",
33
+ "typescript": "5.9.2",
34
+ "vitest": "^3.2.4"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "files": [
40
+ "src",
41
+ "dist",
42
+ "README.md"
43
+ ],
44
+ "description": "Per-customer saved items for later purchase, with admin listing across users.",
45
+ "homepage": "https://porulle-docs.vercel.app",
46
+ "bugs": {
47
+ "url": "https://github.com/asyncdotengineering/porulle/issues"
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/asyncdotengineering/porulle.git",
52
+ "directory": "packages/plugins/plugin-wishlist"
53
+ },
54
+ "author": "Porulle contributors"
55
+ }
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { defineCommercePlugin } from "@porulle/core";
2
+ import { wishlistItems } from "./schema.js";
3
+ import { WishlistService } from "./services/wishlist-service.js";
4
+ import { buildWishlistRoutes } from "./routes/wishlist.js";
5
+ export type { Db } from "./types.js";
6
+ export { WishlistService } from "./services/wishlist-service.js";
7
+
8
+ export function wishlistPlugin() {
9
+ return defineCommercePlugin({
10
+ id: "wishlist",
11
+ version: "1.0.0",
12
+ permissions: [
13
+ { scope: "wishlist:read", description: "View wishlist items." },
14
+ { scope: "wishlist:write", description: "Add/remove own wishlist items." },
15
+ { scope: "wishlist:admin", description: "Manage any user's wishlist." },
16
+ ],
17
+ schema: () => ({ wishlistItems }),
18
+ hooks: () => [],
19
+ routes: (ctx) => {
20
+ const db = ctx.database.db;
21
+ if (!db) return [];
22
+ return buildWishlistRoutes(new WishlistService(db), ctx);
23
+ },
24
+ });
25
+ }
@@ -0,0 +1,46 @@
1
+ import { router } from "@porulle/core";
2
+ import { z } from "@hono/zod-openapi";
3
+ import type { WishlistService } from "../services/wishlist-service.js";
4
+ import type { PluginRouteRegistration } from "@porulle/core";
5
+
6
+ export function buildWishlistRoutes(
7
+ service: WishlistService,
8
+ ctx: { services?: Record<string, unknown>; database?: { db: unknown } },
9
+ ): PluginRouteRegistration[] {
10
+ const r = router("Wishlist", "/wishlist", ctx);
11
+
12
+ r.get("/").summary("List my wishlist").auth()
13
+ .handler(async ({ actor, orgId }) => {
14
+ const result = await service.list(orgId, actor!.userId);
15
+ if (!result.ok) throw new Error(result.error);
16
+ return result.value;
17
+ });
18
+
19
+ r.post("/").summary("Add to wishlist").auth()
20
+ .input(z.object({ entityId: z.string().uuid(), note: z.string().max(500).optional() }))
21
+ .handler(async ({ input, actor, orgId }) => {
22
+ const body = input as { entityId: string; note?: string };
23
+ const result = await service.add(orgId, actor!.userId, body);
24
+ if (!result.ok) throw new Error(result.error);
25
+ return result.value;
26
+ });
27
+
28
+ r.delete("/{id}").summary("Remove from wishlist").auth()
29
+ .handler(async ({ params, actor, orgId }) => {
30
+ const result = await service.remove(orgId, actor!.userId, params.id!);
31
+ if (!result.ok) throw new Error(result.error);
32
+ return result.value;
33
+ });
34
+
35
+ r.get("/admin").summary("List all wishlists (admin)").permission("wishlist:admin")
36
+ .handler(async ({ orgId, db }) => {
37
+ // Admin can see all wishlists in their org — use db directly
38
+ const { wishlistItems } = await import("../schema.js");
39
+ const { eq } = await import("@porulle/core/drizzle");
40
+ const rows = await (db as import("../types.js").Db).select().from(wishlistItems)
41
+ .where(eq(wishlistItems.organizationId, orgId));
42
+ return rows;
43
+ });
44
+
45
+ return r.routes();
46
+ }
package/src/schema.ts ADDED
@@ -0,0 +1,28 @@
1
+ import {
2
+ pgTable,
3
+ uuid,
4
+ text,
5
+ timestamp,
6
+ index,
7
+ uniqueIndex,
8
+ } from "@porulle/core/drizzle";
9
+ import { organization } from "@porulle/core/auth-schema";
10
+ import { sellableEntities } from "@porulle/core/schema";
11
+
12
+ export const wishlistItems = pgTable("wishlist_items", {
13
+ id: uuid("id").defaultRandom().primaryKey(),
14
+ organizationId: text("organization_id")
15
+ .notNull()
16
+ .references(() => organization.id, { onDelete: "cascade" }),
17
+ userId: text("user_id").notNull(),
18
+ entityId: uuid("entity_id")
19
+ .notNull()
20
+ .references(() => sellableEntities.id, { onDelete: "cascade" }),
21
+ note: text("note"),
22
+ addedAt: timestamp("added_at", { withTimezone: true }).defaultNow().notNull(),
23
+ }, (table) => ({
24
+ orgIdx: index("idx_wishlist_items_org").on(table.organizationId),
25
+ userIdx: index("idx_wishlist_items_user").on(table.organizationId, table.userId),
26
+ orgUserEntityUnique: uniqueIndex("wishlist_items_org_user_entity_unique")
27
+ .on(table.organizationId, table.userId, table.entityId),
28
+ }));
@@ -0,0 +1,55 @@
1
+ import { eq, and } from "@porulle/core/drizzle";
2
+ import { Ok, Err } from "@porulle/core";
3
+ import type { PluginResult } from "@porulle/core";
4
+ import { wishlistItems } from "../schema.js";
5
+ import type { Db, WishlistItem } from "../types.js";
6
+
7
+ export class WishlistService {
8
+ constructor(private db: Db) {}
9
+
10
+ async list(orgId: string, userId: string): Promise<PluginResult<WishlistItem[]>> {
11
+ const rows = await this.db.select().from(wishlistItems)
12
+ .where(and(eq(wishlistItems.organizationId, orgId), eq(wishlistItems.userId, userId)));
13
+ return Ok(rows);
14
+ }
15
+
16
+ async add(orgId: string, userId: string, input: {
17
+ entityId: string; note?: string;
18
+ }): Promise<PluginResult<WishlistItem>> {
19
+ // Check duplicate
20
+ const existing = await this.db.select().from(wishlistItems)
21
+ .where(and(
22
+ eq(wishlistItems.organizationId, orgId),
23
+ eq(wishlistItems.userId, userId),
24
+ eq(wishlistItems.entityId, input.entityId),
25
+ ));
26
+ if (existing.length > 0) return Err("Item already in wishlist");
27
+
28
+ const rows = await this.db.insert(wishlistItems).values({
29
+ organizationId: orgId, userId, entityId: input.entityId, note: input.note,
30
+ }).returning();
31
+ return Ok(rows[0]!);
32
+ }
33
+
34
+ async remove(orgId: string, userId: string, itemId: string): Promise<PluginResult<{ deleted: boolean }>> {
35
+ const rows = await this.db.delete(wishlistItems)
36
+ .where(and(
37
+ eq(wishlistItems.id, itemId),
38
+ eq(wishlistItems.organizationId, orgId),
39
+ eq(wishlistItems.userId, userId),
40
+ )).returning();
41
+ if (rows.length === 0) return Err("Item not found");
42
+ return Ok({ deleted: true });
43
+ }
44
+
45
+ async removeByEntity(orgId: string, userId: string, entityId: string): Promise<PluginResult<{ deleted: boolean }>> {
46
+ const rows = await this.db.delete(wishlistItems)
47
+ .where(and(
48
+ eq(wishlistItems.organizationId, orgId),
49
+ eq(wishlistItems.userId, userId),
50
+ eq(wishlistItems.entityId, entityId),
51
+ )).returning();
52
+ if (rows.length === 0) return Err("Item not found");
53
+ return Ok({ deleted: true });
54
+ }
55
+ }
package/src/types.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { wishlistItems } from "./schema.js";
2
+ export type { PluginDb as Db } from "@porulle/core";
3
+ export type WishlistItem = typeof wishlistItems.$inferSelect;