hazo_notes 1.1.6 → 2.0.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.
Files changed (62) hide show
  1. package/README.md +54 -11
  2. package/SETUP_CHECKLIST.md +28 -12
  3. package/config/hazo_notes_config.ini +48 -0
  4. package/db_setup_postgres.sql +49 -0
  5. package/db_setup_sqlite.sql +22 -0
  6. package/dist/api/create_files_handler.d.ts +1 -1
  7. package/dist/api/create_files_handler.d.ts.map +1 -1
  8. package/dist/api/create_files_handler.js +196 -139
  9. package/dist/api/create_files_handler.js.map +1 -1
  10. package/dist/api/create_notes_handler.d.ts.map +1 -1
  11. package/dist/api/create_notes_handler.js +246 -160
  12. package/dist/api/create_notes_handler.js.map +1 -1
  13. package/dist/components/hazo_notes_entry.js +1 -1
  14. package/dist/components/hazo_notes_entry.js.map +1 -1
  15. package/dist/components/hazo_notes_file_preview.js +1 -1
  16. package/dist/components/hazo_notes_file_preview.js.map +1 -1
  17. package/dist/components/hazo_notes_icon.js +1 -1
  18. package/dist/components/hazo_notes_icon.js.map +1 -1
  19. package/dist/components/hazo_notes_panel.js +1 -1
  20. package/dist/components/hazo_notes_panel.js.map +1 -1
  21. package/dist/components/internal/sheet.js +1 -1
  22. package/dist/components/internal/sheet.js.map +1 -1
  23. package/dist/index.client.d.ts +0 -1
  24. package/dist/index.client.d.ts.map +1 -1
  25. package/dist/index.client.js +2 -2
  26. package/dist/index.client.js.map +1 -1
  27. package/dist/lib/config/hazo_notes_config.d.ts +125 -0
  28. package/dist/lib/config/hazo_notes_config.d.ts.map +1 -0
  29. package/dist/lib/config/hazo_notes_config.js +76 -0
  30. package/dist/lib/config/hazo_notes_config.js.map +1 -0
  31. package/dist/lib/config/index.d.ts +3 -0
  32. package/dist/lib/config/index.d.ts.map +1 -0
  33. package/dist/lib/config/index.js +2 -0
  34. package/dist/lib/config/index.js.map +1 -0
  35. package/dist/lib/config.d.ts +14 -15
  36. package/dist/lib/config.d.ts.map +1 -1
  37. package/dist/lib/config.js +45 -93
  38. package/dist/lib/config.js.map +1 -1
  39. package/dist/lib/index.d.ts +2 -0
  40. package/dist/lib/index.d.ts.map +1 -1
  41. package/dist/lib/index.js +1 -0
  42. package/dist/lib/index.js.map +1 -1
  43. package/dist/logger/server.d.ts +13 -9
  44. package/dist/logger/server.d.ts.map +1 -1
  45. package/dist/logger/server.js +21 -15
  46. package/dist/logger/server.js.map +1 -1
  47. package/dist/logger/types.d.ts +3 -2
  48. package/dist/logger/types.d.ts.map +1 -1
  49. package/dist/logger/types.js +2 -1
  50. package/dist/logger/types.js.map +1 -1
  51. package/dist/types/index.d.ts +31 -1
  52. package/dist/types/index.d.ts.map +1 -1
  53. package/dist/utils/index.d.ts +2 -1
  54. package/dist/utils/index.d.ts.map +1 -1
  55. package/dist/utils/index.js +2 -1
  56. package/dist/utils/index.js.map +1 -1
  57. package/package.json +30 -15
  58. package/dist/utils/cn.d.ts +0 -16
  59. package/dist/utils/cn.d.ts.map +0 -1
  60. package/dist/utils/cn.js +0 -19
  61. package/dist/utils/cn.js.map +0 -1
  62. package/migrations/001_create_hazo_notes_table.sql +0 -77
