@lunora/server 1.0.0-alpha.45 → 1.0.0-alpha.47

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
@@ -7,6 +7,7 @@ export type { LunoraErrorCode } from '@lunora/errors';
7
7
  import { Context, Hono } from 'hono';
8
8
  import { b as Permission, R as Role, T as TypedDefinePolicyInput, a as Policy, D as DefinePolicyInput, W as WhereInput, c as RlsOptions } from "./packem_shared/types.d-C4CMJK8x.mjs";
9
9
  export type { d as PolicyContext, e as PolicyDecision, f as PolicyDecisionOf, P as PolicyOperation } from "./packem_shared/types.d-C4CMJK8x.mjs";
10
+ import { SearchLanguage, SearchStrategy } from '@lunora/search-core';
10
11
  export { type CronJob, type CronJobsBuilder, type CronScheduleKind, type DailySchedule, type IntervalSchedule, type MonthlySchedule, type WeeklySchedule, cronJobs } from '@lunora/scheduler';
11
12
  import "./data-model.mjs";
12
13
  /**
@@ -1443,10 +1444,23 @@ interface TableBuilder<Shape extends Record<string, Validator> = Record<string,
1443
1444
  rankIndex: (name: string, options: InlineRankIndexOptions<Shape>) => TableBuilder<Shape>;
1444
1445
  /** Declare relations to other tables, loaded via `findMany({ with })`. */
1445
1446
  relations: (build: (r: RelationBuilder) => Record<string, RelationDefinition>) => TableBuilder<Shape>;
1446
- /** Add a search index over a field with optional filter fields. */
1447
+ /**
1448
+ * Add a full-text search index over `field`, queried with
1449
+ * `.withSearchIndex(name, q => q.search(field, term))`. `field` may be a
1450
+ * dot-separated path into a nested object (`"properties.name"`).
1451
+ * `filterFields` (at most 16) lists the columns `.eq()` may narrow by inside
1452
+ * the search. `language` selects the text analysis (accent folding always,
1453
+ * plus that language's stopwords). `staged: true` skips the migration-time
1454
+ * backfill on a large existing table. `strategy: "native"` uses the engine's
1455
+ * own full-text index where it has one (Postgres) — faster on large corpora,
1456
+ * at the cost of the engine ranking rather than the shared scorer.
1457
+ */
1447
1458
  searchIndex: (name: string, options: {
1448
1459
  field: string;
1449
1460
  filterFields?: ReadonlyArray<string>;
1461
+ language?: SearchLanguage;
1462
+ staged?: boolean;
1463
+ strategy?: SearchStrategy;
1450
1464
  }) => TableBuilder<Shape>;
1451
1465
  /** Route storage by the named field — one DO per distinct value. */
1452
1466
  shardBy: (field: keyof Shape & string) => TableBuilder<Shape>;
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export type { LunoraErrorCode } from '@lunora/errors';
7
7
  import { Context, Hono } from 'hono';
8
8
  import { b as Permission, R as Role, T as TypedDefinePolicyInput, a as Policy, D as DefinePolicyInput, W as WhereInput, c as RlsOptions } from "./packem_shared/types.d-DdYF8E18.js";
9
9
  export type { d as PolicyContext, e as PolicyDecision, f as PolicyDecisionOf, P as PolicyOperation } from "./packem_shared/types.d-DdYF8E18.js";
10
+ import { SearchLanguage, SearchStrategy } from '@lunora/search-core';
10
11
  export { type CronJob, type CronJobsBuilder, type CronScheduleKind, type DailySchedule, type IntervalSchedule, type MonthlySchedule, type WeeklySchedule, cronJobs } from '@lunora/scheduler';
11
12
  import "./data-model.js";
12
13
  /**
@@ -1443,10 +1444,23 @@ interface TableBuilder<Shape extends Record<string, Validator> = Record<string,
1443
1444
  rankIndex: (name: string, options: InlineRankIndexOptions<Shape>) => TableBuilder<Shape>;
1444
1445
  /** Declare relations to other tables, loaded via `findMany({ with })`. */
1445
1446
  relations: (build: (r: RelationBuilder) => Record<string, RelationDefinition>) => TableBuilder<Shape>;
1446
- /** Add a search index over a field with optional filter fields. */
1447
+ /**
1448
+ * Add a full-text search index over `field`, queried with
1449
+ * `.withSearchIndex(name, q => q.search(field, term))`. `field` may be a
1450
+ * dot-separated path into a nested object (`"properties.name"`).
1451
+ * `filterFields` (at most 16) lists the columns `.eq()` may narrow by inside
1452
+ * the search. `language` selects the text analysis (accent folding always,
1453
+ * plus that language's stopwords). `staged: true` skips the migration-time
1454
+ * backfill on a large existing table. `strategy: "native"` uses the engine's
1455
+ * own full-text index where it has one (Postgres) — faster on large corpora,
1456
+ * at the cost of the engine ranking rather than the shared scorer.
1457
+ */
1447
1458
  searchIndex: (name: string, options: {
1448
1459
  field: string;
1449
1460
  filterFields?: ReadonlyArray<string>;
1461
+ language?: SearchLanguage;
1462
+ staged?: boolean;
1463
+ strategy?: SearchStrategy;
1450
1464
  }) => TableBuilder<Shape>;
1451
1465
  /** Route storage by the named field — one DO per distinct value. */
