@objectstack/objectql 4.0.5 → 4.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/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { ServiceObject, ObjectOwnership, HookContext, QueryAST, EngineQueryOptions, DataEngineInsertOptions, EngineUpdateOptions, EngineDeleteOptions, EngineCountOptions, EngineAggregateOptions, Hook } from '@objectstack/spec/data';
1
+ import { ServiceObject, ObjectOwnership, HookContext, QueryAST, EngineQueryOptions, DataEngineInsertOptions, EngineUpdateOptions, EngineDeleteOptions, EngineCountOptions, EngineAggregateOptions, DateGranularityValue, Hook } from '@objectstack/spec/data';
2
2
  import { ObjectStackManifest, InstalledPackage, ExecutionContext } from '@objectstack/spec/kernel';
3
3
  import { ObjectStackProtocol, MetadataCacheRequest, MetadataCacheResponse, BatchUpdateRequest, BatchUpdateResponse, UpdateManyDataRequest, DeleteManyDataRequest } from '@objectstack/spec/api';
4
4
  import { IDataEngine, DriverInterface, Logger, Plugin, PluginContext, ObjectKernel } from '@objectstack/core';
@@ -105,6 +105,13 @@ interface SchemaRegistryOptions {
105
105
  * - `organization_id` — multi-tenant deployments. Required-false (the
106
106
  * SecurityPlugin populates it on insert; nullable rows are still
107
107
  * filtered out by the `tenant_isolation` RLS USING clause).
108
+ * - `created_at` / `created_by` / `updated_at` / `updated_by` — audit
109
+ * fields. Marked `system: true, readonly: true` so detail views can
110
+ * surface them in a dedicated "System Information" section while
111
+ * edit forms / drawers filter them out. The driver populates the
112
+ * timestamps; the `*_by` lookups are filled by the runtime when an
113
+ * authenticated session is present (NULL otherwise — e.g. seeded
114
+ * rows).
108
115
  */
109
116
  declare function applySystemFields(schema: ServiceObject, opts: {
110
117
  multiTenant: boolean;
@@ -257,6 +264,17 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
257
264
  */
258
265
  private projectId?;
259
266
  constructor(engine: IDataEngine, getServicesRegistry?: () => Map<string, any>, getFeedService?: () => IFeedService | undefined, projectId?: string);
267
+ /**
268
+ * One-time guard for ensuring the overlay-uniqueness UNIQUE INDEX exists
269
+ * on `sys_metadata`. ADR-0005: scopes overlays by
270
+ * `(type, name, organization_id, project_id, scope)` for active rows only.
271
+ * Idempotent SQL — safe to attempt on every protocol instance.
272
+ *
273
+ * Inlined here (rather than importing from @objectstack/metadata/migrations)
274
+ * to avoid a circular dependency: metadata already depends on objectql.
275
+ */
276
+ private overlayIndexEnsured;
277
+ private ensureOverlayIndex;
260
278
  /**
261
279
  * Exposes the project scope the protocol is bound to. Consumers like
262
280
  * the HTTP dispatcher use this to decide whether to trust the process-
@@ -312,6 +330,7 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
312
330
  getMetaItems(request: {
313
331
  type: string;
314
332
  packageId?: string;
333
+ organizationId?: string;
315
334
  }): Promise<{
316
335
  type: string;
317
336
  items: unknown[];
@@ -320,6 +339,7 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
320
339
  type: string;
321
340
  name: string;
322
341
  packageId?: string;
342
+ organizationId?: string;
323
343
  }): Promise<{
324
344
  type: string;
325
345
  name: string;
@@ -413,6 +433,80 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
413
433
  id: string;
414
434
  success: boolean;
415
435
  }>;
436
+ /**
437
+ * Cross-object substring search across all registered objects that opt in
438
+ * via `enable.searchable !== false` and `enable.apiEnabled !== false`.
439
+ * Searches text-like fields (text/textarea/email/url/phone/markdown/html/string)
440
+ * whose `searchable: true` flag is set, falling back to the object's
441
+ * `displayNameField` (or `name`) when no fields are explicitly searchable.
442
+ *
443
+ * The query is split into whitespace-separated terms; each term must match
444
+ * (case-insensitive LIKE) at least one searchable field. RBAC/RLS is
445
+ * enforced by forwarding the caller's `context` to `engine.find` so users
446
+ * only see records they are entitled to read.
447
+ */
448
+ searchAll(request: {
449
+ q: string;
450
+ objects?: string[];
451
+ limit?: number;
452
+ perObject?: number;
453
+ context?: any;
454
+ }): Promise<{
455
+ query: string;
456
+ hits: Array<{
457
+ object: string;
458
+ id: string;
459
+ title: string;
460
+ snippet?: string;
461
+ record: any;
462
+ }>;
463
+ totalObjects: number;
464
+ totalHits: number;
465
+ truncated: boolean;
466
+ }>;
467
+ /**
468
+ * Convert a qualified Lead into an Account + Contact (+ optional
469
+ * Opportunity) and mark the Lead as converted. Mirrors the Salesforce
470
+ * lead-conversion model:
471
+ *
472
+ * - If `accountId` is provided, the lead's company info is NOT used
473
+ * to create a new account; the new contact and opportunity link to
474
+ * the existing account instead.
475
+ * - If `contactId` is provided, no new contact is created either —
476
+ * useful when the lead is a new contact at an existing account.
477
+ * - `createOpportunity` defaults to true; pass `false` to convert
478
+ * without producing an opportunity (some teams convert "logos
479
+ * only" first).
480
+ * - Lead is updated atomically: `is_converted=true`,
481
+ * `converted_account`/`converted_contact`/`converted_opportunity`
482
+ * pointers, `converted_date`, and `status='converted'`.
483
+ *
484
+ * Atomicity is enforced via the default driver's transaction support
485
+ * when available; otherwise a best-effort compensation (delete
486
+ * already-created child records on failure) is attempted. Permission
487
+ * checks on each child object are inherited from the caller's
488
+ * execution context so SecurityPlugin still gates account/contact/
489
+ * opportunity creates.
490
+ */
491
+ convertLead(request: {
492
+ leadId: string;
493
+ accountId?: string;
494
+ contactId?: string;
495
+ createOpportunity?: boolean;
496
+ opportunity?: {
497
+ name?: string;
498
+ amount?: number;
499
+ close_date?: string;
500
+ stage?: string;
501
+ };
502
+ convertedStatus?: string;
503
+ context?: any;
504
+ }): Promise<{
505
+ lead: any;
506
+ account: any;
507
+ contact: any;
508
+ opportunity: any | null;
509
+ }>;
416
510
  getMetaItemCached(request: {
417
511
  type: string;
418
512
  name: string;
@@ -432,23 +526,51 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
432
526
  private mapAnalyticsOperator;
433
527
  triggerAutomation(_request: any): Promise<any>;
434
528
  deleteManyData(request: DeleteManyDataRequest): Promise<any>;
529
+ /**
530
+ * Metadata types that are customer-overridable via {@link saveMetaItem}/
531
+ * {@link deleteMetaItem} in project-kernel mode. Derived from the canonical
532
+ * registry in {@link DEFAULT_METADATA_TYPE_REGISTRY}: a type opts in by
533
+ * setting `allowOrgOverride: true` on its registry entry. The set is
534
+ * augmented with the plural form of every singular so callers using REST
535
+ * conventions (`/api/v1/meta/views/...`) get the same gate. See ADR-0005
536
+ * §"Whitelist enforcement" for the rationale and the per-type rollout
537
+ * checklist.
538
+ */
539
+ private static readonly OVERLAY_ALLOWED_TYPES;
540
+ /** Normalize plural→singular before consulting the allow-list. */
541
+ private static isOverlayAllowed;
435
542
  saveMetaItem(request: {
436
543
  type: string;
437
544
  name: string;
438
545
  item?: any;
546
+ organizationId?: string;
439
547
  }): Promise<{
440
548
  success: boolean;
441
549
  message: string;
442
- warning?: undefined;
443
- } | {
550
+ }>;
551
+ /**
552
+ * Remove a customization overlay row for the given metadata item, so the
553
+ * next read falls through to the artifact-loaded default. Implements the
554
+ * "Reset to factory default" semantic from ADR-0005. Whitelist is shared
555
+ * with {@link saveMetaItem}.
556
+ */
557
+ deleteMetaItem(request: {
558
+ type: string;
559
+ name: string;
560
+ organizationId?: string;
561
+ }): Promise<{
444
562
  success: boolean;
445
- message: string;
446
- warning: any;
563
+ message?: string;
564
+ reset?: boolean;
447
565
  }>;
448
566
  /**
449
567
  * Hydrate SchemaRegistry from the database on startup.
450
568
  * Loads all active metadata records and registers them in the in-memory registry.
451
569
  * Safe to call repeatedly — idempotent (latest DB record wins).
570
+ *
571
+ * Per ADR-0005, project-kernel mode ALSO hydrates from sys_metadata —
572
+ * customization overlay rows must survive restart. Scope filter
573
+ * (`project_id = this.projectId ?? null`) keeps tenants isolated.
452
574
  */
453
575
  loadMetaFromDb(): Promise<{
454
576
  loaded: number;
@@ -662,6 +784,20 @@ declare class ObjectQL implements IDataEngine {
662
784
  * Build a HookContext.session from ExecutionContext
663
785
  */
664
786
  private buildSession;
787
+ /**
788
+ * Build the DriverOptions blob passed to every IDataDriver call.
789
+ *
790
+ * Always carries `tenantId` from the active ExecutionContext so the
791
+ * driver can enforce per-tenant isolation (SQL driver auto-scopes reads
792
+ * and auto-injects the tenant column on writes). Existing user-supplied
793
+ * shapes (transactions, AST extras) are preserved by spreading them
794
+ * first.
795
+ *
796
+ * System / isSystem callers may still cross tenants by clearing
797
+ * `tenantId` themselves on the resulting object; this helper does not
798
+ * mask the system path.
799
+ */
800
+ private buildDriverOptions;
665
801
  /**
666
802
  * Build a HookContext.api: a ScopedContext that hooks can use to
667
803
  * read/write other objects within the same execution context.
@@ -795,7 +931,40 @@ declare class ObjectQL implements IDataEngine {
795
931
  delete(object: string, options?: EngineDeleteOptions): Promise<any>;
796
932
  count(object: string, query?: EngineCountOptions): Promise<number>;
797
933
  aggregate(object: string, query: EngineAggregateOptions): Promise<any[]>;
934
+ /**
935
+ * Run raw driver-specific commands (SQL for SqlDriver, REST for RestDriver, …).
936
+ *
937
+ * ⚠️ **Tenant isolation bypass.** Raw `execute()` does NOT thread the
938
+ * caller's `ExecutionContext.tenantId` into a `WHERE organization_id`
939
+ * predicate — drivers see the command verbatim. Callers MUST inline the
940
+ * tenant filter themselves, or restrict raw execution to genuinely global
941
+ * statements (schema migrations, sys_* / control-plane tables).
942
+ *
943
+ * Prefer the typed entry points (`find`, `update`, `delete`, `count`, …)
944
+ * whenever feasible — they auto-apply tenancy + soft-delete + audit warnings.
945
+ */
798
946
  execute(command: any, options?: Record<string, any>): Promise<any>;
947
+ /**
948
+ * Execute a callback inside a database transaction.
949
+ *
950
+ * The callback receives a context object that should be passed to all
951
+ * downstream `engine.insert/update/delete/find/findOne` calls (as
952
+ * `{ context: trxCtx }`). The transaction handle threads through
953
+ * `OperationContext.context.transaction` and the SQL driver's per-builder
954
+ * `.transacting(trx)` call.
955
+ *
956
+ * - If the default driver does not support `beginTransaction`, the callback
957
+ * runs directly with the supplied base context (no rollback). This keeps
958
+ * the API safe to call on drivers without ACID support (e.g. the
959
+ * in-memory driver in tests).
960
+ * - On callback success the transaction is committed; on any thrown error
961
+ * it is rolled back and the original error is re-thrown.
962
+ *
963
+ * Use case: multi-step operations that must be atomic (e.g. CRM
964
+ * `convertLead`, which creates an account + contact + opportunity + flips
965
+ * the lead in a single unit of work).
966
+ */
967
+ transaction<T>(callback: (trxCtx: any) => Promise<T>, baseContext?: any): Promise<T>;
799
968
  /**
800
969
  * Register a single object definition.
801
970
  *
@@ -964,6 +1133,17 @@ declare class ScopedContext {
964
1133
  get transactionHandle(): unknown;
965
1134
  }
966
1135
 
1136
+ /**
1137
+ * Group + aggregate raw rows according to the AST's `groupBy` /
1138
+ * `aggregations`. When neither is present, returns the rows unchanged.
1139
+ */
1140
+ declare function applyInMemoryAggregation(rows: any[], ast: Pick<QueryAST, 'groupBy' | 'aggregations'>): any[];
1141
+ /**
1142
+ * Bucket a date-like value into an ISO-formatted period label. Weeks start
1143
+ * Monday and use ISO week numbering.
1144
+ */
1145
+ declare function bucketDateValue(value: unknown, granularity: DateGranularityValue): string;
1146
+
967
1147
  /**
968
1148
  * Hook Execution Metrics
969
1149
  *
@@ -1156,6 +1336,46 @@ interface WrapDeclarativeOptions {
1156
1336
  */
1157
1337
  declare function wrapDeclarativeHook(meta: Hook, handler: HookHandler, opts?: WrapDeclarativeOptions): HookHandler;
1158
1338
 
1339
+ interface FieldValidationError {
1340
+ field: string;
1341
+ code: 'required' | 'min_length' | 'max_length' | 'min_value' | 'max_value' | 'invalid_email' | 'invalid_url' | 'invalid_phone' | 'invalid_number' | 'invalid_boolean' | 'invalid_date' | 'invalid_option';
1342
+ message: string;
1343
+ /** Allowed values for select/multiselect, when applicable. */
1344
+ options?: string[];
1345
+ }
1346
+ declare class ValidationError extends Error {
1347
+ readonly code = "VALIDATION_FAILED";
1348
+ readonly fields: FieldValidationError[];
1349
+ constructor(fields: FieldValidationError[]);
1350
+ }
1351
+ type Mode = 'insert' | 'update';
1352
+ interface FieldDef {
1353
+ name?: string;
1354
+ type: string;
1355
+ required?: boolean;
1356
+ readonly?: boolean;
1357
+ system?: boolean;
1358
+ multiple?: boolean;
1359
+ maxLength?: number;
1360
+ minLength?: number;
1361
+ min?: number;
1362
+ max?: number;
1363
+ options?: Array<{
1364
+ value: string | number;
1365
+ label?: string;
1366
+ } | string | number>;
1367
+ }
1368
+ /**
1369
+ * Validate a payload against a list of declared fields. `objectSchema`
1370
+ * comes from `ObjectQL.getRegistry().getObject(name)` and exposes a
1371
+ * `fields` map of `{ [fieldName]: FieldDef }`.
1372
+ *
1373
+ * Returns void on success; throws `ValidationError` on failure.
1374
+ */
1375
+ declare function validateRecord(objectSchema: {
1376
+ fields?: Record<string, FieldDef>;
1377
+ } | undefined | null, data: Record<string, unknown> | undefined | null, mode: Mode): void;
1378
+
1159
1379
  /**
1160
1380
  * MetadataFacade
1161
1381
  *
@@ -1231,14 +1451,48 @@ interface ObjectQLPluginOptions {
1231
1451
  hostContext?: Record<string, any>;
1232
1452
  /** Scope sys_metadata reads/writes to this project. */
1233
1453
  projectId?: string;
1454
+ /**
1455
+ * Override the kernel's default plugin-start timeout for this plugin.
1456
+ * Defaults to 120000 (120s). Schema sync to a remote SQL backend
1457
+ * (Neon/Postgres/Turso) is latency-bound — the SQL driver currently
1458
+ * does NOT support `batchSchemaSync`, so it issues one round-trip per
1459
+ * registered object × twice (Phase 1 + Phase 3 in `start()`). On a
1460
+ * cold remote DB with N tables this can blow past the kernel's
1461
+ * default 30s easily, even though everything is healthy.
1462
+ */
1463
+ startupTimeout?: number;
1464
+ /**
1465
+ * Skip both `syncRegisteredSchemas()` calls inside `start()` and
1466
+ * assume DDL is managed out-of-band (e.g. an `apps/cloud/scripts/migrate.ts`
1467
+ * run before deploy that connects directly to the database and creates
1468
+ * all `sys_*` + custom tables once).
1469
+ *
1470
+ * Use this on cold-start-sensitive runtimes (Cloudflare Containers,
1471
+ * Lambda) where the platform's inbound-request budget is shorter than
1472
+ * a fresh remote-DB schema sync. The plugin still hydrates the
1473
+ * SchemaRegistry from `sys_metadata` (Phase 2), so custom user
1474
+ * objects come up — they just aren't re-DDL'd on every cold boot.
1475
+ *
1476
+ * Falls back to `process.env.OS_SKIP_SCHEMA_SYNC === '1'` when the
1477
+ * option is unset, so containers can flip it via their env without a
1478
+ * code change.
1479
+ */
1480
+ skipSchemaSync?: boolean;
1234
1481
  }
1235
1482
  declare class ObjectQLPlugin implements Plugin {
1236
1483
  name: string;
1237
1484
  type: string;
1238
1485
  version: string;
1486
+ /**
1487
+ * Schema sync to remote SQL DBs is latency-bound (one round-trip per
1488
+ * table × 2 phases). Default to 120s instead of the kernel's 30s so
1489
+ * cold Neon/Turso starts don't get killed mid-sync.
1490
+ */
1491
+ startupTimeout: number;
1239
1492
  private ql;
1240
1493
  private hostContext?;
1241
1494
  private projectId?;
1495
+ private skipSchemaSync;
1242
1496
  constructor(qlOrOptions?: ObjectQL | ObjectQLPluginOptions, hostContext?: Record<string, any>);
1243
1497
  init: (ctx: PluginContext) => Promise<void>;
1244
1498
  start: (ctx: PluginContext) => Promise<void>;
@@ -1422,4 +1676,4 @@ declare function convertIntrospectedSchemaToObjects(introspectedSchema: Introspe
1422
1676
  skipSystemColumns?: boolean;
1423
1677
  }): ServiceObject[];
1424
1678
 
1425
- export { type BindHooksOptions, type BindHooksResult, DEFAULT_EXTENDER_PRIORITY, DEFAULT_OWNER_PRIORITY, type EngineMiddleware, type HookEntry, type HookHandler, type HookMetricLabel, type HookMetricOutcome, type HookMetricsRecorder, type HookSkipReason, InMemoryHookMetricsRecorder, type IntrospectedColumn, type IntrospectedForeignKey, type IntrospectedSchema, type IntrospectedTable, MetadataFacade, type ObjectContributor, ObjectQL, type ObjectQLHostContext, type ObjectQLKernelOptions, ObjectQLPlugin, ObjectRepository, ObjectStackProtocolImplementation, type OperationContext, RESERVED_NAMESPACES, SchemaRegistry, type SchemaRegistryOptions, ScopedContext, type WrapDeclarativeOptions, applySystemFields, bindHooksToEngine, computeFQN, convertIntrospectedSchemaToObjects, createObjectQLKernel, noopHookMetricsRecorder, parseFQN, toTitleCase, wrapDeclarativeHook };
1679
+ export { type BindHooksOptions, type BindHooksResult, DEFAULT_EXTENDER_PRIORITY, DEFAULT_OWNER_PRIORITY, type EngineMiddleware, type FieldValidationError, type HookEntry, type HookHandler, type HookMetricLabel, type HookMetricOutcome, type HookMetricsRecorder, type HookSkipReason, InMemoryHookMetricsRecorder, type IntrospectedColumn, type IntrospectedForeignKey, type IntrospectedSchema, type IntrospectedTable, MetadataFacade, type ObjectContributor, ObjectQL, type ObjectQLHostContext, type ObjectQLKernelOptions, ObjectQLPlugin, ObjectRepository, ObjectStackProtocolImplementation, type OperationContext, RESERVED_NAMESPACES, SchemaRegistry, type SchemaRegistryOptions, ScopedContext, ValidationError, type WrapDeclarativeOptions, applyInMemoryAggregation, applySystemFields, bindHooksToEngine, bucketDateValue, computeFQN, convertIntrospectedSchemaToObjects, createObjectQLKernel, noopHookMetricsRecorder, parseFQN, toTitleCase, validateRecord, wrapDeclarativeHook };