@nubase/create 0.1.23 → 0.1.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubase/create",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Create a new Nubase application",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,34 @@
1
+ import { createHandlerFactory } from "@nubase/backend";
2
+ import { apiEndpoints, type ApiEndpoints } from "schema";
3
+ import type { __PROJECT_NAME_PASCAL__User } from "../auth";
4
+
5
+ /**
6
+ * Pre-configured handler factory for __PROJECT_NAME_PASCAL__.
7
+ *
8
+ * This factory pre-binds:
9
+ * - The apiEndpoints object for endpoint schema inference
10
+ * - The __PROJECT_NAME_PASCAL__User type for authenticated handlers
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * // Required auth - user is guaranteed to be __PROJECT_NAME_PASCAL__User
15
+ * getTickets: createHandler((e) => e.getTickets, {
16
+ * auth: "required",
17
+ * handler: async ({ params, user, ctx }) => { ... },
18
+ * }),
19
+ *
20
+ * // No auth (default) - no user in handler
21
+ * loginStart: createHandler((e) => e.loginStart, {
22
+ * handler: async ({ body }) => { ... },
23
+ * }),
24
+ *
25
+ * // Optional auth - user may be null
26
+ * getMe: createHandler((e) => e.getMe, {
27
+ * auth: "optional",
28
+ * handler: async ({ user }) => { ... },
29
+ * }),
30
+ * ```
31
+ */
32
+ export const createHandler = createHandlerFactory<ApiEndpoints, __PROJECT_NAME_PASCAL__User>({
33
+ endpoints: apiEndpoints,
34
+ });
@@ -1,20 +1,15 @@
1
- import {
2
- createHttpHandler,
3
- getAuthController,
4
- HttpError,
5
- } from "@nubase/backend";
1
+ import { getAuthController, HttpError } from "@nubase/backend";
6
2
  import bcrypt from "bcrypt";
7
3
  import { and, eq, inArray } from "drizzle-orm";
8
- import { apiEndpoints } from "schema";
9
4
  import jwt from "jsonwebtoken";
10
5
  import type { __PROJECT_NAME_PASCAL__User } from "../../auth";
11
6
  import { getAdminDb } from "../../db/helpers/drizzle";
12
7
  import { users, userWorkspaces, workspaces } from "../../db/schema";
8
+ import { createHandler } from "../handler-factory";
13
9
 
14
10
  // Short-lived secret for login tokens (in production, use a proper secret)
15
11
  const LOGIN_TOKEN_SECRET =
16
- process.env.LOGIN_TOKEN_SECRET ||
17
- "nubase-login-token-secret-change-in-production";
12
+ process.env.LOGIN_TOKEN_SECRET || "nubase-login-token-secret-change-in-production";
18
13
  const LOGIN_TOKEN_EXPIRY = "5m"; // 5 minutes to complete workspace selection
19
14
 
