@murumets-ee/entity 0.1.0 → 0.1.1
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/dist/admin/index.cjs +1 -0
- package/dist/admin/index.d.cts +510 -0
- package/dist/admin/index.js +1 -1
- package/dist/index.cjs +64 -0
- package/dist/index.d.cts +1027 -0
- package/dist/index.d.ts +177 -177
- package/dist/index.js +2 -2
- package/dist/query/index.cjs +6 -0
- package/dist/query/index.d.cts +417 -0
- package/dist/query/index.js +1 -1
- package/dist/refs/index.cjs +1 -0
- package/dist/refs/index.d.cts +263 -0
- package/package.json +10 -6
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,49 @@
|
|
|
1
|
+
import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
|
1
2
|
import * as drizzle_orm from 'drizzle-orm';
|
|
2
3
|
import { SQL } from 'drizzle-orm';
|
|
3
4
|
import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
|
|
4
5
|
import { PgTableWithColumns } from 'drizzle-orm/pg-core';
|
|
5
|
-
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Optional admin UI configuration for entities.
|
|
9
|
+
* Controls how entities appear in the admin sidebar, list pages, and forms.
|
|
10
|
+
*/
|
|
11
|
+
interface EntityAdminConfig {
|
|
12
|
+
/** Sidebar section: 'content' | 'structure' | custom string. Default: 'content' */
|
|
13
|
+
group?: string;
|
|
14
|
+
/** Plural display name for sidebar + list pages. Default: title-cased pluralized entity name */
|
|
15
|
+
label?: string;
|
|
16
|
+
/** Singular label for "New X" button. Default: title-cased entity name */
|
|
17
|
+
labelSingular?: string;
|
|
18
|
+
/** Lucide icon name as string, e.g. 'file-text' */
|
|
19
|
+
icon?: string;
|
|
20
|
+
/** Description shown on list page */
|
|
21
|
+
description?: string;
|
|
22
|
+
/** Form layout. Default: 'single' */
|
|
23
|
+
layout?: 'single' | 'two-column';
|
|
24
|
+
/** Fields to hide in the form */
|
|
25
|
+
hiddenFields?: string[];
|
|
26
|
+
/** Columns to hide in the list */
|
|
27
|
+
hiddenColumns?: string[];
|
|
28
|
+
/** Per-field label/description/placeholder overrides */
|
|
29
|
+
fieldOverrides?: Record<string, {
|
|
30
|
+
label?: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
placeholder?: string;
|
|
33
|
+
}>;
|
|
34
|
+
/** Default list sort field. Default: 'createdAt' */
|
|
35
|
+
defaultSort?: string;
|
|
36
|
+
/** Default list sort direction. Default: 'desc' */
|
|
37
|
+
defaultSortDirection?: 'asc' | 'desc';
|
|
38
|
+
/** List page size. Default: 20 */
|
|
39
|
+
pageSize?: number;
|
|
40
|
+
/** For block editor: fields to show in Puck root config */
|
|
41
|
+
rootFields?: string[];
|
|
42
|
+
/** Suppress "New" button in the list page */
|
|
43
|
+
disableCreate?: boolean;
|
|
44
|
+
/** Order within sidebar group. Default: 0 */
|
|
45
|
+
sortOrder?: number;
|
|
46
|
+
}
|
|
6
47
|
|
|
7
48
|
/**
|
|
8
49
|
* Field type definitions
|
|
@@ -401,45 +442,143 @@ declare const behavior: {
|
|
|
401
442
|
};
|
|
402
443
|
|
|
403
444
|
/**
|
|
404
|
-
*
|
|
405
|
-
*
|
|
445
|
+
* In-memory TTL cache for COUNT(*) query results.
|
|
446
|
+
*
|
|
447
|
+
* Reduces database load on paginated list pages where the total count
|
|
448
|
+
* is recalculated on every pagination/sort/search interaction. The cache
|
|
449
|
+
* is per-process (not shared across workers) with a short TTL (default 5s)
|
|
450
|
+
* so counts are at most a few seconds stale.
|
|
451
|
+
*
|
|
452
|
+
* Cache keys include the entity name + serialized WHERE clause, so filtered
|
|
453
|
+
* and unfiltered counts are cached independently.
|
|
406
454
|
*/
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
455
|
+
/**
|
|
456
|
+
* Interface for count cache implementations.
|
|
457
|
+
* Used in client configs to avoid TypeScript private-field structural incompatibility
|
|
458
|
+
* across separate .d.ts files.
|
|
459
|
+
*/
|
|
460
|
+
interface CountCacheLike {
|
|
461
|
+
get(key: string): number | undefined;
|
|
462
|
+
set(key: string, count: number): void;
|
|
463
|
+
invalidate(prefix: string): void;
|
|
464
|
+
}
|
|
465
|
+
declare class CountCache implements CountCacheLike {
|
|
466
|
+
private cache;
|
|
467
|
+
private ttlMs;
|
|
468
|
+
constructor(ttlMs?: number);
|
|
469
|
+
/**
|
|
470
|
+
* Get a cached count. Returns undefined on miss or expired entry.
|
|
471
|
+
*/
|
|
472
|
+
get(key: string): number | undefined;
|
|
473
|
+
/**
|
|
474
|
+
* Cache a count result with the configured TTL.
|
|
475
|
+
*/
|
|
476
|
+
set(key: string, count: number): void;
|
|
477
|
+
/**
|
|
478
|
+
* Invalidate all cache entries whose key starts with the given prefix.
|
|
479
|
+
* Typically called with the entity name after mutations (create/update/delete).
|
|
480
|
+
*/
|
|
481
|
+
invalidate(prefix: string): void;
|
|
482
|
+
/**
|
|
483
|
+
* Remove all expired entries. Useful for periodic cleanup in long-running processes.
|
|
484
|
+
*/
|
|
485
|
+
prune(): void;
|
|
486
|
+
/**
|
|
487
|
+
* Clear the entire cache.
|
|
488
|
+
*/
|
|
489
|
+
clear(): void;
|
|
490
|
+
/**
|
|
491
|
+
* Number of entries currently in the cache (including expired ones not yet pruned).
|
|
492
|
+
*/
|
|
493
|
+
get size(): number;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Postgres row count estimation using pg_class statistics.
|
|
498
|
+
*
|
|
499
|
+
* For unfiltered counts on large tables, querying `pg_class.reltuples`
|
|
500
|
+
* is effectively instant (no table scan) and returns a good approximation
|
|
501
|
+
* that is updated by VACUUM and ANALYZE. This is suitable for pagination
|
|
502
|
+
* totals where exact precision is not critical.
|
|
503
|
+
*
|
|
504
|
+
* SECURITY: The table name is NOT interpolated into SQL — it is passed as a
|
|
505
|
+
* parameterized value to the `relname = $1` comparison. This prevents SQL injection.
|
|
506
|
+
*/
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Estimate the total row count for a table using Postgres statistics.
|
|
510
|
+
*
|
|
511
|
+
* Returns the approximate row count from `pg_class.reltuples`, which is
|
|
512
|
+
* updated by VACUUM/ANALYZE. Returns 0 if the table is not found or
|
|
513
|
+
* statistics are not yet available (e.g., freshly created table).
|
|
514
|
+
*
|
|
515
|
+
* @param db - Drizzle Postgres database instance
|
|
516
|
+
* @param tableName - The Postgres table name (without schema prefix)
|
|
517
|
+
* @returns Estimated row count (non-negative integer)
|
|
518
|
+
*/
|
|
519
|
+
declare function estimateRowCount(db: PostgresJsDatabase, tableName: string): Promise<number>;
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Cursor-based (keyset) pagination utilities.
|
|
523
|
+
*
|
|
524
|
+
* Cursor pagination avoids the performance cliff of OFFSET at scale (1M+ rows).
|
|
525
|
+
* Instead of `OFFSET N`, it uses a WHERE condition:
|
|
526
|
+
* `WHERE (sortField < lastValue) OR (sortField = lastValue AND id < lastId)`
|
|
527
|
+
* which Postgres can serve from an index in constant time.
|
|
528
|
+
*
|
|
529
|
+
* The cursor is opaque to the client — base64-encoded JSON.
|
|
530
|
+
*
|
|
531
|
+
* Security:
|
|
532
|
+
* - `field` must be whitelisted against the entity's actual fields
|
|
533
|
+
* - `id` must be a valid UUID
|
|
534
|
+
* - `value` is parameterized (never interpolated into SQL)
|
|
535
|
+
* - Malformed cursors return null (caller returns 400)
|
|
536
|
+
*/
|
|
537
|
+
|
|
538
|
+
/** Cursor input for keyset pagination. */
|
|
539
|
+
interface CursorInput {
|
|
540
|
+
/** Sort field name (e.g. 'createdAt'). Must be a real column on the entity. */
|
|
541
|
+
field: string;
|
|
542
|
+
/** Last seen value of the sort field. */
|
|
543
|
+
value: string | number;
|
|
544
|
+
/** Sort direction — must match the ORDER BY direction. */
|
|
545
|
+
direction: 'asc' | 'desc';
|
|
546
|
+
/** Tie-breaker: last seen entity ID. Required for non-unique sort fields. */
|
|
547
|
+
id?: string;
|
|
548
|
+
}
|
|
549
|
+
/** Decoded cursor (internal, after validation). */
|
|
550
|
+
interface DecodedCursor {
|
|
551
|
+
field: string;
|
|
552
|
+
value: string | number;
|
|
553
|
+
direction: 'asc' | 'desc';
|
|
554
|
+
id?: string;
|
|
442
555
|
}
|
|
556
|
+
/**
|
|
557
|
+
* Encode a cursor for API transport (base64url).
|
|
558
|
+
* Built from the last item in a result set.
|
|
559
|
+
*/
|
|
560
|
+
declare function encodeCursor(item: Record<string, unknown>, sortField: string, direction: 'asc' | 'desc'): string;
|
|
561
|
+
/**
|
|
562
|
+
* Decode and validate a cursor string from query params.
|
|
563
|
+
* Returns null if the cursor is malformed, tampered, or invalid.
|
|
564
|
+
*
|
|
565
|
+
* Security: the `field` value is NOT validated here — the caller must
|
|
566
|
+
* whitelist it against the entity's actual columns.
|
|
567
|
+
*/
|
|
568
|
+
declare function decodeCursor(encoded: string): DecodedCursor | null;
|
|
569
|
+
/**
|
|
570
|
+
* Build the keyset WHERE condition from a decoded cursor.
|
|
571
|
+
*
|
|
572
|
+
* For DESC: `WHERE (field < value) OR (field = value AND id < cursorId)`
|
|
573
|
+
* For ASC: `WHERE (field > value) OR (field = value AND id > cursorId)`
|
|
574
|
+
*
|
|
575
|
+
* The caller must verify that `cursor.field` exists on the table before calling.
|
|
576
|
+
*
|
|
577
|
+
* @param table - Drizzle table with columns
|
|
578
|
+
* @param cursor - Decoded and validated cursor
|
|
579
|
+
* @returns SQL condition, or null if the field doesn't exist on the table
|
|
580
|
+
*/
|
|
581
|
+
declare function buildCursorCondition(table: PgTableWithColumns<any>, cursor: DecodedCursor): SQL | null;
|
|
443
582
|
|
|
444
583
|
/**
|
|
445
584
|
* Entity definition API
|
|
@@ -531,145 +670,6 @@ declare function defineEntity<F extends Record<string, FieldConfig>, const B ext
|
|
|
531
670
|
id: IdField;
|
|
532
671
|
} & ExtractBehaviorFields<B> & F>;
|
|
533
672
|
|
|
534
|
-
/**
|
|
535
|
-
* Cursor-based (keyset) pagination utilities.
|
|
536
|
-
*
|
|
537
|
-
* Cursor pagination avoids the performance cliff of OFFSET at scale (1M+ rows).
|
|
538
|
-
* Instead of `OFFSET N`, it uses a WHERE condition:
|
|
539
|
-
* `WHERE (sortField < lastValue) OR (sortField = lastValue AND id < lastId)`
|
|
540
|
-
* which Postgres can serve from an index in constant time.
|
|
541
|
-
*
|
|
542
|
-
* The cursor is opaque to the client — base64-encoded JSON.
|
|
543
|
-
*
|
|
544
|
-
* Security:
|
|
545
|
-
* - `field` must be whitelisted against the entity's actual fields
|
|
546
|
-
* - `id` must be a valid UUID
|
|
547
|
-
* - `value` is parameterized (never interpolated into SQL)
|
|
548
|
-
* - Malformed cursors return null (caller returns 400)
|
|
549
|
-
*/
|
|
550
|
-
|
|
551
|
-
/** Cursor input for keyset pagination. */
|
|
552
|
-
interface CursorInput {
|
|
553
|
-
/** Sort field name (e.g. 'createdAt'). Must be a real column on the entity. */
|
|
554
|
-
field: string;
|
|
555
|
-
/** Last seen value of the sort field. */
|
|
556
|
-
value: string | number;
|
|
557
|
-
/** Sort direction — must match the ORDER BY direction. */
|
|
558
|
-
direction: 'asc' | 'desc';
|
|
559
|
-
/** Tie-breaker: last seen entity ID. Required for non-unique sort fields. */
|
|
560
|
-
id?: string;
|
|
561
|
-
}
|
|
562
|
-
/** Decoded cursor (internal, after validation). */
|
|
563
|
-
interface DecodedCursor {
|
|
564
|
-
field: string;
|
|
565
|
-
value: string | number;
|
|
566
|
-
direction: 'asc' | 'desc';
|
|
567
|
-
id?: string;
|
|
568
|
-
}
|
|
569
|
-
/**
|
|
570
|
-
* Encode a cursor for API transport (base64url).
|
|
571
|
-
* Built from the last item in a result set.
|
|
572
|
-
*/
|
|
573
|
-
declare function encodeCursor(item: Record<string, unknown>, sortField: string, direction: 'asc' | 'desc'): string;
|
|
574
|
-
/**
|
|
575
|
-
* Decode and validate a cursor string from query params.
|
|
576
|
-
* Returns null if the cursor is malformed, tampered, or invalid.
|
|
577
|
-
*
|
|
578
|
-
* Security: the `field` value is NOT validated here — the caller must
|
|
579
|
-
* whitelist it against the entity's actual columns.
|
|
580
|
-
*/
|
|
581
|
-
declare function decodeCursor(encoded: string): DecodedCursor | null;
|
|
582
|
-
/**
|
|
583
|
-
* Build the keyset WHERE condition from a decoded cursor.
|
|
584
|
-
*
|
|
585
|
-
* For DESC: `WHERE (field < value) OR (field = value AND id < cursorId)`
|
|
586
|
-
* For ASC: `WHERE (field > value) OR (field = value AND id > cursorId)`
|
|
587
|
-
*
|
|
588
|
-
* The caller must verify that `cursor.field` exists on the table before calling.
|
|
589
|
-
*
|
|
590
|
-
* @param table - Drizzle table with columns
|
|
591
|
-
* @param cursor - Decoded and validated cursor
|
|
592
|
-
* @returns SQL condition, or null if the field doesn't exist on the table
|
|
593
|
-
*/
|
|
594
|
-
declare function buildCursorCondition(table: PgTableWithColumns<any>, cursor: DecodedCursor): SQL | null;
|
|
595
|
-
|
|
596
|
-
/**
|
|
597
|
-
* In-memory TTL cache for COUNT(*) query results.
|
|
598
|
-
*
|
|
599
|
-
* Reduces database load on paginated list pages where the total count
|
|
600
|
-
* is recalculated on every pagination/sort/search interaction. The cache
|
|
601
|
-
* is per-process (not shared across workers) with a short TTL (default 5s)
|
|
602
|
-
* so counts are at most a few seconds stale.
|
|
603
|
-
*
|
|
604
|
-
* Cache keys include the entity name + serialized WHERE clause, so filtered
|
|
605
|
-
* and unfiltered counts are cached independently.
|
|
606
|
-
*/
|
|
607
|
-
/**
|
|
608
|
-
* Interface for count cache implementations.
|
|
609
|
-
* Used in client configs to avoid TypeScript private-field structural incompatibility
|
|
610
|
-
* across separate .d.ts files.
|
|
611
|
-
*/
|
|
612
|
-
interface CountCacheLike {
|
|
613
|
-
get(key: string): number | undefined;
|
|
614
|
-
set(key: string, count: number): void;
|
|
615
|
-
invalidate(prefix: string): void;
|
|
616
|
-
}
|
|
617
|
-
declare class CountCache implements CountCacheLike {
|
|
618
|
-
private cache;
|
|
619
|
-
private ttlMs;
|
|
620
|
-
constructor(ttlMs?: number);
|
|
621
|
-
/**
|
|
622
|
-
* Get a cached count. Returns undefined on miss or expired entry.
|
|
623
|
-
*/
|
|
624
|
-
get(key: string): number | undefined;
|
|
625
|
-
/**
|
|
626
|
-
* Cache a count result with the configured TTL.
|
|
627
|
-
*/
|
|
628
|
-
set(key: string, count: number): void;
|
|
629
|
-
/**
|
|
630
|
-
* Invalidate all cache entries whose key starts with the given prefix.
|
|
631
|
-
* Typically called with the entity name after mutations (create/update/delete).
|
|
632
|
-
*/
|
|
633
|
-
invalidate(prefix: string): void;
|
|
634
|
-
/**
|
|
635
|
-
* Remove all expired entries. Useful for periodic cleanup in long-running processes.
|
|
636
|
-
*/
|
|
637
|
-
prune(): void;
|
|
638
|
-
/**
|
|
639
|
-
* Clear the entire cache.
|
|
640
|
-
*/
|
|
641
|
-
clear(): void;
|
|
642
|
-
/**
|
|
643
|
-
* Number of entries currently in the cache (including expired ones not yet pruned).
|
|
644
|
-
*/
|
|
645
|
-
get size(): number;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* Postgres row count estimation using pg_class statistics.
|
|
650
|
-
*
|
|
651
|
-
* For unfiltered counts on large tables, querying `pg_class.reltuples`
|
|
652
|
-
* is effectively instant (no table scan) and returns a good approximation
|
|
653
|
-
* that is updated by VACUUM and ANALYZE. This is suitable for pagination
|
|
654
|
-
* totals where exact precision is not critical.
|
|
655
|
-
*
|
|
656
|
-
* SECURITY: The table name is NOT interpolated into SQL — it is passed as a
|
|
657
|
-
* parameterized value to the `relname = $1` comparison. This prevents SQL injection.
|
|
658
|
-
*/
|
|
659
|
-
|
|
660
|
-
/**
|
|
661
|
-
* Estimate the total row count for a table using Postgres statistics.
|
|
662
|
-
*
|
|
663
|
-
* Returns the approximate row count from `pg_class.reltuples`, which is
|
|
664
|
-
* updated by VACUUM/ANALYZE. Returns 0 if the table is not found or
|
|
665
|
-
* statistics are not yet available (e.g., freshly created table).
|
|
666
|
-
*
|
|
667
|
-
* @param db - Drizzle Postgres database instance
|
|
668
|
-
* @param tableName - The Postgres table name (without schema prefix)
|
|
669
|
-
* @returns Estimated row count (non-negative integer)
|
|
670
|
-
*/
|
|
671
|
-
declare function estimateRowCount(db: PostgresJsDatabase, tableName: string): Promise<number>;
|
|
672
|
-
|
|
673
673
|
/**
|
|
674
674
|
* Generic entity usage lookup — "where is this entity referenced?"
|
|
675
675
|
*
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {lt,gt,or,and,eq
|
|
1
|
+
import {sql,lt,gt,or,and,eq}from'drizzle-orm';import {uuid,pgTable,index,varchar,unique,jsonb,integer,text,timestamp,boolean,doublePrecision}from'drizzle-orm/pg-core';var s={id:t=>({type:"id",required:true,indexed:true,...t}),text:t=>({type:"text",...t}),number:t=>({type:"number",...t}),boolean:t=>({type:"boolean",default:false,...t}),date:t=>({type:"date",...t}),select:t=>({type:"select",...t}),reference:t=>({type:"reference",cardinality:"one",onDelete:"set-null",...t}),media:t=>({type:"media",...t}),richtext:t=>({type:"richtext",...t}),slug:t=>({type:"slug",unique:true,indexed:true,...t}),json:t=>({type:"json",...t}),blocks:t=>({type:"blocks",...t})};async function R(){try{return (await import(["@murumets-ee","core"].join("/"))).getCurrentUser()?.id}catch{return}}function y(){return {name:"auditable",fields:{createdBy:s.text(),updatedBy:s.text(),createdAt:s.date({indexed:true}),updatedAt:s.date({indexed:true})},hooks:{beforeCreate:async t=>{t.createdAt=new Date,t.updatedAt=new Date;let e=await R();return e&&(t.createdBy=e,t.updatedBy=e),t},beforeUpdate:async(t,e)=>{e.updatedAt=new Date;let r=await R();return r&&(e.updatedBy=r),e}}}}function h(t){return {name:"hierarchical",fields:{parentId:s.reference({entity:"_self",required:false}),path:s.text({indexed:true,maxLength:2048}),depth:s.number({integer:true,default:0,indexed:true})},hooks:{beforeCreate:async e=>(e.parentId||(e.depth=0),e)}}}function g(){return {name:"publishable",fields:{status:s.select({options:["draft","published"],default:"draft",indexed:true}),publishedAt:s.date({indexed:true})},hooks:{beforeUpdate:async(t,e)=>(e.status==="published"&&!e.publishedAt&&(e.publishedAt=new Date),e)}}}function x(){return {name:"revisionable",fields:{_version:s.number({required:true,default:1,integer:true})},hooks:{beforeCreate:async t=>(t._version=1,t),beforeUpdate:async(t,e)=>(e._version=(Number(e._version)||0)+1,e)}}}function f(t){return t.toLowerCase().trim().replace(/[^\w\s-]/g,"").replace(/[\s_-]+/g,"-").replace(/^-+|-+$/g,"")}function F(t,e){return {name:"sluggable",fields:{slug:s.slug({from:t,...e?.translatable?{translatable:true}:{}})},hooks:{beforeCreate:async r=>(!r.slug&&r[t]&&(r.slug=f(String(r[t]))),r),beforeUpdate:async(r,o)=>(o[t]&&!o.slug&&(o.slug=f(String(o[t]))),o)}}}function v(){return {name:"timestamped",fields:{createdAt:s.date({indexed:true}),updatedAt:s.date({indexed:true})},hooks:{beforeCreate:async t=>(t.createdAt=new Date,t.updatedAt=new Date,t),beforeUpdate:async(t,e)=>(e.updatedAt=new Date,e)}}}var D={publishable:g,auditable:y,sluggable:F,revisionable:x,hierarchical:h,timestamped:v};var _=class{cache=new Map;ttlMs;constructor(e=5e3){this.ttlMs=e;}get(e){let r=this.cache.get(e);if(!r||Date.now()>r.expiresAt){r&&this.cache.delete(e);return}return r.count}set(e,r){this.cache.set(e,{count:r,expiresAt:Date.now()+this.ttlMs});}invalidate(e){for(let r of this.cache.keys())r.startsWith(e)&&this.cache.delete(r);}prune(){let e=Date.now();for(let[r,o]of this.cache.entries())e>o.expiresAt&&this.cache.delete(r);}clear(){this.cache.clear();}get size(){return this.cache.size}};async function P(t,e){let r=await t.execute(sql`SELECT reltuples::bigint AS estimate FROM pg_class WHERE relname = ${e}`),n=(Array.isArray(r)?r:r.rows??[])[0],i=Number(n?.estimate??0);return i>0?i:0}var M=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function J(t,e,r){let o={field:e,value:t[e],direction:r,id:t.id};return btoa(JSON.stringify(o))}function V(t){try{let e=atob(t),r=JSON.parse(e);if(typeof r!="object"||r===null)return null;let o=r;return typeof o.field!="string"||!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(o.field)||typeof o.value!="string"&&typeof o.value!="number"||o.direction!=="asc"&&o.direction!=="desc"||o.id!==void 0&&(typeof o.id!="string"||!M.test(o.id))?null:{field:o.field,value:o.value,direction:o.direction,id:o.id}}catch{return null}}function W(t,e){let r=t[e.field];if(!r)return null;let n=e.direction==="desc"?lt:gt,i=n(r,e.value);if(!e.id)return i;let a=t.id;if(!a)return i;let l=n(a,e.id);return or(i,and(eq(r,e.value),l))}function Q(t){let e={},r={};for(let n of t.behaviors||[]){if(n.fields)for(let[i,a]of Object.entries(n.fields)){if(e[i]){console.warn(`Field '${i}' from behavior '${n.name}' conflicts with existing field. Skipping.`);continue}e[i]=a;}if(n.hooks)for(let[i,a]of Object.entries(n.hooks)){let l=i;if(a){let c=r[l];if(c){let S=c,N=a,A=async(...m)=>{let E=await S(...m);if(E!==void 0){let b=[...m];return b[b.length-1]=E,await N(...b)}return await N(...m)};r[l]=A;}else r[l]=a;}}}let o={id:s.id(),...e,...t.fields};for(let[,n]of Object.entries(o))n.type==="slug"&&!n.translatable&&o[n.from]?.translatable&&(n.translatable=true);return {...t,allFields:o,hooks:r}}var C=class extends Error{entityName;entityId;usages;constructor(e,r,o){let n=o.length;super(`Cannot delete ${e} '${r}': referenced by ${n} other entit${n===1?"y":"ies"}`),this.name="ReferencedEntityError",this.entityName=e,this.entityId=r,this.usages=o;}};function w(t,e,r){let o=r?.nullable??false;switch(e.type){case "id":return uuid(t).primaryKey().defaultRandom();case "text":if(e.maxLength&&e.maxLength<=255){let n=varchar(t,{length:e.maxLength});return e.unique&&(n=n.unique()),!o&&e.required&&(n=n.notNull()),!o&&e.default!==void 0&&(n=n.default(e.default)),n}else {let n=text(t);return e.unique&&(n=n.unique()),!o&&e.required&&(n=n.notNull()),!o&&e.default!==void 0&&(n=n.default(e.default)),n}case "number":{let n=e.integer?integer(t):doublePrecision(t);return !o&&e.required&&(n=n.notNull()),!o&&e.default!==void 0&&(n=n.default(e.default)),n}case "boolean":{let n=boolean(t);if(!o&&e.required&&(n=n.notNull()),!o){let i=e.default!==void 0?e.default:false;n=n.default(i);}return n}case "date":{let n=timestamp(t,{withTimezone:true});return !o&&e.required&&(n=n.notNull()),!o&&e.default!==void 0&&(n=n.default(e.default)),n}case "select":{let n=varchar(t,{length:100});return !o&&e.required&&(n=n.notNull()),!o&&e.default!==void 0&&(n=n.default(e.default)),n}case "reference":{if(e.cardinality==="many")return uuid(t).array();let n=uuid(t);return !o&&e.required&&(n=n.notNull()),n}case "media":{let n=uuid(t);return !o&&e.required&&(n=n.notNull()),n}case "slug":{let n=varchar(t,{length:255});return e.unique&&!o&&(n=n.unique()),!o&&e.required&&(n=n.notNull()),n}case "richtext":{let n=text(t);return !o&&e.required&&(n=n.notNull()),n}case "json":return jsonb(t);default:return text(t)}}function q(t,e,r){let o=r?.nullable??false;switch(e.type){case "id":return "id: uuid('id').primaryKey().defaultRandom()";case "text":{let n=e.maxLength||255,i=n<=255?`varchar('${t}', { length: ${n} })`:`text('${t}')`;return e.unique&&(i+=".unique()"),!o&&e.required&&(i+=".notNull()"),!o&&e.default&&(i+=`.default('${e.default}')`),`${t}: ${i}`}case "number":{let n=e.integer?`integer('${t}')`:`doublePrecision('${t}')`;return !o&&e.required&&(n+=".notNull()"),!o&&e.default!==void 0&&(n+=`.default(${e.default})`),`${t}: ${n}`}case "boolean":{if(o)return `${t}: boolean('${t}')`;let n=e.default??false;return `${t}: boolean('${t}').default(${n})`}case "date":{let n=`timestamp('${t}', { withTimezone: true })`;return !o&&e.required&&(n+=".notNull()"),`${t}: ${n}`}case "reference":{if(e.cardinality==="many")return `${t}: uuid('${t}').array()`;let n=`uuid('${t}')`;return !o&&e.required&&(n+=".notNull()"),`${t}: ${n}`}case "media":{let n=`uuid('${t}')`;return !o&&e.required&&(n+=".notNull()"),`${t}: ${n}`}case "slug":{let n=`varchar('${t}', { length: 255 })`;return e.unique&&!o&&(n+=".unique()"),!o&&e.required&&(n+=".notNull()"),`${t}: ${n}`}case "select":{let n=`varchar('${t}', { length: 100 })`;return !o&&e.required&&(n+=".notNull()"),!o&&e.default&&(n+=`.default('${e.default}')`),`${t}: ${n}`}case "richtext":{let n=`text('${t}')`;return !o&&e.required&&(n+=".notNull()"),`${t}: ${n}`}case "json":return `${t}: jsonb('${t}')`;default:return `${t}: text('${t}')`}}function Y(t){let e=t.name,r={};for(let[i,a]of Object.entries(t.allFields))a.type!=="blocks"&&(r[i]=w(i,a));(t.scope==="team"||t.scope==="user")&&(r._scopeId=uuid("_scope_id"));let o=Object.entries(t.allFields).filter(([i,a])=>a.type!=="blocks"&&a.type!=="id"&&a.indexed&&!a.unique),n=t.behaviors?.some(i=>i.name==="publishable")??false;return o.length===0&&!n?pgTable(e,r):pgTable(e,r,i=>{let a={};for(let[l]of o)a[`idx_${e}_${l}`]=index(`idx_${e}_${l}`).on(i[l]);return n&&i.status&&i.createdAt&&(a[`idx_${e}_status_created`]=index(`idx_${e}_status_created`).on(i.status,i.createdAt)),a})}function ee(t){let e=t.name,r=[],o=Object.entries(t.allFields).filter(([a,l])=>l.type!=="blocks"&&l.type!=="id"&&l.indexed&&!l.unique),n=t.behaviors?.some(a=>a.name==="publishable")??false,i=o.length>0||n;r.push(`export const ${e} = pgTable('${e}', {`);for(let[a,l]of Object.entries(t.allFields)){if(l.type==="blocks")continue;let c=q(a,l);r.push(` ${c},`);}if((t.scope==="team"||t.scope==="user")&&r.push(" _scopeId: uuid('_scope_id'),"),i){r.push("}, (table) => ({");for(let[a]of o)r.push(` idx_${e}_${a}: index('idx_${e}_${a}').on(table.${a}),`);n&&r.push(` idx_${e}_status_created: index('idx_${e}_status_created').on(table.status, table.createdAt),`),r.push("}))");}else r.push(")");return r.join(`
|
|
2
2
|
`)}function te(t){let e=Object.entries(t.allFields).filter(([i,a])=>a.translatable);if(e.length===0)return null;let r=`${t.name}_translations`,o={id:uuid("id").primaryKey().defaultRandom(),entityId:uuid("entity_id").notNull(),locale:varchar("locale",{length:10}).notNull()},n=e.some(([i,a])=>a.type==="slug");for(let[i,a]of e)o[i]=w(i,a,{nullable:true});return pgTable(r,o,i=>({uniqueEntityLocale:unique().on(i.entityId,i.locale),...n&&i.slug?{uniqueSlugLocale:unique().on(i.slug,i.locale)}:{}}))}function ne(t){let e=Object.entries(t.allFields).filter(([a,l])=>l.translatable);if(e.length===0)return null;let r=`${t.name}_translations`,o=t.name,n=[];n.push(`export const ${r} = pgTable('${r}', {`),n.push(" id: uuid('id').primaryKey().defaultRandom(),"),n.push(` entityId: uuid('entity_id').notNull().references(() => ${o}.id, { onDelete: 'cascade' }),`),n.push(" locale: varchar('locale', { length: 10 }).notNull(),");for(let[a,l]of e){let c=q(a,l,{nullable:true});n.push(` ${c},`);}let i=e.some(([a,l])=>l.type==="slug");return n.push("}, (table) => ({"),n.push(" uniqueEntityLocale: unique().on(table.entityId, table.locale), // One translation per locale"),i&&n.push(" uniqueSlugLocale: unique().on(table.slug, table.locale), // Per-locale slug uniqueness"),n.push("}))"),n.join(`
|
|
3
3
|
`)}function I(t){return Object.values(t.allFields).some(e=>e.type==="blocks")}function re(t){if(!I(t))return null;let e=`${t.name}_layout`;return pgTable(e,{id:uuid("id").primaryKey().defaultRandom(),entityId:uuid("entity_id").notNull(),fieldName:varchar("field_name",{length:100}).notNull(),blockType:varchar("block_type",{length:100}).notNull(),sortOrder:integer("sort_order").notNull().default(0),data:jsonb("data"),locale:varchar("locale",{length:10})},r=>({idx_entity_locale_sort:index(`idx_${e}_entity_locale_sort`).on(r.entityId,r.locale,r.sortOrder)}))}function oe(t){return t.behaviors?.some(e=>e.name==="versionable")??false}function ie(t){if(!(t.behaviors?.some(o=>o.name==="publishable")??false))return false;let r=t.allFields;return Object.values(r).some(o=>o.translatable)}function ae(t){let e=t.name,r=`${e}_locale_status`;return `export const ${r} = pgTable('${r}', {
|
|
4
4
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
@@ -61,4 +61,4 @@ import {lt,gt,or,and,eq,sql}from'drizzle-orm';import {uuid,pgTable,index,varchar
|
|
|
61
61
|
isAutosave: boolean('is_autosave').notNull().default(false),
|
|
62
62
|
}, (table) => ({
|
|
63
63
|
uniqueEntityVersionLocale: unique().on(table.entityId, table.version, table.locale),
|
|
64
|
-
}))`}function me(t){let e=Object.values(t.allFields).filter(n=>n.type==="blocks"&&!("localized"in n&&n.localized));if(e.length===0||!e.some(n=>n.type!=="blocks"?false:n.blocks.some(i=>Object.values(i.fields).some(a=>a.translatable))))return null;let o=`${t.name}_layout_translations`;return pgTable(o,{id:uuid("id").primaryKey().defaultRandom(),layoutId:uuid("layout_id").notNull(),locale:varchar("locale",{length:10}).notNull(),fields:jsonb("fields").notNull()},n=>({uniqueLayoutLocale:unique().on(n.layoutId,n.locale)}))}export{_ as CountCache,C as ReferencedEntityError,y as auditable,D as behavior,
|
|
64
|
+
}))`}function me(t){let e=Object.values(t.allFields).filter(n=>n.type==="blocks"&&!("localized"in n&&n.localized));if(e.length===0||!e.some(n=>n.type!=="blocks"?false:n.blocks.some(i=>Object.values(i.fields).some(a=>a.translatable))))return null;let o=`${t.name}_layout_translations`;return pgTable(o,{id:uuid("id").primaryKey().defaultRandom(),layoutId:uuid("layout_id").notNull(),locale:varchar("locale",{length:10}).notNull(),fields:jsonb("fields").notNull()},n=>({uniqueLayoutLocale:unique().on(n.layoutId,n.locale)}))}export{_ as CountCache,C as ReferencedEntityError,y as auditable,D as behavior,W as buildCursorCondition,V as decodeCursor,Q as defineEntity,J as encodeCursor,P as estimateRowCount,s as field,ue as generateContentLocksCode,se as generateDraftsCode,ce as generateLayoutCode,re as generateLayoutSchema,pe as generateLayoutTranslationCode,me as generateLayoutTranslationSchema,ae as generateLocaleStatusCode,Y as generateSchema,ee as generateSchemaCode,te as generateTranslationSchema,ne as generateTranslationSchemaCode,fe as generateVersionsCode,I as hasBlocksFields,de as hasTranslatableBlocks,h as hierarchical,le as isPublishable,oe as isVersionable,ie as needsLocaleStatus,g as publishable,x as revisionable,F as sluggable,f as slugify,v as timestamped};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";var I=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var B=Object.prototype.hasOwnProperty;var L=(a,e)=>{for(var t in e)I(a,t,{get:e[t],enumerable:!0})},$=(a,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of A(e))!B.call(a,s)&&s!==t&&I(a,s,{get:()=>e[s],enumerable:!(i=v(e,s))||i.enumerable});return a};var P=a=>$(I({},"__esModule",{value:!0}),a);var S={};L(S,{QueryClient:()=>C});module.exports=P(S);var w=require("@murumets-ee/db"),l=require("drizzle-orm");var m=require("drizzle-orm");function O(a,e){let t=a[e.field];if(!t)return null;let s=e.direction==="desc"?m.lt:m.gt,n=s(t,e.value);if(!e.id)return n;let r=a.id;if(!r)return n;let g=s(r,e.id);return(0,m.or)(n,(0,m.and)((0,m.eq)(t,e.value),g))}function D(a,e,t){if(!e)return null;let i={},s=t?.select||Object.keys(a.allFields);for(let n of s){if(!t?.includeInternal&&n.startsWith("_"))continue;let r=a.allFields[n];r&&r.type!=="blocks"&&(i[n]=e[n])}return i}function T(a,e,t){return e.map(i=>D(a,i,t))}var C=class{entity;db;logger;table;isPublishable;countCache;constructor(e){this.entity=e.entity,this.db=e.db,this.logger=e.logger,this.countCache=e.countCache,this.isPublishable=e.entity.behaviors?.some(i=>i.name==="publishable")??!1;let t=w.schemaRegistry.get(e.entity.name);if(!t)throw new Error(`Schema for entity '${e.entity.name}' not found in registry. Ensure schemas are generated and registered before creating QueryClient.`);this.table=t}async findById(e,t){this.logger?.info({entity:this.entity.name,id:e,locale:t?.locale},"Query: Finding entity by ID");let i=[(0,l.eq)(this.table.id,e)];this.isPublishable&&i.push(this.buildPublishFilter(t?.locale));let[s]=await this.db.select().from(this.table).where((0,l.and)(...i));if(!s)return null;let n=D(this.entity,s,{select:t?.select});if(!n)return null;if(this.getBlocksFields().length>0){let r=await this.loadBlocks([n.id],t?.locale,{defaultLocale:t?.defaultLocale});this.attachBlocks([n],r)}return t?.locale?(await this.mergeTranslations([n],t.locale))[0]:n}async findMany(e){this.logger?.info({entity:this.entity.name,options:e,locale:e?.locale},"Query: Finding entities");let t=this.db.select().from(this.table).$dynamic(),i=[];if(this.isPublishable&&i.push(this.buildPublishFilter(e?.locale)),e?.where&&i.push(e.where),e?.cursor){let r=this.entity.allFields;if(!(e.cursor.field in r)&&e.cursor.field!=="id")throw new Error(`Invalid cursor field: '${e.cursor.field}' is not a field on '${this.entity.name}'`);let g=O(this.table,e.cursor);g&&i.push(g)}if(i.length>0&&(t=t.where((0,l.and)(...i))),e?.limit&&(t=t.limit(e.limit)),e?.offset&&!e?.cursor&&(t=t.offset(e.offset)),e?.orderBy){let r=Array.isArray(e.orderBy)?e.orderBy:[e.orderBy];t=t.orderBy(...r)}let s=await t,n=T(this.entity,s,{select:e?.select}).filter(r=>r!==null);if(this.getBlocksFields().length>0&&n.length>0){let r=n.map(y=>y.id),g=await this.loadBlocks(r,e?.locale,{defaultLocale:e?.defaultLocale});this.attachBlocks(n,g)}return e?.locale?await this.mergeTranslations(n,e.locale):n}async count(e){this.logger?.info({entity:this.entity.name,options:e},"Query: Counting entities");let t=this.buildCountCacheKey(e?.where);if(this.countCache){let r=this.countCache.get(t);if(r!==void 0)return this.logger?.debug?.({entity:this.entity.name,cached:r},"Count cache hit"),r}let i=this.db.select({count:l.sql`count(*)`}).from(this.table).$dynamic();if(this.isPublishable){let r=this.buildPublishFilter();e?.where?i=i.where((0,l.and)(r,e.where)):i=i.where(r)}else e?.where&&(i=i.where(e.where));let[s]=await i,n=Number(s.count);return this.countCache&&this.countCache.set(t,n),n}buildPublishFilter(e){if(e){let t=w.schemaRegistry.get(`${this.entity.name}_locale_status`);if(t)return l.sql`COALESCE(
|
|
2
|
+
(SELECT ${t.status} FROM ${t}
|
|
3
|
+
WHERE ${t.entityId} = ${this.table.id}
|
|
4
|
+
AND ${t.locale} = ${e}),
|
|
5
|
+
${this.table.status}
|
|
6
|
+
) = 'published'`}return(0,l.eq)(this.table.status,"published")}async mergeTranslations(e,t){if(!e.length)return e;let i=`${this.entity.name}_translations`,s=w.schemaRegistry.get(i);if(!s)return e;let n=e.map(o=>o.id),r=await this.db.select().from(s).where((0,l.and)((0,l.inArray)(s.entityId,n),(0,l.eq)(s.locale,t))),g=Object.entries(this.entity.allFields).filter(([o,h])=>h.translatable).map(([o])=>o),y=new Map;for(let o of r){let h={};for(let c of g){let d=o[c];d!=null&&(h[c]=d)}y.set(o.entityId,h)}return e.map(o=>{let h=y.get(o.id);return h?{...o,...h}:o})}getBlocksFields(){return Object.entries(this.entity.allFields).filter(([e,t])=>t.type==="blocks").map(([e,t])=>({name:e,config:t}))}async loadBlocks(e,t,i){let s=this.getBlocksFields();if(s.length===0||e.length===0)return new Map;let n=w.schemaRegistry.get(`${this.entity.name}_layout`);if(!n)return new Map;let r=s.filter(({config:o})=>!("localized"in o&&o.localized)),g=s.filter(({config:o})=>"localized"in o&&o.localized),y=new Map;if(r.length>0){let o=await this.db.select().from(n).where((0,l.and)((0,l.inArray)(n.entityId,e),(0,l.isNull)(n.locale))).orderBy(n.sortOrder),h;if(t&&o.length>0){let c=w.schemaRegistry.get(`${this.entity.name}_layout_translations`);if(c){let d=o.map(f=>f.id),u=await this.db.select().from(c).where((0,l.and)((0,l.inArray)(c.layoutId,d),(0,l.eq)(c.locale,t)));h=new Map;for(let f of u)h.set(f.layoutId,f.fields??{})}}for(let c of o){let d=c.entityId,u=c.fieldName;y.has(d)||y.set(d,{});let f=y.get(d);f[u]||(f[u]=[]);let b=c.data??{},p=h?.get(c.id);f[u].push({_block:c.blockType,_id:c.id,...b,...p??{}})}}if(g.length>0){let o=!t||t===i?.defaultLocale,h=t?o?(0,l.or)((0,l.eq)(n.locale,t),(0,l.isNull)(n.locale)):(0,l.eq)(n.locale,t):(0,l.isNull)(n.locale),c=await this.db.select().from(n).where((0,l.and)((0,l.inArray)(n.entityId,e),h)).orderBy(n.sortOrder),d=new Map;for(let u of c){let f=`${u.entityId}::${u.fieldName}`;d.has(f)||d.set(f,{localeRows:[],nullRows:[]});let b=d.get(f);u.locale?b.localeRows.push(u):b.nullRows.push(u)}for(let[,{localeRows:u,nullRows:f}]of d){let b=u.length>0?u:f;for(let p of b){let F=p.entityId,k=p.fieldName;y.has(F)||y.set(F,{});let R=y.get(F);R[k]||(R[k]=[]);let E=p.data??{};R[k].push({_block:p.blockType,_id:p.id,...E})}}}return y}attachBlocks(e,t){let i=this.getBlocksFields();if(i.length!==0)for(let s of e){let n=s.id,r=t.get(n)??{};for(let{name:g}of i)s[g]=r[g]??[]}}buildCountCacheKey(e){let t=`query:${this.entity.name}`;return e?`${t}:${String(e)}`:t}};0&&(module.exports={QueryClient});
|