package/README.md CHANGED
@@ -70,21 +70,24 @@ Create `app/api/hazo_notes/[ref_id]/route.ts`:
70
70
  ```typescript
71
71
  import { createNotesHandler } from 'hazo_notes/api';
72
72
  import { getHazoConnectSingleton } from 'hazo_connect/nextjs/setup';
73
- // Import your auth and user lookup functions
74
- import { getSession } from '@/lib/auth'; // Replace with your auth
73
+ import { hazo_get_tenant_auth } from 'hazo_auth/server-lib';
75
74
  import { getUserById } from '@/lib/users'; // Replace with your user lookup
75
+ import type { NextRequest } from 'next/server';
76
76
 
77
77
  export const dynamic = 'force-dynamic';
78
78
 
79
79
  const { GET, POST } = createNotesHandler({
80
80
  getHazoConnect: () => getHazoConnectSingleton(),
81
- getUserIdFromRequest: async (req) => {
82
- // IMPORTANT: Replace with your authentication logic
83
- const session = await getSession(req);
84
- return session?.user?.id || null;
81
+ // Preferred: hazo_auth@6.x tenant auth. Used by both GET and POST.
82
+ getAuth: async (req) => {
83
+ const auth = await hazo_get_tenant_auth(req as NextRequest);
84
+ if (!auth.authenticated || !auth.user?.id) return null;
85
+ return {
86
+ user_id: auth.user.id,
87
+ scope_id: auth.selected_scope_id ?? null,
88
+ };
85
89
  },
86
90
  getUserProfile: async (userId) => {
87
- // IMPORTANT: Replace with your user profile lookup
88
91
  const user = await getUserById(userId);
89
92
  return {
90
93
  id: userId,
@@ -98,6 +101,15 @@ const { GET, POST } = createNotesHandler({
98
101
  export { GET, POST };
99
102
  ```
100
103
 
104
+ > **GET requires auth.** Starting in `1.2.0`, GET responds `401` when the auth
105
+ > resolver returns `null`, matching POST. Older releases left GET anonymous.
106
+ > If you're still on `hazo_auth@5.x`, use `getUserIdFromRequest` instead of
107
+ > `getAuth` — the package falls back to it for backward compatibility.
108
+
109
+ > **Error detail.** On `500` responses the underlying error message is
110
+ > included as `cause` in dev (when `NODE_ENV !== 'production'`). Set
111
+ > `expose_error_cause: false` to suppress, or `true` to force it on in prod.
112
+
101
113
  ### 3. Set Up Database
102
114
 
103
115
  Run the migration:
@@ -114,6 +126,11 @@ CREATE TABLE hazo_notes (
114
126
  );
115
127
 
116
128
  CREATE INDEX idx_hazo_notes_ref_id ON hazo_notes(ref_id);
129
+
130
+ -- REQUIRED for PostgREST consumers — without this, GET/POST return 500 with
131
+ -- cause "permission denied for table hazo_notes" (PostgREST error 42501).
132
+ -- Shipped as migrations/002_grant_api_user_hazo_notes.sql.
133
+ GRANT SELECT, INSERT, UPDATE, DELETE ON hazo_notes TO api_user;
117
134
  ```
118
135
 
119
136
  **SQLite:**
@@ -672,17 +689,43 @@ getUserProfile: async (userId) => {
672
689
 
673
690
  ### Authentication errors
674
691
 
675
- **Problem**: "Unauthorized" when adding notes.
692
+ **Problem**: `401 Unauthorized` on GET or POST.
693
+
694
+ **Solution**: Wire `getAuth` (preferred, hazo_auth@6.x) or `getUserIdFromRequest`
695
+ (hazo_auth@5.x). GET and POST both require a non-null user; both return `401`
696
+ if the resolver returns `null`.
676
697
 
677
- **Solution**: Implement `getUserIdFromRequest` to return authenticated user ID:
678
698
  ```typescript
699
+ // hazo_auth@6.x — preferred
700
+ getAuth: async (req) => {
701
+ const auth = await hazo_get_tenant_auth(req as NextRequest);
702
+ if (!auth.authenticated || !auth.user?.id) return null;
703
+ return { user_id: auth.user.id, scope_id: auth.selected_scope_id ?? null };
704
+ }
705
+
706
+ // hazo_auth@5.x — backward-compatible fallback
679
707
  getUserIdFromRequest: async (req) => {
680
708
  const session = await getSession(req);
681
- if (!session?.user?.id) return null;
682
- return session.user.id;
709
+ return session?.user?.id ?? null;
683
710
  }