1452
1466
  shardBy: (field: keyof Shape & string) => TableBuilder<Shape>;
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{default as t}from"./packem_shared/asBucketStorage-1pFfH-Tn.mjs";import{initLunora as i}from"./packem_shared/initLunora-CHh1EmR4.mjs";import{createSecrets as a}from"./packem_shared/createSecrets-CgVPiW2C.mjs";import{LunoraEnvError as d,defineEnv as m,redactSecrets as x}from"./packem_shared/LunoraEnvError-CgpI2Mm_.mjs";import{LunoraError as c}from"./packem_shared/LunoraError-LVhdU0Lo.mjs";import{bindOrm as u,bindTableFacade as E}from"./packem_shared/bindOrm-Bp9hsM2q.mjs";import{httpAction as g,httpRoute as R,httpRouter as P,isSafeHeaderValue as h,serveStorageObject as b}from"./packem_shared/httpAction-C14NuF3V.mjs";import{defineIdentity as A}from"./packem_shared/defineIdentity-B7gfAgxx.mjs";import{onConnect as L,onDisconnect as T}from"./packem_shared/onConnect-CEtRmUpJ.mjs";import{defineMigration as C}from"./packem_shared/defineMigration-Bfpwxv2f.mjs";import{defineMutator as M}from"./packem_shared/defineMutator-BgpQ-xUo.mjs";import{composePluginMiddleware as _,defineComponent as k,definePlugin as N,defineSchemaExtension as O,installPlugins as w,mergeSchemaExtension as B}from"./packem_shared/composePluginMiddleware-COr09CXA.mjs";import{PRESENCE_DEFAULT_TTL_MS as W,PRESENCE_TABLE as j,definePresence as H,presenceExtension as J}from"./packem_shared/PRESENCE_DEFAULT_TTL_MS-CF_9uiu1.mjs";import{protectPublic as q}from"./packem_shared/protectPublic-BhKewPqm.mjs";import{defineAggregateIndex as G,defineRankIndex as K,defineSchema as Q,defineTable as X,defineVectorIndex as Y}from"./packem_shared/defineAggregateIndex-CYuH6IbJ.mjs";import{defineShape as $}from"./packem_shared/defineShape-Ds8uNqzX.mjs";import{anyApi as oe}from"./types.mjs";import{cronJobs as te}from"@lunora/scheduler";import{ValidationError as ie,v as fe}from"@lunora/values";import{allowAll as pe,deny as de,isDeny as me,toWhereInput as xe}from"./packem_shared/allowAll-BnyNbJZT.mjs";import{buildRlsReadRegistry as ce,composeShapeReadWhere as le}from"./packem_shared/buildRlsReadRegistry-WdiqSj87.mjs";import{createPolicyDsl as Ee,definePermission as Se,definePolicies as ge,definePolicy as Re,defineRole as Pe}from"./packem_shared/createPolicyDsl-sV1swpkD.mjs";import{defineStorageRule as be,defineStorageRules as ye}from"./packem_shared/defineStorageRule-BDu01PUn.mjs";import{mask as Ie}from"./packem_shared/mask-C6Bi78qj.mjs";import{rls as Te}from"./packem_shared/rls-_iVsPvhX.mjs";import{storageRules as Ce}from"./packem_shared/storageRules-BptPZbi8.mjs";const e="0.0.0";export{d as LunoraEnvError,c as LunoraError,W as PRESENCE_DEFAULT_TTL_MS,j as PRESENCE_TABLE,e as VERSION,ie as ValidationError,pe as allowAll,oe as anyApi,t as asBucketStorage,u as bindOrm,E as bindTableFacade,ce as buildRlsReadRegistry,_ as composePluginMiddleware,le as composeShapeReadWhere,Ee as createPolicyDsl,a as createSecrets,te as cronJobs,G as defineAggregateIndex,k as defineComponent,m as defineEnv,A as defineIdentity,C as defineMigration,M as defineMutator,Se as definePermission,N as definePlugin,ge as definePolicies,Re as definePolicy,H as definePresence,K as defineRankIndex,Pe as defineRole,Q as defineSchema,O as defineSchemaExtension,$ as defineShape,be as defineStorageRule,ye as defineStorageRules,X as defineTable,Y as defineVectorIndex,de as deny,g as httpAction,R as httpRoute,P as httpRouter,i as initLunora,w as installPlugins,me as isDeny,h as isSafeHeaderValue,Ie as mask,B as mergeSchemaExtension,L as onConnect,T as onDisconnect,J as presenceExtension,q as protectPublic,x as redactSecrets,Te as rls,b as serveStorageObject,Ce as storageRules,xe as toWhereInput,fe as v};
1
+ import{default as t}from"./packem_shared/asBucketStorage-1pFfH-Tn.mjs";import{initLunora as i}from"./packem_shared/initLunora-CHh1EmR4.mjs";import{createSecrets as a}from"./packem_shared/createSecrets-CgVPiW2C.mjs";import{LunoraEnvError as d,defineEnv as m,redactSecrets as x}from"./packem_shared/LunoraEnvError-CgpI2Mm_.mjs";import{LunoraError as c}from"./packem_shared/LunoraError-LVhdU0Lo.mjs";import{bindOrm as u,bindTableFacade as E}from"./packem_shared/bindOrm-Bp9hsM2q.mjs";import{httpAction as g,httpRoute as R,httpRouter as P,isSafeHeaderValue as h,serveStorageObject as b}from"./packem_shared/httpAction-C14NuF3V.mjs";import{defineIdentity as A}from"./packem_shared/defineIdentity-B7gfAgxx.mjs";import{onConnect as L,onDisconnect as T}from"./packem_shared/onConnect-CEtRmUpJ.mjs";import{defineMigration as C}from"./packem_shared/defineMigration-Bfpwxv2f.mjs";import{defineMutator as M}from"./packem_shared/defineMutator-BgpQ-xUo.mjs";import{composePluginMiddleware as _,defineComponent as k,definePlugin as N,defineSchemaExtension as O,installPlugins as w,mergeSchemaExtension as B}from"./packem_shared/composePluginMiddleware-COr09CXA.mjs";import{PRESENCE_DEFAULT_TTL_MS as W,PRESENCE_TABLE as j,definePresence as H,presenceExtension as J}from"./packem_shared/PRESENCE_DEFAULT_TTL_MS-DA0unh6n.mjs";import{protectPublic as q}from"./packem_shared/protectPublic-BhKewPqm.mjs";import{defineAggregateIndex as G,defineRankIndex as K,defineSchema as Q,defineTable as X,defineVectorIndex as Y}from"./packem_shared/defineAggregateIndex-B-401CrP.mjs";import{defineShape as $}from"./packem_shared/defineShape-Ds8uNqzX.mjs";import{anyApi as oe}from"./types.mjs";import{cronJobs as te}from"@lunora/scheduler";import{ValidationError as ie,v as fe}from"@lunora/values";import{allowAll as pe,deny as de,isDeny as me,toWhereInput as xe}from"./packem_shared/allowAll-BnyNbJZT.mjs";import{buildRlsReadRegistry as ce,composeShapeReadWhere as le}from"./packem_shared/buildRlsReadRegistry-WdiqSj87.mjs";import{createPolicyDsl as Ee,definePermission as Se,definePolicies as ge,definePolicy as Re,defineRole as Pe}from"./packem_shared/createPolicyDsl-sV1swpkD.mjs";import{defineStorageRule as be,defineStorageRules as ye}from"./packem_shared/defineStorageRule-BDu01PUn.mjs";import{mask as Ie}from"./packem_shared/mask-C6Bi78qj.mjs";import{rls as Te}from"./packem_shared/rls-_iVsPvhX.mjs";import{storageRules as Ce}from"./packem_shared/storageRules-BptPZbi8.mjs";const e="0.0.0";export{d as LunoraEnvError,c as LunoraError,W as PRESENCE_DEFAULT_TTL_MS,j as PRESENCE_TABLE,e as VERSION,ie as ValidationError,pe as allowAll,oe as anyApi,t as asBucketStorage,u as bindOrm,E as bindTableFacade,ce as buildRlsReadRegistry,_ as composePluginMiddleware,le as composeShapeReadWhere,Ee as createPolicyDsl,a as createSecrets,te as cronJobs,G as defineAggregateIndex,k as defineComponent,m as defineEnv,A as defineIdentity,C as defineMigration,M as defineMutator,Se as definePermission,N as definePlugin,ge as definePolicies,Re as definePolicy,H as definePresence,K as defineRankIndex,Pe as defineRole,Q as defineSchema,O as defineSchemaExtension,$ as defineShape,be as defineStorageRule,ye as defineStorageRules,X as defineTable,Y as defineVectorIndex,de as deny,g as httpAction,R as httpRoute,P as httpRouter,i as initLunora,w as installPlugins,me as isDeny,h as isSafeHeaderValue,Ie as mask,B as mergeSchemaExtension,L as onConnect,T as onDisconnect,J as presenceExtension,q as protectPublic,x as redactSecrets,Te as rls,b as serveStorageObject,Ce as storageRules,xe as toWhereInput,fe as v};
package/dist/otel.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { SpanHandle, LunoraTracer } from "./types.mjs";
2
+ import '@lunora/search-core';
2
3
  import '@lunora/values';
