spooder 6.1.4 → 6.1.6

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 CHANGED
@@ -601,6 +601,9 @@ log_list(input: any[], delimiter = ', ');
601
601
  http_serve(port: number, hostname?: string): Server;
602
602
  server.stop(immediate: boolean): Promise<void>;
603
603
 
604
+ // cookies
605
+ cookies_get(req: Request): Bun.CookieMap
606
+
604
607
  // routing
605
608
  server.route(path: string, handler: RequestHandler, method?: HTTP_METHODS);
606
609
  server.json(path: string, handler: JSONRequestHandler, method?: HTTP_METHODS);
@@ -680,6 +683,7 @@ git_get_hashes_sync(length: number): Record<string, string>;
680
683
  // database utilities
681
684
  db_set_cast<T extends string>(set: string | null): Set<T>;
682
685
  db_set_serialize<T extends string>(set: Iterable<T> | null): string;
686
+ db_exists(db: SQL, table_name: string, value: string|number, column_name = 'id'): Promise<boolean>;
683
687
 
684
688
  // database schema
685
689
  type SchemaOptions = {
@@ -1417,35 +1421,6 @@ server.websocket('/path/to/websocket', {
1417
1421
  > [!IMPORTANT]
1418
1422
  > While it is possible to register multiple routes for websockets, the only handler which is unique per route is `accept()`. The last handlers provided to any route (with the exception of `accept()`) will apply to ALL websocket routes. This is a limitation in Bun.
1419
1423
 
1420
- <a id="api-http-cookies"></a>
1421
- ## API > HTTP > Cookies
1422
-
1423
- ### 🔧 `cookies_get(req: Request): Bun.CookieMap`
1424
-
1425
- When called on a request, the `cookies_get` function will return a `Bun.CookieMap` contains all of the cookies parsed from the `Cookie` header on the request.
1426
-
1427
- ```ts
1428
- server.route('/', (req, url) => {
1429
- const cookies = cookies_get(req);
1430
- return `Hello ${cookies.get('person') ?? 'unknown'}`;
1431
- });
1432
- ```
1433
-
1434
- The return `Bun.CookieMap` is an iterable map with a custom API for reading/setting cookies. The full API [can be seen here](https://bun.com/docs/runtime/cookies).
1435
-
1436
- Any changes made to the cookie map (adding, deletion, editing, etc) will be sent as `Set-Cookie` headers on the response automatically. Unchanged cookies are not sent.
1437
-
1438
- ```ts
1439
- server.route('/', (req, url) => {
1440
- const cookies = cookies_get(req);
1441
- cookies.set('test', 'foobar');
1442
- return 'Hello, world!';
1443
-
1444
- // the response automatically gets:
1445
- // Set-Cookie test=foobar; Path=/; SameSite=Lax
1446
- });
1447
- ```
1448
-
1449
1424
  <a id="api-http-bootstrap"></a>
1450
1425
  ## API > HTTP > Bootstrap
1451
1426
 
@@ -1778,6 +1753,35 @@ Functions in `global_subs` and route-specific `subs` are called during template
1778
1753
  3. Route-specific `subs` and `global_subs` are applied
1779
1754
  4. Hash substitutions (if enabled) are applied
1780
1755
 
1756
+ <a id="api-http-cookies"></a>
1757
+ ## API > HTTP > Cookies
1758
+
1759
+ ### 🔧 `cookies_get(req: Request): Bun.CookieMap`
1760
+
1761
+ When called on a request, the `cookies_get` function will return a `Bun.CookieMap` contains all of the cookies parsed from the `Cookie` header on the request.
1762
+
1763
+ ```ts
1764
+ server.route('/', (req, url) => {
1765
+ const cookies = cookies_get(req);
1766
+ return `Hello ${cookies.get('person') ?? 'unknown'}`;
1767
+ });
1768
+ ```
1769
+
1770
+ The return `Bun.CookieMap` is an iterable map with a custom API for reading/setting cookies. The full API [can be seen here](https://bun.com/docs/runtime/cookies).
1771
+
1772
+ Any changes made to the cookie map (adding, deletion, editing, etc) will be sent as `Set-Cookie` headers on the response automatically. Unchanged cookies are not sent.
1773
+
1774
+ ```ts
1775
+ server.route('/', (req, url) => {
1776
+ const cookies = cookies_get(req);
1777
+ cookies.set('test', 'foobar');
1778
+ return 'Hello, world!';
1779
+
1780
+ // the response automatically gets:
1781
+ // Set-Cookie test=foobar; Path=/; SameSite=Lax
1782
+ });
1783
+ ```
1784
+
1781
1785
  <a id="api-error-handling"></a>
1782
1786
  ## API > Error Handling
1783
1787
 
@@ -2589,7 +2593,7 @@ if (set.has(Fruits.Apple)) {
2589
2593
  }
2590
2594
  ```
2591
2595
 
2592
- ### 🔧 ``db_set_serialize<T extends string>(set: Iterable<T> | null): string``
2596
+ ### ``db_set_serialize<T extends string>(set: Iterable<T> | null): string``
2593
2597
 
2594
2598
  Takes an `Iterable<T>` and returns a database SET string. If the set is empty or `null`, it returns an empty string.
2595
2599
 
@@ -2613,6 +2617,18 @@ await sql`UPDATE some_table SET fruits = ${sql(db_set_serialize(fruits))} WHERE
2613
2617
  await sql`UPDATE some_table SET fruits = ${sql(db_set_serialize([Fruits.Apple, Fruits.Lemon]))}`;
2614
2618
  ```
2615
2619
 
2620
+ ### 🔧 ``db_exists(db: SQL, table_name: string, value: string|number, column_name = 'id'): Promise<boolean>``
2621
+
2622
+ Returns true if a database row exists in the table.
2623
+
2624
+ ```ts
2625
+ // checks if row exists with id 1 in 'table'
2626
+ const exists = await db_exists(db, 'table', 1);
2627
+
2628
+ // checks if row exists with column 'foo' = 'bar' in 'table'
2629
+ const exists = await db_exists(db, 'table', 'bar', 'foo');
2630
+ ```
2631
+
2616
2632
  <a id="api-database-schema"></a>
2617
2633
  ## API > Database > Schema
2618
2634
 
package/bun.lock CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "lockfileVersion": 1,
3
+ "configVersion": 1,
3
4
  "workspaces": {
4
5
  "": {
5
6
  "name": "spooder",
@@ -9,15 +10,11 @@
9
10
  },
10
11
  },
11
12
  "packages": {
12
- "@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
13
+ "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
13
14
 
14
- "@types/node": ["@types/node@24.9.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA=="],
15
+ "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
15
16
 
16
- "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
17
-
18
- "bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
19
-
20
- "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
17
+ "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
21
18
 
22
19
  "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
23
20
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "spooder",
3
3
  "author": "Kruithne <kruithne@gmail.com>",
4
4
  "type": "module",
5
- "version": "6.1.4",
5
+ "version": "6.1.6",
6
6
  "module": "./src/api.ts",
7
7
  "bin": {
8
8
  "spooder": "./src/cli.ts"
package/src/api.ts CHANGED
@@ -1841,24 +1841,37 @@ export function http_serve(port: number, hostname?: string) {
1841
1841
  /** Register a JSON endpoint with automatic content validation. */
1842
1842
  json: (path: string, handler: JSONRequestHandler, method: HTTP_METHODS = 'POST'): void => {
1843
1843
  const json_wrapper: RequestHandler = async (req: Request, url: URL) => {
1844
+ // handle CORS preflight
1845
+ if (req.method === 'OPTIONS') {
1846
+ return new Response(null, {
1847
+ status: 204,
1848
+ headers: {
1849
+ 'Access-Control-Allow-Origin': '*',
1850
+ 'Access-Control-Allow-Methods': `${Array.isArray(method) ? method.join(', ') : method}, OPTIONS`,
1851
+ 'Access-Control-Allow-Headers': 'Content-Type, User-Agent'
1852
+ }
1853
+ });
1854
+ }
1855
+
1844
1856
  try {
1845
1857
  if (req.headers.get('Content-Type') !== 'application/json')
1846
1858
  return 400; // Bad Request
1847
-
1859
+
1848
1860
  const json = await req.json();
1849
1861
  if (json === null || typeof json !== 'object' || Array.isArray(json))
1850
1862
  return 400; // Bad Request
1851
-
1863
+
1852
1864
  return handler(req, url, json as JsonObject);
1853
1865
  } catch (e) {
1854
1866
  return 400; // Bad Request
1855
1867
  }
1856
1868
  };
1857
-
1869
+
1858
1870
  if (path.length > 1 && path.endsWith('/'))
1859
1871
  path = path.slice(0, -1);
1860
-
1861
- routes.push([path.split('/'), json_wrapper, method]);
1872
+
1873
+ const methods: HTTP_METHODS = Array.isArray(method) ? [...method, 'OPTIONS'] : [method, 'OPTIONS'];
1874
+ routes.push([path.split('/'), json_wrapper, methods]);
1862
1875
  },
1863
1876
 
1864
1877
  /** Unregister a specific route */
@@ -2182,6 +2195,11 @@ export function db_set_serialize<T extends string>(set: Iterable<T> | null): str
2182
2195
  return set ? Array.from(set).join(',') : '';
2183
2196
  }
2184
2197
 
2198
+ export async function db_exists(db: SQL, table_name: string, value: string|number, column_name = 'id'): Promise<boolean> {
2199
+ const rows = await db`SELECT 1 FROM ${db(table_name)} WHERE ${db(column_name)} = ${value} LIMIT 1`;
2200
+ return rows.length > 0;
2201
+ }
2202
+
2185
2203
  export async function db_get_schema_revision(db: SQL): Promise<number|null> {
2186
2204
  try {
2187
2205
  const [result] = await db`SELECT MAX(revision_number) as latest_revision FROM db_schema`;