684
711
  ```
685
712
 
713
+ ### PostgREST `permission denied for table hazo_notes` (500 with cause 42501)
714
+
715
+ **Problem**: GET/POST return 500. Response includes
716
+ `"cause": "permission denied for table hazo_notes"` (when `expose_error_cause`
717
+ is enabled).
718
+
719
+ **Solution**: Apply `migrations/002_grant_api_user_hazo_notes.sql`. It grants
720
+ `SELECT, INSERT, UPDATE, DELETE` on `hazo_notes` to the `api_user` role used
721
+ by PostgREST. Reproducing the underlying error directly:
722
+
723
+ ```bash
724
+ curl "$POSTGREST_URL/hazo_notes?limit=1"
725
+ # Before grant: 401 / "permission denied for table hazo_notes"
726
+ # After grant: [] or rows
727
+ ```
728
+
686
729
  ## Examples
687
730
 
688
731
  See the `test-app/` directory for complete working examples:
@@ -66,15 +66,25 @@ CREATE TABLE IF NOT EXISTS hazo_notes (
66
66
  CREATE INDEX IF NOT EXISTS idx_hazo_notes_ref_id ON hazo_notes(ref_id);
67
67
  ```
68
68
 
69
- **Alternative**: Use the provided migration file:
69
+ **Alternative**: Use the provided migration files:
70
70
  ```bash
71
- # PostgreSQL
71
+ # PostgreSQL — apply both files in order
72
72
  psql -d your_database -f node_modules/hazo_notes/migrations/001_create_hazo_notes_table.sql
73
+ psql -d your_database -f node_modules/hazo_notes/migrations/002_grant_api_user_hazo_notes.sql
73
74
 
74
- # SQLite
75
+ # SQLite — only 001 (SQLite has no role system, skip 002)
75
76
  sqlite3 your_database.db < node_modules/hazo_notes/migrations/001_create_hazo_notes_table.sql
76
77
  ```
77
78
 
79
+ > **PostgREST consumers MUST apply migration 002.** Without it, PostgREST
80
+ > returns `42501 / permission denied for table hazo_notes` and the API
81
+ > surfaces a generic 500. Migration 002 grants
82
+ > `SELECT, INSERT, UPDATE, DELETE` on `hazo_notes` to `api_user`. Verify with:
83
+ >
84
+ > ```bash
85
+ > curl "$POSTGREST_URL/hazo_notes?limit=1" # should return [] not 401
86
+ > ```
87
+
78
88
  ## API Routes
79
89
 
80
90
  ### 4. Create the notes API route
@@ -84,25 +94,27 @@ Create `app/api/hazo_notes/[ref_id]/route.ts`:
84
94
  ```typescript
85
95
  import { createNotesHandler } from 'hazo_notes/api';
86
96
  import { getHazoConnectSingleton } from 'hazo_connect/nextjs/setup';
87
- // Import your authentication and user lookup functions
88
- import { getSession } from '@/lib/auth';
97
+ import { hazo_get_tenant_auth } from 'hazo_auth/server-lib';
89
98
  import { getUserById } from '@/lib/users';
99
+ import type { NextRequest } from 'next/server';
90
100
 
91
101
  export const dynamic = 'force-dynamic';
92
102
 
93
103
  const { GET, POST } = createNotesHandler({
94
104
  getHazoConnect: () => getHazoConnectSingleton(),
95
105
 
96
- getUserIdFromRequest: async (req) => {
97
- // IMPORTANT: Replace this with your actual authentication logic
98
- // This function should return the authenticated user's ID or null
99
- const session = await getSession(req);
100
- return session?.user?.id || null;
106
+ // Preferred (hazo_auth@6.x). Used by BOTH GET and POST — both require auth.
107
+ getAuth: async (req) => {
108
+ const auth = await hazo_get_tenant_auth(req as NextRequest);
109
+ if (!auth.authenticated || !auth.user?.id) return null;
110
+ return {
111
+ user_id: auth.user.id,
112
+ scope_id: auth.selected_scope_id ?? null,
113
+ };
101
114
  },
102
115
 
103
116
  getUserProfile: async (userId) => {
104
117
  // IMPORTANT: Replace this with your actual user profile lookup
105
- // This function fetches user details for attribution
106
118
  const user = await getUserById(userId);
107
119
  return {
108
120
  id: userId,
@@ -117,9 +129,13 @@ export { GET, POST };
117
129
  ```