3
4
  /**
4
5
  * @since 1.0.0
package/dist/otel.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { SpanHandle, LunoraTracer } from "./types.js";
2
+ import '@lunora/search-core';
2
3
  import '@lunora/values';
3
4
  /**
4
5
  * @since 1.0.0
@@ -1 +1 @@
1
- import{v as o}from"@lunora/values";import{initLunora as E}from"./initLunora-CHh1EmR4.mjs";import{LunoraError as p}from"./LunoraError-LVhdU0Lo.mjs";import{onDisconnect as v}from"./onConnect-CEtRmUpJ.mjs";import{defineSchemaExtension as q,defineComponent as R}from"./composePluginMiddleware-COr09CXA.mjs";import{defineTable as _}from"./defineAggregateIndex-CYuH6IbJ.mjs";const D=3e4,y=4096,l="presence",h="present",c=`${l}_${h}`,M=q(l,{tables:{[h]:_({data:o.optional(o.record(o.string(),o.any())),lastSeen:o.number(),roomId:o.string(),sessionId:o.string(),userId:o.optional(o.string())}).index("byRoomSession",["roomId","sessionId"]).index("byRoom",["roomId"])}}),{mutation:b,query:T}=E.dataModel().create(),C=(u={})=>{const m=u.ttlMs??D,f=Math.max(0,Math.min(u.disconnectGraceMs??0,m)),w=b.input({data:o.optional(o.record(o.string(),o.any())),roomId:o.string(),sessionId:o.string()}).mutation(async({args:t,ctx:s})=>{const r=Date.now(),i=s.auth.userId??void 0;if(t.data!==void 0&&new TextEncoder().encode(JSON.stringify(t.data)).length>y)throw new p("BAD_REQUEST",`presence data exceeds the ${String(y)}-byte limit`);const e=await s.db.query(c).withIndex("byRoomSession",n=>n.eq("roomId",t.roomId).eq("sessionId",t.sessionId)).first();if(e&&(e.userId??void 0)!==i)throw new p("FORBIDDEN","presence heartbeat denied: this (roomId, sessionId) is held by another identity");const a={lastSeen:r,roomId:t.roomId,sessionId:t.sessionId,...t.data===void 0?{}:{data:t.data},...i===void 0?{}:{userId:i}};return await(e?s.db.patch(e._id,a):s.db.insert(c,a)),{lastSeen:r}}),S=T.input({roomId:o.string()}).query(async({args:t,ctx:s})=>{const r=Date.now()-m,i=(await s.db.query(c).withIndex("byRoom",n=>n.eq("roomId",t.roomId)).collect()).filter(n=>n.lastSeen>r).toSorted((n,d)=>d.lastSeen-n.lastSeen),e=new Set,a=[];for(const n of i){const d=n.userId;if(d!==void 0){if(e.has(d))continue;e.add(d)}const I={lastSeen:n.lastSeen,roomId:n.roomId};d!==void 0&&(I.userId=d),n.data!==void 0&&(I.data=n.data),a.push(I)}return a}),g={...b.input({roomId:o.string()}).mutation(async({args:t,ctx:s})=>{const r=Date.now()-m,i=await s.db.query(c).withIndex("byRoom",e=>e.eq("roomId",t.roomId)).filter(e=>e.lastSeen<=r).collect();return await Promise.all(i.map(e=>s.db.delete(e._id))),{deleted:i.length}}),visibility:"internal"},x=v(async(t,s)=>{const r=s.context?.roomId,i=s.context?.sessionId;if(typeof r!="string"||typeof i!="string")return;const e=await t.db.query(c).withIndex("byRoomSession",d=>d.eq("roomId",r).eq("sessionId",i)).first();if(!e)return;const a=s.userId??void 0;if((e.userId??void 0)!==a)return;if(f===0){await t.db.delete(e._id);return}const n=Math.min(e.lastSeen,Date.now()+f-m);await t.db.patch(e._id,{lastSeen:n})});return R(l,{extension:M,functions:{disconnect:x,heartbeat:w,listPresent:S,sweep:g}})};export{D as PRESENCE_DEFAULT_TTL_MS,c as PRESENCE_TABLE,C as definePresence,M as presenceExtension};
1
+ import{v as o}from"@lunora/values";import{initLunora as E}from"./initLunora-CHh1EmR4.mjs";import{LunoraError as p}from"./LunoraError-LVhdU0Lo.mjs";import{onDisconnect as q}from"./onConnect-CEtRmUpJ.mjs";import{defineSchemaExtension as v,defineComponent as R}from"./composePluginMiddleware-COr09CXA.mjs";import{defineTable as _}from"./defineAggregateIndex-B-401CrP.mjs";const D=3e4,y=4096,l="presence",h="present",c=`${l}_${h}`,M=v(l,{tables:{[h]:_({data:o.optional(o.record(o.string(),o.any())),lastSeen:o.number(),roomId:o.string(),sessionId:o.string(),userId:o.optional(o.string())}).index("byRoomSession",["roomId","sessionId"]).index("byRoom",["roomId"])}}),{mutation:b,query:T}=E.dataModel().create(),$=(u={})=>{const m=u.ttlMs??D,f=Math.max(0,Math.min(u.disconnectGraceMs??0,m)),w=b.input({data:o.optional(o.record(o.string(),o.any())),roomId:o.string(),sessionId:o.string()}).mutation(async({args:t,ctx:s})=>{const r=Date.now(),i=s.auth.userId??void 0;if(t.data!==void 0&&new TextEncoder().encode(JSON.stringify(t.data)).length>y)throw new p("BAD_REQUEST",`presence data exceeds the ${String(y)}-byte limit`);const e=await s.db.query(c).withIndex("byRoomSession",n=>n.eq("roomId",t.roomId).eq("sessionId",t.sessionId)).first();if(e&&(e.userId??void 0)!==i)throw new p("FORBIDDEN","presence heartbeat denied: this (roomId, sessionId) is held by another identity");const a={lastSeen:r,roomId:t.roomId,sessionId:t.sessionId,...t.data===void 0?{}:{data:t.data},...i===void 0?{}:{userId:i}};return await(e?s.db.patch(e._id,a):s.db.insert(c,a)),{lastSeen:r}}),S=T.input({roomId:o.string()}).query(async({args:t,ctx:s})=>{const r=Date.now()-m,i=(await s.db.query(c).withIndex("byRoom",n=>n.eq("roomId",t.roomId)).collect()).filter(n=>n.lastSeen>r).toSorted((n,d)=>d.lastSeen-n.lastSeen),e=new Set,a=[];for(const n of i){const d=n.userId;if(d!==void 0){if(e.has(d))continue;e.add(d)}const I={lastSeen:n.lastSeen,roomId:n.roomId};d!==void 0&&(I.userId=d),n.data!==void 0&&(I.data=n.data),a.push(I)}return a}),g={...b.input({roomId:o.string()}).mutation(async({args:t,ctx:s})=>{const r=Date.now()-m,i=await s.db.query(c).withIndex("byRoom",e=>e.eq("roomId",t.roomId)).filter(e=>e.lastSeen<=r).collect();return await Promise.all(i.map(e=>s.db.delete(e._id))),{deleted:i.length}}),visibility:"internal"},x=q(async(t,s)=>{const r=s.context?.roomId,i=s.context?.sessionId;if(typeof r!="string"||typeof i!="string")return;const e=await t.db.query(c).withIndex("byRoomSession",d=>d.eq("roomId",r).eq("sessionId",i)).first();if(!e)return;const a=s.userId??void 0;if((e.userId??void 0)!==a)return;if(f===0){await t.db.delete(e._id);return}const n=Math.min(e.lastSeen,Date.now()+f-m);await t.db.patch(e._id,{lastSeen:n})});return R(l,{extension:M,functions:{disconnect:x,heartbeat:w,listPresent:S,sweep:g}})};export{D as PRESENCE_DEFAULT_TTL_MS,c as PRESENCE_TABLE,$ as definePresence,M as presenceExtension};
@@ -0,0 +1 @@
1
+ import{LunoraError as i}from"@lunora/errors";import{isSearchLanguage as L,SEARCH_LANGUAGES as R,isSearchStrategy as S,SEARCH_STRATEGIES as T}from"@lunora/search-core";import{isOrWrapsFromValidator as B,v as k}from"@lunora/values";import{mergeSchemaExtension as q}from"./composePluginMiddleware-COr09CXA.mjs";const A=16,M={many:(e,t)=>({field:t.field,kind:"many",references:t.references??"_id",table:e}),one:(e,t)=>({field:t.field,kind:"one",onDelete:t.onDelete,references:t.references??"_id",table:e})},c=(e,t,a)=>({handler:a,op:t,timing:e}),D=()=>({afterDelete:e=>c("after","delete",e),afterInsert:e=>c("after","insert",e),afterUpdate:e=>c("after","update",e),beforeDelete:e=>c("before","delete",e),beforeInsert:e=>c("before","insert",e),beforeUpdate:e=>c("before","update",e)}),z=e=>{const t={...e};for(const[n,r]of Object.entries(t))if(B(r))throw new i("INTERNAL",`defineTable: column "${n}" uses v.from() which is args-only — table columns need a concrete v.* type`);const a=[],o=[],l=[],h=[],p={},b=[],w={},v=D(),y=[];let m={kind:"root"},g=!1,I=!1,x,N,E,$;const s={aggregateIndex(n,r){const d=r?.op??"count";if(d!=="count"&&!r?.field)throw new i("INTERNAL",`aggregateIndex "${n}": op "${d}" requires a "field"`);return a.push({by:r?.by,field:r?.field,name:n,on:"",op:d,where:r?.where}),s},get aggregateIndexes(){return a},get externalSource(){return $},externallyManaged(){return g=!0,s},geoIndex(n,r){return o.push({field:r.field,name:n,precision:r.precision}),s},get geoIndexes(){return o},global(n){return m={backend:n?.backend??"d1",kind:"global"},s},get isExternallyManaged(){return g},get isPublic(){return I},index(n,r,d){return l.push({fields:r,name:n,unique:d?.unique??!1}),s},get indexes(){return l},ownedBy(n){return x=n,s},get ownerField(){return x},public(){return I=!0,s},rankIndex(n,r){if(!r.sortBy||r.sortBy.length===0)throw new i("INTERNAL",`rankIndex "${n}": "sortBy" is required and must list at least one key`);const d=r.sortBy.map(u=>({direction:u.direction??"asc",field:u.field}));return h.push({name:n,on:"",partitionBy:r.partitionBy,sortBy:d,where:r.where}),s},get rankIndexes(){return h},get relationMap(){return p},relations(n){return Object.assign(p,n(M)),s},searchIndex(n,r){if(r.filterFields&&r.filterFields.length>A)throw new i("INTERNAL",`searchIndex "${n}": at most ${String(A)} filterFields are supported (got ${String(r.filterFields.length)})`);const{language:d}=r;if(d!==void 0&&!L(d))throw new i("INTERNAL",`searchIndex "${n}": unknown language "${d}" (supported: ${R.join(", ")})`);const{strategy:u}=r;if(u!==void 0&&!S(u))throw new i("INTERNAL",`searchIndex "${n}": unknown strategy "${u}" (supported: ${T.join(", ")})`);return b.push({field:r.field,filterFields:r.filterFields,language:r.language,name:n,staged:r.staged,strategy:r.strategy}),s},get searchIndexes(){return b},shape:t,shardBy(n){return m={field:n,kind:"shardBy"},s},get shardMode(){return m},get softDeleteMode(){return N},source(n){if(!n.binding)throw new i("INTERNAL","source: `binding` is required (the wrangler Hyperdrive binding name)");if(!n.query)throw new i("INTERNAL","source: `query` is required (the tenant-membership SQL)");return $=n,g=!0,s},softDelete(n){const r=n?.field??"deletedAt";return N={field:r},r in t||(t[r]=k.optional(k.number().nullable())),s},get triggerMap(){return w},triggers(n){return Object.assign(w,n(v)),s},ttl(n,r){return E={after:r?.after,field:n},s},get ttlPolicy(){return E},get vectorIndexes(){return y},vectorize(n,r){return y.push({dimensions:r.dimensions,embed:r.embed,field:n,metadata:r.metadata,metric:r.metric,name:r.index}),s}};return s},P=e=>({dimensions:e.dimensions,embed:e.embed,kind:"vectorIndex",metadata:e.metadata,metric:e.metric,select:e.source.select,table:e.source.table}),Q=(e,t)=>{const a=t.op??"count";if(a!=="count"&&!t.field)throw new i("INTERNAL",`aggregateIndex "${e}": op "${a}" requires a "field"`);return{by:t.by,field:t.field,name:e,on:t.on,op:a,where:t.where}},V=(e,t)=>{if(!t.sortBy||t.sortBy.length===0)throw new i("INTERNAL",`rankIndex "${e}": "sortBy" is required and must list at least one key`);const a=t.sortBy.map(o=>({direction:o.direction??"asc",field:o.field}));return{name:e,on:t.table,partitionBy:t.partitionBy,sortBy:a,where:t.where}},f=e=>({...e,extend(t){return f(q(e,t))},jurisdiction(t){return f(e)},rls(t){return f({...e,rlsMode:t})}}),j=e=>{for(const[t,a]of Object.entries(e)){for(const o of a.aggregateIndexes)o.on===""&&(o.on=t);for(const o of a.rankIndexes)o.on===""&&(o.on=t)}},O=(e,t,a)=>{for(const o of Object.values(t)){const l=e[o.on];if(!l)throw new i("INTERNAL",`defineAggregateIndex "${o.name}": unknown table "${o.on}"`);l.aggregateIndexes.push(o)}for(const o of Object.values(a)){const l=e[o.on];if(!l)throw new i("INTERNAL",`defineRankIndex "${o.name}": unknown table "${o.on}"`);l.rankIndexes.push(o)}},F=e=>{if(e.cursor)return"cursor";if(e.reconcileEveryMs!==void 0)return"reconcileEveryMs";if(e.softDeleteColumn!==void 0)return"softDeleteColumn"},C=(e,t)=>{const a=t.mode==="incremental";if(t.mode!==void 0&&t.mode!=="full-pull"&&!a)throw new i("INTERNAL",`defineSchema: table "${e}" uses \`mode: ${JSON.stringify(t.mode)}\` — supported modes are "full-pull" (default) and "incremental".`);if(!a){const o=F(t);if(o)throw new i("INTERNAL",`defineSchema: table "${e}" sets \`${o}\` but is not \`mode: "incremental"\` — that knob only applies to incremental ingest. Set \`mode: "incremental"\` or remove \`${o}\`.`);return}if(!t.cursor?.column||!t.cursor.query)throw new i("INTERNAL",`defineSchema: table "${e}" is \`mode: "incremental"\` but has no \`cursor\` — add \`cursor: { column: "updated_at", query: "… WHERE … > $N" }\` binding the watermark as the trailing parameter.`);if(t.reconcileEveryMs===void 0&&t.softDeleteColumn===void 0)throw new i("INTERNAL",`defineSchema: table "${e}" is \`mode: "incremental"\` with no delete-visibility path — an incremental pull never sees upstream deletes, so it would accumulate phantom rows. Add \`reconcileEveryMs\` (a periodic full-pull sweep) or \`softDeleteColumn\` (an upstream tombstone column).`)},_=e=>{for(const[t,a]of Object.entries(e)){const o=a.externalSource;if(o){if(a.shardMode.kind==="global")throw new i("INTERNAL",`defineSchema: table "${t}" cannot be both .source() and .global() — a sourced table materializes into a shard DO's SQLite, a global table lives in the external tier`);if(a.shardMode.kind==="shardBy"&&!o.tenantBy)throw new i("INTERNAL",`defineSchema: sourced + .shardBy() table "${t}" needs a \`tenantBy\` mapper — without it every tenant's DO would run the same unscoped query and replicate the whole multitenant table (a cross-tenant leak). Add \`tenantBy: (shardKey) => [shardKey]\` binding the shard key into the query's parameters.`);C(t,o)}}},W=(e,t={},a={},o={})=>(j(e),O(e,a,o),_(e),f({tables:e,vectorIndexes:t}));export{Q as defineAggregateIndex,V as defineRankIndex,W as defineSchema,z as defineTable,P as defineVectorIndex};
package/dist/types.d.mts CHANGED
@@ -1,3 +1,5 @@
1
+ import { SearchLanguage, SearchStrategy } from '@lunora/search-core';
2
+ export type { SearchLanguage, SearchStrategy } from '@lunora/search-core';
1
3
  import { ValidatorMap, InferValidatorMap, Id, Validator, Infer } from '@lunora/values';
2
4
  /** Map of validators describing a function's args record. Alias of `@lunora/values`' shared {@link ValidatorMap}. */
