@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 +54 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/routes/wishlist.d.ts +9 -0
- package/dist/routes/wishlist.d.ts.map +1 -0
- package/dist/routes/wishlist.js +38 -0
- package/dist/schema.d.ts +110 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +20 -0
- package/dist/services/wishlist-service.d.ts +18 -0
- package/dist/services/wishlist-service.d.ts.map +1 -0
- package/dist/services/wishlist-service.js +39 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +55 -0
- package/src/index.ts +25 -0
- package/src/routes/wishlist.ts +46 -0
- package/src/schema.ts +28 -0
- package/src/services/wishlist-service.ts +55 -0
- package/src/types.ts +3 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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