118
130
 
119
131
  **Important Notes**:
120
- - The `getUserIdFromRequest` function MUST return a valid user ID for POST requests
132
+ - Either `getAuth` (hazo_auth@6.x) or `getUserIdFromRequest` (hazo_auth@5.x)
133
+ MUST be provided. GET and POST both return `401` if the resolver returns
134
+ `null`. GET-anonymous behavior from `1.1.x` is gone in `1.2.0`.
121
135
  - The `getUserProfile` function enriches notes with user information
122
136
  - If you don't have user profiles, you can omit `getUserProfile`
137
+ - On a 500 response, the underlying error message is included as `cause`
138
+ in non-production. Set `expose_error_cause: false` to suppress.
123
139
 
124
140
  ### 5. (Optional) Create the files API route for file attachments
125
141
 
@@ -0,0 +1,48 @@
1
+ # hazo_notes Configuration
2
+ # This file configures the notes system behavior and storage
3
+ #
4
+ # Env-var overrides follow the pattern HAZO_NOTES_<SECTION>_<KEY> (uppercase).
5
+ # Per-env overlays: place hazo_notes_config.<HAZO_ENV>.ini next to this file.
6
+
7
+ [general]
8
+ # Optional override for the deployment environment. Defaults to
9
+ # HAZO_ENV → NODE_ENV → 'development' (resolved by hazo_core).
10
+ # env = production
11
+
12
+ [log.overrides]
13
+ # Per-namespace log level overrides (per D-015). Format:
14
+ # <namespace> = <level>
15
+ # Examples:
16
+ # hazo_notes/poll = warn
17
+ # hazo_notes = debug
18
+
19
+ [ui]
20
+ # Background color for notes panel (Tailwind CSS class)
21
+ background_color = bg-yellow-100
22
+
23
+ # Panel presentation style: popover | slide_panel
24
+ panel_style = popover
25
+
26
+ # Save behavior: explicit (save/cancel buttons) | auto (save on blur)
27
+ save_mode = explicit
28
+
29
+ [storage]
30
+ # File storage mode: jsonb (in database) | filesystem (on server)
31
+ file_storage_mode = jsonb
32
+
33
+ # Path for filesystem storage (only used when file_storage_mode = filesystem)
34
+ file_storage_path = /uploads/notes
35
+
36
+ [files]
37
+ # Maximum file size in MB
38
+ max_file_size_mb = 10
39
+
40
+ # Allowed file types (comma-separated extensions)
41
+ allowed_file_types = pdf,png,jpg,jpeg,gif,doc,docx
42
+
43
+ # Maximum files per single note entry
44
+ max_files_per_note = 5
45
+
46
+ [logging]
47
+ # Log file path
48
+ logfile = logs/hazo_notes.log
@@ -0,0 +1,49 @@
1
+ -- hazo_notes database schema
2
+ -- PostgreSQL -- idempotent (safe to run multiple times)
3
+ -- Run this file to set up all tables required by hazo_notes
4
+ --
5
+ -- Tables: hazo_notes
6
+
7
+ -- ============================================================
8
+ -- hazo_notes
9
+ -- Database-backed notes linked to any entity via ref_id.
10
+ -- Notes are stored as a JSONB array with user attribution and
11
+ -- optional file attachments.
12
+ -- ============================================================
13
+
14
+ CREATE TABLE IF NOT EXISTS hazo_notes (
15
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
16
+ ref_id UUID NOT NULL,
17
+ note JSONB NOT NULL DEFAULT '[]'::jsonb,
18
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
19
+ changed_at TIMESTAMPTZ NULL,
20
+ note_count INTEGER NOT NULL DEFAULT 0
21
+ );
22
+
23
+ CREATE INDEX IF NOT EXISTS idx_hazo_notes_ref_id ON hazo_notes(ref_id);
24
+
25
+ COMMENT ON TABLE hazo_notes IS 'Notes linked to any entity via ref_id. note column is a JSONB array of {userid, created_at, note_text, note_files}.';
26
+ COMMENT ON COLUMN hazo_notes.id IS 'Primary key UUID';
27
+ COMMENT ON COLUMN hazo_notes.ref_id IS 'UUID reference to the parent entity (e.g. form field, document, task)';
28
+ COMMENT ON COLUMN hazo_notes.note IS 'JSONB array of note entries';
29
+ COMMENT ON COLUMN hazo_notes.created_at IS 'Timestamp when the notes row was first created';
30
+ COMMENT ON COLUMN hazo_notes.changed_at IS 'Timestamp of the last modification to the notes';
31
+ COMMENT ON COLUMN hazo_notes.note_count IS 'Denormalised count of notes, kept in sync on write to avoid parsing JSONB on read';
32
+
33
+ -- ============================================================
34
+ -- PostgREST role grants
35
+ -- Required for the bundled API handlers, which talk to PostgREST.
36
+ -- The api_user role must exist beforehand (created by hazo_connect setup).
37
+ -- ============================================================
38
+
39
+ DO $$ BEGIN
40
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'api_user') THEN
41
+ GRANT SELECT, INSERT, UPDATE, DELETE ON hazo_notes TO api_user;
42
+ END IF;
43
+ END $$;
44
+
45
+ -- ============================================================
46
+ -- Notify PostgREST to reload the schema cache
47
+ -- ============================================================
48
+
49
+ SELECT pg_notify('pgrst', 'reload schema');
@@ -0,0 +1,22 @@
1
+ -- hazo_notes database schema
2
+ -- SQLite -- idempotent (safe to run multiple times)
3
+ -- Run this file to set up all tables required by hazo_notes
4
+ --
5
+ -- Tables: hazo_notes
6
+
7
+ -- ============================================================
8
+ -- hazo_notes
9
+ -- Notes linked to any entity via ref_id. The note column is a
10
+ -- JSON-encoded array stored as TEXT (SQLite has no native JSONB).
11
+ -- ============================================================
12
+
13
+ CREATE TABLE IF NOT EXISTS hazo_notes (
14
+ id TEXT PRIMARY KEY,
15
+ ref_id TEXT NOT NULL,
16
+ note TEXT NOT NULL DEFAULT '[]',
17
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
18
+ changed_at TEXT,
19
+ note_count INTEGER NOT NULL DEFAULT 0
20
+ );
21
+
22
+ CREATE INDEX IF NOT EXISTS idx_hazo_notes_ref_id ON hazo_notes(ref_id);
@@ -32,7 +32,7 @@ import type { CreateFilesHandlerOptions, FileUploadApiResponse } from '../types/
32
32
  * @returns Object with POST (upload) and GET (download) handlers