3
5
  type ArgsValidator = ValidatorMap;
@@ -113,9 +115,48 @@ interface IndexDefinition {
113
115
  unique?: boolean;
114
116
  }
115
117
  interface SearchIndexDefinition {
118
+ /** Indexed text column; a dot-separated path (`"properties.name"`) reads a nested field. */
116
119
  field: string;
120
+ /** Columns `.eq()` may narrow by inside the search builder. At most 16. */
117
121
  filterFields?: ReadonlyArray<string>;
122
+ /**
123
+ * Text analysis for this index. Accent folding is always applied — it is
124
+ * what makes `café` and `cafe` the same token on every backend, which they
125
+ * otherwise are not. Naming a language additionally drops that language's
126
+ * stopwords from both documents and queries.
127
+ *
128
+ * Analysis is baked into the stored index, so changing this rebuilds it:
129
+ * the runtime records which profile a companion was built with and
130
+ * re-indexes when it no longer matches.
131
+ */
132
+ language?: SearchLanguage;
118
133
  name: string;
134
+ /**
135
+ * Skip the migration-time backfill. By default, creating the index's
136
+ * companion also indexes the rows already in the table, so a search index
137
+ * added to a populated table works immediately. On a very large table that
138
+ * scan is expensive to run inside a deploy: `staged: true` maintains the
139
+ * index on write only and leaves the initial population to an out-of-band
140
+ * `backfillSearchIndexes`.
141
+ */
142
+ staged?: boolean;
143
+ /**
144
+ * How the index is stored and matched.
145
+ *
146
+ * `"portable"` (default) keeps one implementation on every backend, with
147
+ * identical matching *and* ranking — the invariant the rest of the search
148
+ * docs rest on.
149
+ *
150
+ * `"native"` hands matching to the engine's own full-text index where it has
151
+ * one (Postgres `tsvector` + GIN today; ignored elsewhere). It scales far
152
+ * better on large corpora and common terms, because the portable path
153
+ * aggregates every matching token row before ranking. The trade: the engine
154
+ * ranks, so results are ordered by its formula rather than the shared
155
+ * scorer. Matching still agrees — the vector is built from the same analyzed
156
+ * tokens — but a `.global()` table using it will not return rows in the same
157
+ * order as the sharded twin.
158
+ */
159
+ strategy?: SearchStrategy;
119
160
  }