20
15
  interface LoginTokenPayload {
@@ -27,8 +22,7 @@ export const authHandlers = {
27
22
  * Login Start handler - Step 1 of two-step auth.
28
23
  * Validates credentials and returns list of workspaces.
29
24
  */
30
- loginStart: createHttpHandler({
31
- endpoint: apiEndpoints.loginStart,
25
+ loginStart: createHandler((e) => e.loginStart, {
32
26
  handler: async ({ body }) => {
33
27
  // Find user by email
34
28
  const [user] = await getAdminDb()
@@ -41,10 +35,7 @@ export const authHandlers = {
41
35
  }
42
36
 
43
37
  // Verify password
44
- const isValidPassword = await bcrypt.compare(
45
- body.password,
46
- user.passwordHash,
47
- );
38
+ const isValidPassword = await bcrypt.compare(body.password, user.passwordHash);
48
39
  if (!isValidPassword) {
49
40
  throw new HttpError(401, "Invalid email or password");
50
41
  }
@@ -92,18 +83,14 @@ export const authHandlers = {
92
83
  * Login Complete handler - Step 2 of two-step auth.
93
84
  * Validates the login token and selected workspace.
94
85
  */
95
- loginComplete: createHttpHandler({
96
- endpoint: apiEndpoints.loginComplete,
86
+ loginComplete: createHandler((e) => e.loginComplete, {
97
87
  handler: async ({ body, ctx }) => {
98
88
  const authController = getAuthController<__PROJECT_NAME_PASCAL__User>(ctx);
99
89
 
100
90
  // Verify the login token
101
91
  let decoded: LoginTokenPayload;
102
92
  try {
103
- decoded = jwt.verify(
104
- body.loginToken,
105
- LOGIN_TOKEN_SECRET,
106
- ) as LoginTokenPayload;
93
+ decoded = jwt.verify(body.loginToken, LOGIN_TOKEN_SECRET) as LoginTokenPayload;
107
94
  } catch {
108
95
  throw new HttpError(401, "Invalid or expired login token");
109
96
  }
@@ -174,8 +161,7 @@ export const authHandlers = {
174
161
  * Legacy Login handler - validates credentials and sets HttpOnly cookie.
175
162
  * @deprecated Use loginStart and loginComplete for two-step flow
176
163
  */
177
- login: createHttpHandler({
178
- endpoint: apiEndpoints.login,
164
+ login: createHandler((e) => e.login, {
179
165
  handler: async ({ body, ctx }) => {
180
166
  const authController = getAuthController<__PROJECT_NAME_PASCAL__User>(ctx);
181
167
 
@@ -200,10 +186,7 @@ export const authHandlers = {
200
186
  }
201
187
 
202
188
  // Verify password
203
- const isValidPassword = await bcrypt.compare(
204
- body.password,
205
- dbUser.passwordHash,
206
- );
189
+ const isValidPassword = await bcrypt.compare(body.password, dbUser.passwordHash);
207
190
  if (!isValidPassword) {
208
191
  throw new HttpError(401, "Invalid email or password");
209
192
  }
@@ -246,8 +229,7 @@ export const authHandlers = {
246
229
  }),
247
230
 
248
231
  /** Logout handler - clears the auth cookie. */
249
- logout: createHttpHandler({
250
- endpoint: apiEndpoints.logout,
232
+ logout: createHandler((e) => e.logout, {
251
233
  handler: async ({ ctx }) => {
252
234
  const authController = getAuthController(ctx);
253
235
  authController.clearTokenFromResponse(ctx);
@@ -256,12 +238,7 @@ export const authHandlers = {
256
238
  }),
257
239
 
258
240
  /** Get current user handler. */
259
- getMe: createHttpHandler<
260
- typeof apiEndpoints.getMe,
261
- "optional",
262
- __PROJECT_NAME_PASCAL__User
263
- >({
264
- endpoint: apiEndpoints.getMe,
241
+ getMe: createHandler((e) => e.getMe, {
265
242
  auth: "optional",
266
243
  handler: async ({ user }) => {
267
244
  if (!user) {
@@ -279,8 +256,7 @@ export const authHandlers = {
279
256
  }),
280
257
 
281
258
  /** Signup handler - creates a new workspace and admin user. */
282
- signup: createHttpHandler({
283
- endpoint: apiEndpoints.signup,
259
+ signup: createHandler((e) => e.signup, {
284
260
  handler: async ({ body, ctx }) => {
285
261
  const authController = getAuthController<__PROJECT_NAME_PASCAL__User>(ctx);
286
262
 
@@ -1,5 +1,4 @@
1
- import { createHttpHandler } from "@nubase/backend";
2
- import { apiEndpoints } from "schema";
1
+ import { createHandler } from "../handler-factory";
3
2
 
4
3
  /**
5
4
  * Dashboard widget endpoints.
@@ -7,8 +6,7 @@ import { apiEndpoints } from "schema";
7
6
  */
8
7
  export const dashboardHandlers = {
9
8
  /** Revenue chart - returns series data for area/line/bar charts. */
10
- getRevenueChart: createHttpHandler({
11
- endpoint: apiEndpoints.getRevenueChart,
9
+ getRevenueChart: createHandler((e) => e.getRevenueChart, {
12
10
  handler: async () => ({
13
11
  type: "series",
14
12
  config: {
@@ -26,8 +24,7 @@ export const dashboardHandlers = {
26
24
  }),
27
25
 
28
26
  /** Browser stats - returns proportional data for pie/donut charts. */
29
- getBrowserStats: createHttpHandler({
30
- endpoint: apiEndpoints.getBrowserStats,
27
+ getBrowserStats: createHandler((e) => e.getBrowserStats, {
31
28
  handler: async () => ({
32
29
  type: "proportional",
33
30
  data: [
@@ -41,8 +38,7 @@ export const dashboardHandlers = {
41
38
  }),
42
39
 
43
40
  /** Total revenue KPI - returns single value with trend. */
44
- getTotalRevenue: createHttpHandler({
45
- endpoint: apiEndpoints.getTotalRevenue,
41
+ getTotalRevenue: createHandler((e) => e.getTotalRevenue, {
46
42
  handler: async () => ({
47
43
  type: "kpi",
48
44
  value: "$45,231.89",
@@ -53,8 +49,7 @@ export const dashboardHandlers = {
53
49
  }),
54
50
 
55
51
  /** Active users KPI - returns single value with trend. */
56
- getActiveUsers: createHttpHandler({
57
- endpoint: apiEndpoints.getActiveUsers,
52
+ getActiveUsers: createHandler((e) => e.getActiveUsers, {
58
53
  handler: async () => ({
59
54
  type: "kpi",
60
55
  value: "+2,350",
@@ -65,8 +60,7 @@ export const dashboardHandlers = {
65
60
  }),
66
61
 
67
62
  /** Sales chart - returns series data for bar charts. */
68
- getSalesChart: createHttpHandler({
69
- endpoint: apiEndpoints.getSalesChart,
63
+ getSalesChart: createHandler((e) => e.getSalesChart, {
70
64
  handler: async () => ({
71
65
  type: "series",
72
66
  config: {
@@ -85,8 +79,7 @@ export const dashboardHandlers = {
85
79
  }),
86
80
 
87
81
  /** Recent activity - returns table data. */
88
- getRecentActivity: createHttpHandler({
89
- endpoint: apiEndpoints.getRecentActivity,
82
+ getRecentActivity: createHandler((e) => e.getRecentActivity, {
90
83
  handler: async () => ({
91
84
  type: "table",
92
85
  columns: [
@@ -1,19 +1,30 @@
1
- import { createHttpHandler, HttpError } from "@nubase/backend";
1
+ import { HttpError } from "@nubase/backend";
2
2
  import type { SQL } from "drizzle-orm";
3
- import { and, eq, ilike, inArray } from "drizzle-orm";
4
- import { apiEndpoints } from "schema";
3
+ import { and, eq, ilike, inArray, or } from "drizzle-orm";
5
4
  import { getDb } from "../../db/helpers/drizzle";
6
5
  import { tickets } from "../../db/schema";
6
+ import { createHandler } from "../handler-factory";
7
7
 
8
8
  export const ticketHandlers = {
9
- getTickets: createHttpHandler({
10
- endpoint: apiEndpoints.getTickets,
9
+ getTickets: createHandler((e) => e.getTickets, {
11
10
  handler: async ({ params }) => {
12
11
  const db = getDb();
13
12
 
14
13
  // Build filter conditions
15
14
  const conditions: SQL[] = [];
16
15
 
16
+ // Global text search - OR across searchable text fields
17
+ if (params.q) {
18
+ const searchTerm = `%${params.q}%`;
19
+ const searchCondition = or(
20
+ ilike(tickets.title, searchTerm),
21
+ ilike(tickets.description, searchTerm),
22
+ );
23
+ if (searchCondition) {
24
+ conditions.push(searchCondition);
25
+ }
26
+ }
27
+
17
28
  // Filter by title (case-insensitive partial match)
18
29
  if (params.title) {
19
30
  conditions.push(ilike(tickets.title, `%${params.title}%`));
@@ -52,8 +63,7 @@ export const ticketHandlers = {
52
63
  },
53
64
  }),
54
65
 
55
- getTicket: createHttpHandler({
56
- endpoint: apiEndpoints.getTicket,
66
+ getTicket: createHandler((e) => e.getTicket, {
57
67
  handler: async ({ params }) => {
58
68
  const [ticket] = await getDb()
59
69
  .select()
@@ -73,8 +83,7 @@ export const ticketHandlers = {
73
83
  },
74
84
  }),
75
85
 
76
- postTicket: createHttpHandler({
77
- endpoint: apiEndpoints.postTicket,
86
+ postTicket: createHandler((e) => e.postTicket, {
78
87
  handler: async ({ body }) => {
79
88
  const [ticket] = await getDb()
80
89
  .insert(tickets)
@@ -99,8 +108,7 @@ export const ticketHandlers = {
99
108
  },
100
109
  }),
101
110
 
102
- patchTicket: createHttpHandler({
103
- endpoint: apiEndpoints.patchTicket,
111
+ patchTicket: createHandler((e) => e.patchTicket, {
104
112
  handler: async ({ params, body }) => {
105
113
  const updateData: {
106
114
  title?: string;
@@ -140,8 +148,7 @@ export const ticketHandlers = {
140
148
  },
141
149
  }),
142
150
 
143
- deleteTicket: createHttpHandler({
144
- endpoint: apiEndpoints.deleteTicket,
151
+ deleteTicket: createHandler((e) => e.deleteTicket, {
145
152
  handler: async ({ params }) => {
146
153
  const [deleted] = await getDb()
147
154
  .delete(tickets)
@@ -1,20 +1,31 @@
1
- import { createHttpHandler, HttpError } from "@nubase/backend";
1
+ import { HttpError } from "@nubase/backend";
2
2
  import type { SQL } from "drizzle-orm";
3
- import { and, eq, ilike } from "drizzle-orm";
4
- import { apiEndpoints } from "schema";
3
+ import { and, eq, ilike, or } from "drizzle-orm";
5
4
  import { getDb } from "../../db/helpers/drizzle";
6
5
  import { users } from "../../db/schema";
6
+ import { createHandler } from "../handler-factory";
7
7
 
8
8
  export const userHandlers = {
9
9
  /** Get all users with optional filters. */
10
- getUsers: createHttpHandler({
11
- endpoint: apiEndpoints.getUsers,
10
+ getUsers: createHandler((e) => e.getUsers, {
12
11
  handler: async ({ params }) => {
13
12
  const db = getDb();
14
13
 
15
14
  // Build filter conditions
16
15
  const conditions: SQL[] = [];
17
16
 
17
+ // Global text search - OR across searchable text fields
18
+ if (params.q) {
19
+ const searchTerm = `%${params.q}%`;
20
+ const searchCondition = or(
21
+ ilike(users.displayName, searchTerm),
22
+ ilike(users.email, searchTerm),
23
+ );
24
+ if (searchCondition) {
25
+ conditions.push(searchCondition);
26
+ }
27
+ }
28
+
18
29
  // Filter by displayName (case-insensitive partial match)
19
30
  if (params.displayName) {
20
31
  conditions.push(ilike(users.displayName, `%${params.displayName}%`));
@@ -41,8 +52,7 @@ export const userHandlers = {
41
52
  }),
42
53
 
43
54
  /** Get a single user by ID. */
44
- getUser: createHttpHandler({
45
- endpoint: apiEndpoints.getUser,
55
+ getUser: createHandler((e) => e.getUser, {
46
56
  handler: async ({ params }) => {
47
57
  const db = getDb();
48
58
  const [user] = await db.select().from(users).where(eq(users.id, params.id));
@@ -60,8 +70,7 @@ export const userHandlers = {
60
70
  }),
61
71
 
62
72
  /** Create a new user. */
63
- postUser: createHttpHandler({
64
- endpoint: apiEndpoints.postUser,
73
+ postUser: createHandler((e) => e.postUser, {
65
74
  handler: async ({ body }) => {
66
75
  const db = getDb();
67
76
 
@@ -94,8 +103,7 @@ export const userHandlers = {
94
103
  }),
95
104
 
96
105
  /** Update a user by ID. */
97
- patchUser: createHttpHandler({
98
- endpoint: apiEndpoints.patchUser,
106
+ patchUser: createHandler((e) => e.patchUser, {
99
107
  handler: async ({ params, body }) => {
100
108
  const db = getDb();
101
109
 
@@ -126,8 +134,7 @@ export const userHandlers = {
126
134
  }),
127
135
 
128
136
  /** Delete a user by ID. */
129
- deleteUser: createHttpHandler({
130
- endpoint: apiEndpoints.deleteUser,
137
+ deleteUser: createHandler((e) => e.deleteUser, {
131
138
  handler: async ({ params }) => {
132
139
  const db = getDb();
133
140
 
@@ -142,8 +149,7 @@ export const userHandlers = {
142
149
  }),
143
150
 
144
151
  /** Lookup users for select/autocomplete fields. */
145
- lookupUsers: createHttpHandler({
146
- endpoint: apiEndpoints.lookupUsers,
152
+ lookupUsers: createHandler((e) => e.lookupUsers, {
147
153
  handler: async ({ params }) => {
148
154
  const db = getDb();
149
155
 
@@ -100,12 +100,13 @@ export const userResource = createResource("user")
100
100
  id: "search-users",
101
101
  title: "Users",
102
102
  schemaGet: (api) => api.getUsers.responseBody,
103
+ schemaFilter: (api) => api.getUsers.requestParams,
103
104
  breadcrumbs: () => [{ label: "Users", to: "/r/user/search" }],
104
105
  tableActions: ["delete"],
105
106
  rowActions: ["delete"],
106
107
  onLoad: async ({ context }) => {
107
108
  return context.http.getUsers({
108
- params: {},
109
+ params: context.params || {},
109
110
  });
110
111
  },
111
112
  },
@@ -1,9 +1,9 @@
1
- import { nu, type RequestSchema } from "@nubase/core";
1
+ import { nu, type RequestSchema, withSearchParams } from "@nubase/core";
2
2
  import { ticketSchema } from "../../resources/ticket";
3
3
 
4
4
  export const getTicketsSchema = {
5
5
  method: "GET" as const,
6
6
  path: "/tickets",
7
- requestParams: ticketSchema.omit("id").partial(),
7
+ requestParams: withSearchParams(ticketSchema.omit("id").partial()),
8
8
  responseBody: nu.array(ticketSchema),
9
9
  } satisfies RequestSchema;
@@ -1,9 +1,9 @@
1
- import { nu, type RequestSchema } from "@nubase/core";
1
+ import { nu, type RequestSchema, withSearchParams } from "@nubase/core";
2
2
  import { userSchema } from "../../resources/user";
3
3
 
4
4
  export const getUsersSchema = {
5
5
  method: "GET" as const,
6
6
  path: "/users",
7
- requestParams: userSchema.omit("id").partial(),
7
+ requestParams: withSearchParams(userSchema.omit("id").partial()),
8
8
  responseBody: nu.array(userSchema),
9
9
  } satisfies RequestSchema;