33
33
  */
34
34
  export declare function createFilesHandler(options: CreateFilesHandlerOptions): {
35
- GET: (request: Request, context: {
35
+ GET: (request: Request, _context: {
36
36
  params: Promise<{
37
37
  file_id?: string;
38
38
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"create_files_handler.d.ts","sourceRoot":"","sources":["../../src/api/create_files_handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EACV,yBAAyB,EAEzB,qBAAqB,EAEtB,MAAM,mBAAmB,CAAC;AAkC3B;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB;mBAyIxD,OAAO,WACP;QAAE,MAAM,EAAE,OAAO,CAAC;YAAE,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,KACjD,OAAO,CAAC,YAAY,CAAC;oBArHK,OAAO,KAAG,OAAO,CAAC,YAAY,CAAC,qBAAqB,CAAC,CAAC;EAuLpF"}
1
+ {"version":3,"file":"create_files_handler.d.ts","sourceRoot":"","sources":["../../src/api/create_files_handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAU3C,OAAO,KAAK,EACV,yBAAyB,EAEzB,qBAAqB,EAEtB,MAAM,mBAAmB,CAAC;AAqC3B;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB;mBAgKxD,OAAO,YACN;QAAE,MAAM,EAAE,OAAO,CAAC;YAAE,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,KAClD,OAAO,CAAC,YAAY,CAAC;oBA5IK,OAAO,KAAG,OAAO,CAAC,YAAY,CAAC,qBAAqB,CAAC,CAAC;EA0OpF"}