120
161
  /**
121
162
  * A geospatial index declared via `.geoIndex(name, { field })`. The runtime
@@ -1319,19 +1360,19 @@ interface VectorRecord {
1319
1360
  * Read-only vector surface exposed on {@link QueryCtx}. Mirrors the read half
1320
1361
  * of `@lunora/bindings/vectors`' `LunoraVectors` so the live adapter is assignable.
1321
1362
  */
1322
- interface VectorSearchReader {
1323
- getByIds: (indexName: string, ids: ReadonlyArray<string>) => Promise<ReadonlyArray<VectorRecord>>;
1324
- query: (indexName: string, input: VectorQueryInput) => Promise<VectorMatches>;
1363
+ interface VectorSearchReader<IndexName extends string = string> {
1364
+ getByIds: (indexName: IndexName, ids: ReadonlyArray<string>) => Promise<ReadonlyArray<VectorRecord>>;
1365
+ query: (indexName: IndexName, input: VectorQueryInput) => Promise<VectorMatches>;
1325
1366
  }
1326
1367
  /**
1327
1368
  * Mutating vector surface on {@link MutationCtx} / {@link ActionCtx}. `upsert`
1328
1369
  * is queued post-commit by default; `upsertNow` forces a synchronous write.
1329
1370
  * `db.delete` on a vectorized table auto-propagates the matching `deleteByIds`.
1330
1371
  */
1331
- interface VectorSearch extends VectorSearchReader {
1332
- deleteByIds: (indexName: string, ids: ReadonlyArray<string>) => Promise<void>;
1333
- upsert: (indexName: string, input: VectorUpsertInput) => Promise<void>;
1334
- upsertNow: (indexName: string, input: VectorUpsertInput) => Promise<void>;
1372
+ interface VectorSearch<IndexName extends string = string> extends VectorSearchReader<IndexName> {
1373
+ deleteByIds: (indexName: IndexName, ids: ReadonlyArray<string>) => Promise<void>;
1374
+ upsert: (indexName: IndexName, input: VectorUpsertInput) => Promise<void>;
1375
+ upsertNow: (indexName: IndexName, input: VectorUpsertInput) => Promise<void>;
1335
1376
  }
1336
1377
  /**
1337
1378
  * Structured, filterable key/value fields attached to a log line — the second
package/dist/types.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { SearchLanguage, SearchStrategy } from '@lunora/search-core';
2
+ export type { SearchLanguage, SearchStrategy } from '@lunora/search-core';
1
3
  import { ValidatorMap, InferValidatorMap, Id, Validator, Infer } from '@lunora/values';
2
4
  /** Map of validators describing a function's args record. Alias of `@lunora/values`' shared {@link ValidatorMap}. */
3
5
  type ArgsValidator = ValidatorMap;
@@ -113,9 +115,48 @@ interface IndexDefinition {
113
115
  unique?: boolean;
114
116
  }
115
117
  interface SearchIndexDefinition {
118
+ /** Indexed text column; a dot-separated path (`"properties.name"`) reads a nested field. */
116
119
  field: string;
120
+ /** Columns `.eq()` may narrow by inside the search builder. At most 16. */
117
121
  filterFields?: ReadonlyArray<string>;
122
+ /**
123
+ * Text analysis for this index. Accent folding is always applied — it is
124
+ * what makes `café` and `cafe` the same token on every backend, which they
125
+ * otherwise are not. Naming a language additionally drops that language's
126
+ * stopwords from both documents and queries.
127
+ *
128
+ * Analysis is baked into the stored index, so changing this rebuilds it:
129
+ * the runtime records which profile a companion was built with and
130
+ * re-indexes when it no longer matches.
131
+ */
132
+ language?: SearchLanguage;
118
133
  name: string;
134
+ /**
135
+ * Skip the migration-time backfill. By default, creating the index's
136
+ * companion also indexes the rows already in the table, so a search index
137
+ * added to a populated table works immediately. On a very large table that
138
+ * scan is expensive to run inside a deploy: `staged: true` maintains the
139
+ * index on write only and leaves the initial population to an out-of-band
140
+ * `backfillSearchIndexes`.
141
+ */
142
+ staged?: boolean;
143
+ /**
144
+ * How the index is stored and matched.
145
+ *
146
+ * `"portable"` (default) keeps one implementation on every backend, with
147
+ * identical matching *and* ranking — the invariant the rest of the search
148
+ * docs rest on.
149
+ *
150
+ * `"native"` hands matching to the engine's own full-text index where it has
151
+ * one (Postgres `tsvector` + GIN today; ignored elsewhere). It scales far
152
+ * better on large corpora and common terms, because the portable path
153
+ * aggregates every matching token row before ranking. The trade: the engine
154
+ * ranks, so results are ordered by its formula rather than the shared
155
+ * scorer. Matching still agrees — the vector is built from the same analyzed
156
+ * tokens — but a `.global()` table using it will not return rows in the same
157
+ * order as the sharded twin.
158
+ */
159
+ strategy?: SearchStrategy;
119
160
  }
120
161
  /**
121
162
  * A geospatial index declared via `.geoIndex(name, { field })`. The runtime
@@ -1319,19 +1360,19 @@ interface VectorRecord {
1319
1360
  * Read-only vector surface exposed on {@link QueryCtx}. Mirrors the read half
1320
1361
  * of `@lunora/bindings/vectors`' `LunoraVectors` so the live adapter is assignable.
1321
1362
  */
1322
- interface VectorSearchReader {
1323
- getByIds: (indexName: string, ids: ReadonlyArray<string>) => Promise<ReadonlyArray<VectorRecord>>;
1324
- query: (indexName: string, input: VectorQueryInput) => Promise<VectorMatches>;
1363
+ interface VectorSearchReader<IndexName extends string = string> {
1364
+ getByIds: (indexName: IndexName, ids: ReadonlyArray<string>) => Promise<ReadonlyArray<VectorRecord>>;
1365
+ query: (indexName: IndexName, input: VectorQueryInput) => Promise<VectorMatches>;
1325
1366
  }
1326
1367
  /**
1327
1368
  * Mutating vector surface on {@link MutationCtx} / {@link ActionCtx}. `upsert`
1328
1369
  * is queued post-commit by default; `upsertNow` forces a synchronous write.
1329
1370
  * `db.delete` on a vectorized table auto-propagates the matching `deleteByIds`.
1330
1371
  */
1331
- interface VectorSearch extends VectorSearchReader {
1332
- deleteByIds: (indexName: string, ids: ReadonlyArray<string>) => Promise<void>;
1333
- upsert: (indexName: string, input: VectorUpsertInput) => Promise<void>;
1334
- upsertNow: (indexName: string, input: VectorUpsertInput) => Promise<void>;
1372
+ interface VectorSearch<IndexName extends string = string> extends VectorSearchReader<IndexName> {
1373
+ deleteByIds: (indexName: IndexName, ids: ReadonlyArray<string>) => Promise<void>;
1374
+ upsert: (indexName: IndexName, input: VectorUpsertInput) => Promise<void>;
1375
+ upsertNow: (indexName: IndexName, input: VectorUpsertInput) => Promise<void>;
1335
1376
  }
1336
1377
  /**
1337
1378
  * Structured, filterable key/value fields attached to a log line — the second
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/server",
3
- "version": "1.0.0-alpha.45",
3
+ "version": "1.0.0-alpha.47",
4
4
  "description": "Server primitives for Lunora: defineSchema, defineTable, query, mutation, and action",
5
5
  "keywords": [
6
6
  "backend",
@@ -70,7 +70,8 @@
70
70
  "@lunora/scheduler": "1.0.0-alpha.12",
71
71
  "@lunora/values": "1.0.0-alpha.11",
72
72
  "drizzle-orm": "^0.45.2",
73
- "hono": "^4.12.32"
73
+ "hono": "^4.12.32",
74
+ "@lunora/search-core": "1.0.0-alpha.0"
74
75
  },
75
76
  "peerDependencies": {
76
77
  "@opentelemetry/api": "^1.9.1"
@@ -1 +0,0 @@
1
- import{LunoraError as s}from"@lunora/errors";import{isOrWrapsFromValidator as B,v as E}from"@lunora/values";import{mergeSchemaExtension as A}from"./composePluginMiddleware-COr09CXA.mjs";const L={many:(e,r)=>({field:r.field,kind:"many",references:r.references??"_id",table:e}),one:(e,r)=>({field:r.field,kind:"one",onDelete:r.onDelete,references:r.references??"_id",table:e})},u=(e,r,a)=>({handler:a,op:r,timing:e}),R=()=>({afterDelete:e=>u("after","delete",e),afterInsert:e=>u("after","insert",e),afterUpdate:e=>u("after","update",e),beforeDelete:e=>u("before","delete",e),beforeInsert:e=>u("before","insert",e),beforeUpdate:e=>u("before","update",e)}),F=e=>{const r={...e};for(const[t,n]of Object.entries(r))if(B(n))throw new s("INTERNAL",`defineTable: column "${t}" uses v.from() which is args-only — table columns need a concrete v.* type`);const a=[],o=[],d=[],h=[],g={},b=[],p={},$=R(),y=[];let f={kind:"root"},m=!1,w=!1,I,x,N,v;const i={aggregateIndex(t,n){const l=n?.op??"count";if(l!=="count"&&!n?.field)throw new s("INTERNAL",`aggregateIndex "${t}": op "${l}" requires a "field"`);return a.push({by:n?.by,field:n?.field,name:t,on:"",op:l,where:n?.where}),i},get aggregateIndexes(){return a},get externalSource(){return v},externallyManaged(){return m=!0,i},geoIndex(t,n){return o.push({field:n.field,name:t,precision:n.precision}),i},get geoIndexes(){return o},global(t){return f={backend:t?.backend??"d1",kind:"global"},i},get isExternallyManaged(){return m},get isPublic(){return w},index(t,n,l){return d.push({fields:n,name:t,unique:l?.unique??!1}),i},get indexes(){return d},ownedBy(t){return I=t,i},get ownerField(){return I},public(){return w=!0,i},rankIndex(t,n){if(!n.sortBy||n.sortBy.length===0)throw new s("INTERNAL",`rankIndex "${t}": "sortBy" is required and must list at least one key`);const l=n.sortBy.map(k=>({direction:k.direction??"asc",field:k.field}));return h.push({name:t,on:"",partitionBy:n.partitionBy,sortBy:l,where:n.where}),i},get rankIndexes(){return h},get relationMap(){return g},relations(t){return Object.assign(g,t(L)),i},searchIndex(t,n){return b.push({field:n.field,filterFields:n.filterFields,name:t}),i},get searchIndexes(){return b},shape:r,shardBy(t){return f={field:t,kind:"shardBy"},i},get shardMode(){return f},get softDeleteMode(){return x},source(t){if(!t.binding)throw new s("INTERNAL","source: `binding` is required (the wrangler Hyperdrive binding name)");if(!t.query)throw new s("INTERNAL","source: `query` is required (the tenant-membership SQL)");return v=t,m=!0,i},softDelete(t){const n=t?.field??"deletedAt";return x={field:n},n in r||(r[n]=E.optional(E.number().nullable())),i},get triggerMap(){return p},triggers(t){return Object.assign(p,t($)),i},ttl(t,n){return N={after:n?.after,field:t},i},get ttlPolicy(){return N},get vectorIndexes(){return y},vectorize(t,n){return y.push({dimensions:n.dimensions,embed:n.embed,field:t,metadata:n.metadata,metric:n.metric,name:n.index}),i}};return i},H=e=>({dimensions:e.dimensions,embed:e.embed,kind:"vectorIndex",metadata:e.metadata,metric:e.metric,select:e.source.select,table:e.source.table}),K=(e,r)=>{const a=r.op??"count";if(a!=="count"&&!r.field)throw new s("INTERNAL",`aggregateIndex "${e}": op "${a}" requires a "field"`);return{by:r.by,field:r.field,name:e,on:r.on,op:a,where:r.where}},V=(e,r)=>{if(!r.sortBy||r.sortBy.length===0)throw new s("INTERNAL",`rankIndex "${e}": "sortBy" is required and must list at least one key`);const a=r.sortBy.map(o=>({direction:o.direction??"asc",field:o.field}));return{name:e,on:r.table,partitionBy:r.partitionBy,sortBy:a,where:r.where}},c=e=>({...e,extend(r){return c(A(e,r))},jurisdiction(r){return c(e)},rls(r){return c({...e,rlsMode:r})}}),T=e=>{for(const[r,a]of Object.entries(e)){for(const o of a.aggregateIndexes)o.on===""&&(o.on=r);for(const o of a.rankIndexes)o.on===""&&(o.on=r)}},q=(e,r,a)=>{for(const o of Object.values(r)){const d=e[o.on];if(!d)throw new s("INTERNAL",`defineAggregateIndex "${o.name}": unknown table "${o.on}"`);d.aggregateIndexes.push(o)}for(const o of Object.values(a)){const d=e[o.on];if(!d)throw new s("INTERNAL",`defineRankIndex "${o.name}": unknown table "${o.on}"`);d.rankIndexes.push(o)}},S=e=>{if(e.cursor)return"cursor";if(e.reconcileEveryMs!==void 0)return"reconcileEveryMs";if(e.softDeleteColumn!==void 0)return"softDeleteColumn"},M=(e,r)=>{const a=r.mode==="incremental";if(r.mode!==void 0&&r.mode!=="full-pull"&&!a)throw new s("INTERNAL",`defineSchema: table "${e}" uses \`mode: ${JSON.stringify(r.mode)}\` — supported modes are "full-pull" (default) and "incremental".`);if(!a){const o=S(r);if(o)throw new s("INTERNAL",`defineSchema: table "${e}" sets \`${o}\` but is not \`mode: "incremental"\` — that knob only applies to incremental ingest. Set \`mode: "incremental"\` or remove \`${o}\`.`);return}if(!r.cursor?.column||!r.cursor.query)throw new s("INTERNAL",`defineSchema: table "${e}" is \`mode: "incremental"\` but has no \`cursor\` — add \`cursor: { column: "updated_at", query: "… WHERE … > $N" }\` binding the watermark as the trailing parameter.`);if(r.reconcileEveryMs===void 0&&r.softDeleteColumn===void 0)throw new s("INTERNAL",`defineSchema: table "${e}" is \`mode: "incremental"\` with no delete-visibility path — an incremental pull never sees upstream deletes, so it would accumulate phantom rows. Add \`reconcileEveryMs\` (a periodic full-pull sweep) or \`softDeleteColumn\` (an upstream tombstone column).`)},D=e=>{for(const[r,a]of Object.entries(e)){const o=a.externalSource;if(o){if(a.shardMode.kind==="global")throw new s("INTERNAL",`defineSchema: table "${r}" cannot be both .source() and .global() — a sourced table materializes into a shard DO's SQLite, a global table lives in the external tier`);if(a.shardMode.kind==="shardBy"&&!o.tenantBy)throw new s("INTERNAL",`defineSchema: sourced + .shardBy() table "${r}" needs a \`tenantBy\` mapper — without it every tenant's DO would run the same unscoped query and replicate the whole multitenant table (a cross-tenant leak). Add \`tenantBy: (shardKey) => [shardKey]\` binding the shard key into the query's parameters.`);M(r,o)}}},_=(e,r={},a={},o={})=>(T(e),q(e,a,o),D(e),c({tables:e,vectorIndexes:r}));export{K as defineAggregateIndex,V as defineRankIndex,_ as defineSchema,F as defineTable,H as defineVectorIndex};