@autofleet/sadot 1.11.1-beta-484bfc0a.0 → 1.12.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.cjs CHANGED
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,`__esModule`,{value:!0});const e=require(`./src/utils/logger/index.cjs`),t=require(`./src/utils/constants/index.cjs`),n=require(`./src/models/CustomFieldValue.cjs`),r=require(`./src/models/CustomFieldDefinition.cjs`),i=require(`./src/models/CustomFieldEntries.cjs`),a=require(`./src/models/CustomValidator.cjs`),o=require(`./src/utils/custom-fields-mode.cjs`),s=require(`./src/migrations/index.cjs`),c=require(`./src/models/index.cjs`),l=require(`./src/mat-path-state.cjs`),u=require(`./src/api/index.cjs`),d=require(`./src/utils/db/index.cjs`),f=require(`./src/utils/helpers/index.cjs`),p=require(`./src/scopes/filter.cjs`),m=require(`./src/utils/init.cjs`),h=require(`./src/init-state.cjs`),g=require(`./src/utils/validations/schema/custom-fields.cjs`),_=require(`./src/repository/backfill.cjs`),v=new h.SadotInitializationState,y=async(t,n,r)=>{e.tryAddingTraceIdMiddleware();let{models:i,useModelTypeMapping:a,isMatPathEnabled:s,runLegacyMigrations:f,skipMigrations:p}=r,h=o.resolveCustomFieldsEntriesMode(r);e.default.info(`sadot custom-fields mode: ${h}`),l.matPathState.isMatPathEnabled=s,o.setCustomFieldsEntriesMode(h),t&&t.use(`/api`,u.default);let g=r.sequelize??d.default(r.databaseConfig);return process.env.NODE_ENV===`test`&&await c.initTestModels(g),m.addHooks(i,n,{customFieldsEntriesMode:h}),await c.initTables(g,r.getUser,{customFieldsEntriesMode:h,useModelTypeMapping:a,isMatPathEnabled:s,runLegacyMigrations:f,skipMigrations:p}),m.addScopes(i,n,{customFieldsEntriesMode:h}),m.applyCustomAssociation(i),e.default.debug(`sadot - custom fields finished initializing with models`,i),g},b=async(e,t,n)=>{let r=y(e,t,n);return v.setInitPromise(r,!n.sequelize),await r};var x=b;const S=(e,t)=>{m.removeHooks(e,t)};exports.CUSTOM_FIELDS_ENTRIES_MODE=o.CUSTOM_FIELDS_ENTRIES_MODE,exports.CUSTOM_FIELDS_FILTER_SCOPE=t.CUSTOM_FIELDS_FILTER_SCOPE,exports.CustomFieldDefinition=r.default,exports.CustomFieldDefinitionType=t.CustomFieldDefinitionType,exports.CustomFieldEntries=i.default,exports.CustomFieldValue=n.default,exports.CustomFieldsSchema=g.CustomFieldsSchema,exports.CustomValidator=a.default,exports.backfillCustomFieldEntries=_.backfillCustomFieldEntries,exports.customFieldsSortScope=p.customFieldsSortScope,exports.default=x,exports.disableCustomFields=S,exports.entriesValuesTextExpression=t.entriesValuesTextExpression,exports.generateCustomFieldSearchQueryPayload=f.generateCustomFieldSearchQueryPayload,exports.generateRandomString=f.generateRandomString,exports.runSadotMigrations=s.runSadotMigrations,exports.sadotInitState=v,exports.supportedEntities=t.supportedEntities;
1
+ Object.defineProperty(exports,`__esModule`,{value:!0});const e=require(`./src/utils/logger/index.cjs`),t=require(`./src/utils/constants/index.cjs`),n=require(`./src/models/CustomFieldValue.cjs`),r=require(`./src/models/CustomFieldDefinition.cjs`),i=require(`./src/models/CustomFieldEntries.cjs`),a=require(`./src/models/CustomValidator.cjs`),o=require(`./src/utils/custom-fields-mode.cjs`),s=require(`./src/migrations/index.cjs`),c=require(`./src/models/index.cjs`),l=require(`./src/mat-path-state.cjs`),u=require(`./src/api/index.cjs`),d=require(`./src/utils/db/index.cjs`),f=require(`./src/utils/helpers/index.cjs`),p=require(`./src/scopes/filter.cjs`),m=require(`./src/utils/init.cjs`),h=require(`./src/init-state.cjs`),g=require(`./src/utils/validations/schema/custom-fields.cjs`),_=require(`./src/repository/backfill.cjs`),v=new h.SadotInitializationState,y=async(t,n,r)=>{e.tryAddingTraceIdMiddleware();let{models:i,useModelTypeMapping:a,isMatPathEnabled:s,runLegacyMigrations:f,skipMigrations:p}=r,h=o.resolveCustomFieldsEntriesMode(r);e.default.info(`sadot custom-fields mode: ${h}`),l.matPathState.isMatPathEnabled=s,t&&t.use(`/api`,u.default);let g=r.sequelize??d.default(r.databaseConfig);return process.env.NODE_ENV===`test`&&await c.initTestModels(g),m.addHooks(i,n,{customFieldsEntriesMode:h}),await c.initTables(g,r.getUser,{customFieldsEntriesMode:h,useModelTypeMapping:a,isMatPathEnabled:s,runLegacyMigrations:f,skipMigrations:p}),m.addScopes(i,n,{customFieldsEntriesMode:h}),m.applyCustomAssociation(i),e.default.debug(`sadot - custom fields finished initializing with models`,i),g},b=async(e,t,n)=>{let r=y(e,t,n);return v.setInitPromise(r,!n.sequelize),await r};var x=b;const S=(e,t)=>{m.removeHooks(e,t)};exports.CUSTOM_FIELDS_ENTRIES_MODE=o.CUSTOM_FIELDS_ENTRIES_MODE,exports.CUSTOM_FIELDS_FILTER_SCOPE=t.CUSTOM_FIELDS_FILTER_SCOPE,exports.CustomFieldDefinition=r.default,exports.CustomFieldDefinitionType=t.CustomFieldDefinitionType,exports.CustomFieldEntries=i.default,exports.CustomFieldValue=n.default,exports.CustomFieldsSchema=g.CustomFieldsSchema,exports.CustomValidator=a.default,exports.backfillCustomFieldEntries=_.backfillCustomFieldEntries,exports.customFieldsSortScope=p.customFieldsSortScope,exports.default=x,exports.disableCustomFields=S,exports.entriesValuesTextExpression=t.entriesValuesTextExpression,exports.generateCustomFieldSearchQueryPayload=f.generateCustomFieldSearchQueryPayload,exports.generateRandomString=f.generateRandomString,exports.runSadotMigrations=s.runSadotMigrations,exports.sadotInitState=v,exports.supportedEntities=t.supportedEntities;
2
2
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["sadotInitState: SadotInitializationState","SadotInitializationState","resolveCustomFieldsEntriesMode","api","initDB","initTestModels","initTables"],"sources":["../src/index.ts"],"sourcesContent":["import type { Application } from 'express';\nimport type { Sequelize } from 'sequelize-typescript';\nimport {\n initTables, initTestModels,\n} from './models';\nimport api from './api';\nimport initDB from './utils/db';\nimport logger, { tryAddingTraceIdMiddleware } from './utils/logger';\nimport type { CustomFieldOptions, ModelFetcher, Models } from './types';\nimport {\n addHooks, addScopes, applyCustomAssociation, removeHooks,\n} from './utils/init';\nimport { SadotInitializationState } from './init-state';\nimport { matPathState } from './mat-path-state';\nimport { resolveCustomFieldsEntriesMode, setCustomFieldsEntriesMode } from './utils/custom-fields-mode';\n\nexport { CUSTOM_FIELDS_ENTRIES_MODE } from './utils/custom-fields-mode';\n\nexport type { CustomFieldsEntriesMode } from './utils/custom-fields-mode';\n\nexport * from './utils/validations/schema/custom-fields';\n\nexport * from './utils/constants';\n\nexport * from './utils/helpers';\n\nexport { customFieldsSortScope } from './scopes/filter';\n\nexport { runSadotMigrations } from './migrations';\n\nexport { backfillCustomFieldEntries } from './repository/backfill';\n\nexport {\n CustomFieldDefinition,\n CustomFieldEntries,\n CustomFieldValue,\n CustomValidator,\n} from './models';\n\nexport const sadotInitState: SadotInitializationState = new SadotInitializationState();\n\n/**\n * Internal implementation of custom fields initialization\n * Contains all the business logic without state tracking\n */\nconst internalUseCustomFields = async (\n app: Pick<Application, 'use'> | null,\n getModel: ModelFetcher,\n options: CustomFieldOptions,\n): Promise<Sequelize> => {\n tryAddingTraceIdMiddleware();\n const {\n models,\n useModelTypeMapping,\n isMatPathEnabled,\n runLegacyMigrations,\n skipMigrations,\n } = options;\n // Resolved once here so every downstream call site receives a concrete mode.\n // The old boolean relied on `undefined` being falsy; an enum has no falsy \"off\".\n const customFieldsEntriesMode = resolveCustomFieldsEntriesMode(options);\n logger.info(`sadot custom-fields mode: ${customFieldsEntriesMode}`);\n matPathState.isMatPathEnabled = isMatPathEnabled;\n setCustomFieldsEntriesMode(customFieldsEntriesMode);\n if (app) {\n app.use('/api', api);\n }\n const sequelize = options.sequelize ?? initDB(options.databaseConfig);\n if (process.env.NODE_ENV === 'test') {\n await initTestModels(sequelize);\n }\n // The order is important\n addHooks(models, getModel, { customFieldsEntriesMode });\n await initTables(\n sequelize,\n options.getUser,\n {\n customFieldsEntriesMode,\n useModelTypeMapping,\n isMatPathEnabled,\n runLegacyMigrations,\n skipMigrations,\n }\n );\n addScopes(models, getModel, { customFieldsEntriesMode });\n applyCustomAssociation(models);\n\n logger.debug('sadot - custom fields finished initializing with models', models);\n return sequelize;\n};\n\n/**\n * Adding custom fields enrichment to the models inside the MODELS_FILE_NAME json file\n * @see {@link 'custom-fields/config'} for configurations\n */\nconst useCustomFields = async (\n app: Pick<Application, 'use'> | null,\n getModel: ModelFetcher,\n options: CustomFieldOptions,\n): Promise<Sequelize> => {\n const initPromise = internalUseCustomFields(app, getModel, options);\n\n sadotInitState.setInitPromise(initPromise, !options.sequelize);\n\n return await initPromise;\n};\n\nexport default useCustomFields;\n\nexport const disableCustomFields = (models: Models[], getModel: ModelFetcher): void => {\n removeHooks(models, getModel);\n};\n"],"mappings":"4zBAuCaA,EAA2C,IAAIC,EAAAA,yBAMtD,EAA0B,MAC9B,EACA,EACA,IACuB,CACvB,EAAA,4BAA4B,CAC5B,GAAM,CACJ,SACA,sBACA,mBACA,sBACA,kBACE,EAGE,EAA0BC,EAAAA,+BAA+B,EAAQ,CACvE,EAAA,QAAO,KAAK,6BAA6B,IAA0B,CACnE,EAAA,aAAa,iBAAmB,EAChC,EAAA,2BAA2B,EAAwB,CAC/C,GACF,EAAI,IAAI,OAAQC,EAAAA,QAAI,CAEtB,IAAM,EAAY,EAAQ,WAAaC,EAAAA,QAAO,EAAQ,eAAe,CAqBrE,OApBI,QAAQ,IAAI,WAAa,QAC3B,MAAMC,EAAAA,eAAe,EAAU,CAGjC,EAAA,SAAS,EAAQ,EAAU,CAAE,0BAAyB,CAAC,CACvD,MAAMC,EAAAA,WACJ,EACA,EAAQ,QACR,CACE,0BACA,sBACA,mBACA,sBACA,iBACD,CACF,CACD,EAAA,UAAU,EAAQ,EAAU,CAAE,0BAAyB,CAAC,CACxD,EAAA,uBAAuB,EAAO,CAE9B,EAAA,QAAO,MAAM,0DAA2D,EAAO,CACxE,GAOH,EAAkB,MACtB,EACA,EACA,IACuB,CACvB,IAAM,EAAc,EAAwB,EAAK,EAAU,EAAQ,CAInE,OAFA,EAAe,eAAe,EAAa,CAAC,EAAQ,UAAU,CAEvD,MAAM,GAGf,IAAA,EAAe,EAEf,MAAa,GAAuB,EAAkB,IAAiC,CACrF,EAAA,YAAY,EAAQ,EAAS"}
1
+ {"version":3,"file":"index.cjs","names":["sadotInitState: SadotInitializationState","SadotInitializationState","resolveCustomFieldsEntriesMode","api","initDB","initTestModels","initTables"],"sources":["../src/index.ts"],"sourcesContent":["import type { Application } from 'express';\nimport type { Sequelize } from 'sequelize-typescript';\nimport {\n initTables, initTestModels,\n} from './models';\nimport api from './api';\nimport initDB from './utils/db';\nimport logger, { tryAddingTraceIdMiddleware } from './utils/logger';\nimport type { CustomFieldOptions, ModelFetcher, Models } from './types';\nimport {\n addHooks, addScopes, applyCustomAssociation, removeHooks,\n} from './utils/init';\nimport { SadotInitializationState } from './init-state';\nimport { matPathState } from './mat-path-state';\nimport { resolveCustomFieldsEntriesMode } from './utils/custom-fields-mode';\n\nexport { CUSTOM_FIELDS_ENTRIES_MODE } from './utils/custom-fields-mode';\n\nexport type { CustomFieldsEntriesMode } from './utils/custom-fields-mode';\n\nexport * from './utils/validations/schema/custom-fields';\n\nexport * from './utils/constants';\n\nexport * from './utils/helpers';\n\nexport { customFieldsSortScope } from './scopes/filter';\n\nexport { runSadotMigrations } from './migrations';\n\nexport { backfillCustomFieldEntries } from './repository/backfill';\n\nexport {\n CustomFieldDefinition,\n CustomFieldEntries,\n CustomFieldValue,\n CustomValidator,\n} from './models';\n\nexport const sadotInitState: SadotInitializationState = new SadotInitializationState();\n\n/**\n * Internal implementation of custom fields initialization\n * Contains all the business logic without state tracking\n */\nconst internalUseCustomFields = async (\n app: Pick<Application, 'use'> | null,\n getModel: ModelFetcher,\n options: CustomFieldOptions,\n): Promise<Sequelize> => {\n tryAddingTraceIdMiddleware();\n const {\n models,\n useModelTypeMapping,\n isMatPathEnabled,\n runLegacyMigrations,\n skipMigrations,\n } = options;\n // Resolved once here so every downstream call site receives a concrete mode.\n // The old boolean relied on `undefined` being falsy; an enum has no falsy \"off\".\n const customFieldsEntriesMode = resolveCustomFieldsEntriesMode(options);\n logger.info(`sadot custom-fields mode: ${customFieldsEntriesMode}`);\n matPathState.isMatPathEnabled = isMatPathEnabled;\n if (app) {\n app.use('/api', api);\n }\n const sequelize = options.sequelize ?? initDB(options.databaseConfig);\n if (process.env.NODE_ENV === 'test') {\n await initTestModels(sequelize);\n }\n // The order is important\n addHooks(models, getModel, { customFieldsEntriesMode });\n await initTables(\n sequelize,\n options.getUser,\n {\n customFieldsEntriesMode,\n useModelTypeMapping,\n isMatPathEnabled,\n runLegacyMigrations,\n skipMigrations,\n }\n );\n addScopes(models, getModel, { customFieldsEntriesMode });\n applyCustomAssociation(models);\n\n logger.debug('sadot - custom fields finished initializing with models', models);\n return sequelize;\n};\n\n/**\n * Adding custom fields enrichment to the models inside the MODELS_FILE_NAME json file\n * @see {@link 'custom-fields/config'} for configurations\n */\nconst useCustomFields = async (\n app: Pick<Application, 'use'> | null,\n getModel: ModelFetcher,\n options: CustomFieldOptions,\n): Promise<Sequelize> => {\n const initPromise = internalUseCustomFields(app, getModel, options);\n\n sadotInitState.setInitPromise(initPromise, !options.sequelize);\n\n return await initPromise;\n};\n\nexport default useCustomFields;\n\nexport const disableCustomFields = (models: Models[], getModel: ModelFetcher): void => {\n removeHooks(models, getModel);\n};\n"],"mappings":"4zBAuCaA,EAA2C,IAAIC,EAAAA,yBAMtD,EAA0B,MAC9B,EACA,EACA,IACuB,CACvB,EAAA,4BAA4B,CAC5B,GAAM,CACJ,SACA,sBACA,mBACA,sBACA,kBACE,EAGE,EAA0BC,EAAAA,+BAA+B,EAAQ,CACvE,EAAA,QAAO,KAAK,6BAA6B,IAA0B,CACnE,EAAA,aAAa,iBAAmB,EAC5B,GACF,EAAI,IAAI,OAAQC,EAAAA,QAAI,CAEtB,IAAM,EAAY,EAAQ,WAAaC,EAAAA,QAAO,EAAQ,eAAe,CAqBrE,OApBI,QAAQ,IAAI,WAAa,QAC3B,MAAMC,EAAAA,eAAe,EAAU,CAGjC,EAAA,SAAS,EAAQ,EAAU,CAAE,0BAAyB,CAAC,CACvD,MAAMC,EAAAA,WACJ,EACA,EAAQ,QACR,CACE,0BACA,sBACA,mBACA,sBACA,iBACD,CACF,CACD,EAAA,UAAU,EAAQ,EAAU,CAAE,0BAAyB,CAAC,CACxD,EAAA,uBAAuB,EAAO,CAE9B,EAAA,QAAO,MAAM,0DAA2D,EAAO,CACxE,GAOH,EAAkB,MACtB,EACA,EACA,IACuB,CACvB,IAAM,EAAc,EAAwB,EAAK,EAAU,EAAQ,CAInE,OAFA,EAAe,eAAe,EAAa,CAAC,EAAQ,UAAU,CAEvD,MAAM,GAGf,IAAA,EAAe,EAEf,MAAa,GAAuB,EAAkB,IAAiC,CACrF,EAAA,YAAY,EAAQ,EAAS"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import e,{tryAddingTraceIdMiddleware as t}from"./src/utils/logger/index.js";import{CUSTOM_FIELDS_FILTER_SCOPE as n,CustomFieldDefinitionType as r,entriesValuesTextExpression as i,supportedEntities as a}from"./src/utils/constants/index.js";import o from"./src/models/CustomFieldValue.js";import s from"./src/models/CustomFieldDefinition.js";import c from"./src/models/CustomFieldEntries.js";import l from"./src/models/CustomValidator.js";import{CUSTOM_FIELDS_ENTRIES_MODE as u,resolveCustomFieldsEntriesMode as d,setCustomFieldsEntriesMode as f}from"./src/utils/custom-fields-mode.js";import{runSadotMigrations as p}from"./src/migrations/index.js";import{initTables as m,initTestModels as h}from"./src/models/index.js";import{matPathState as g}from"./src/mat-path-state.js";import _ from"./src/api/index.js";import v from"./src/utils/db/index.js";import{generateCustomFieldSearchQueryPayload as y,generateRandomString as b}from"./src/utils/helpers/index.js";import{customFieldsSortScope as x}from"./src/scopes/filter.js";import{addHooks as S,addScopes as C,applyCustomAssociation as w,removeHooks as T}from"./src/utils/init.js";import{SadotInitializationState as E}from"./src/init-state.js";import{CustomFieldsSchema as D}from"./src/utils/validations/schema/custom-fields.js";import{backfillCustomFieldEntries as O}from"./src/repository/backfill.js";const k=new E,A=async(n,r,i)=>{t();let{models:a,useModelTypeMapping:o,isMatPathEnabled:s,runLegacyMigrations:c,skipMigrations:l}=i,u=d(i);e.info(`sadot custom-fields mode: ${u}`),g.isMatPathEnabled=s,f(u),n&&n.use(`/api`,_);let p=i.sequelize??v(i.databaseConfig);return process.env.NODE_ENV===`test`&&await h(p),S(a,r,{customFieldsEntriesMode:u}),await m(p,i.getUser,{customFieldsEntriesMode:u,useModelTypeMapping:o,isMatPathEnabled:s,runLegacyMigrations:c,skipMigrations:l}),C(a,r,{customFieldsEntriesMode:u}),w(a),e.debug(`sadot - custom fields finished initializing with models`,a),p};var j=async(e,t,n)=>{let r=A(e,t,n);return k.setInitPromise(r,!n.sequelize),await r};const M=(e,t)=>{T(e,t)};export{u as CUSTOM_FIELDS_ENTRIES_MODE,n as CUSTOM_FIELDS_FILTER_SCOPE,s as CustomFieldDefinition,r as CustomFieldDefinitionType,c as CustomFieldEntries,o as CustomFieldValue,D as CustomFieldsSchema,l as CustomValidator,O as backfillCustomFieldEntries,x as customFieldsSortScope,j as default,M as disableCustomFields,i as entriesValuesTextExpression,y as generateCustomFieldSearchQueryPayload,b as generateRandomString,p as runSadotMigrations,k as sadotInitState,a as supportedEntities};
1
+ import e,{tryAddingTraceIdMiddleware as t}from"./src/utils/logger/index.js";import{CUSTOM_FIELDS_FILTER_SCOPE as n,CustomFieldDefinitionType as r,entriesValuesTextExpression as i,supportedEntities as a}from"./src/utils/constants/index.js";import o from"./src/models/CustomFieldValue.js";import s from"./src/models/CustomFieldDefinition.js";import c from"./src/models/CustomFieldEntries.js";import l from"./src/models/CustomValidator.js";import{CUSTOM_FIELDS_ENTRIES_MODE as u,resolveCustomFieldsEntriesMode as d}from"./src/utils/custom-fields-mode.js";import{runSadotMigrations as f}from"./src/migrations/index.js";import{initTables as p,initTestModels as m}from"./src/models/index.js";import{matPathState as h}from"./src/mat-path-state.js";import g from"./src/api/index.js";import _ from"./src/utils/db/index.js";import{generateCustomFieldSearchQueryPayload as v,generateRandomString as y}from"./src/utils/helpers/index.js";import{customFieldsSortScope as b}from"./src/scopes/filter.js";import{addHooks as x,addScopes as S,applyCustomAssociation as C,removeHooks as w}from"./src/utils/init.js";import{SadotInitializationState as T}from"./src/init-state.js";import{CustomFieldsSchema as E}from"./src/utils/validations/schema/custom-fields.js";import{backfillCustomFieldEntries as D}from"./src/repository/backfill.js";const O=new T,k=async(n,r,i)=>{t();let{models:a,useModelTypeMapping:o,isMatPathEnabled:s,runLegacyMigrations:c,skipMigrations:l}=i,u=d(i);e.info(`sadot custom-fields mode: ${u}`),h.isMatPathEnabled=s,n&&n.use(`/api`,g);let f=i.sequelize??_(i.databaseConfig);return process.env.NODE_ENV===`test`&&await m(f),x(a,r,{customFieldsEntriesMode:u}),await p(f,i.getUser,{customFieldsEntriesMode:u,useModelTypeMapping:o,isMatPathEnabled:s,runLegacyMigrations:c,skipMigrations:l}),S(a,r,{customFieldsEntriesMode:u}),C(a),e.debug(`sadot - custom fields finished initializing with models`,a),f};var A=async(e,t,n)=>{let r=k(e,t,n);return O.setInitPromise(r,!n.sequelize),await r};const j=(e,t)=>{w(e,t)};export{u as CUSTOM_FIELDS_ENTRIES_MODE,n as CUSTOM_FIELDS_FILTER_SCOPE,s as CustomFieldDefinition,r as CustomFieldDefinitionType,c as CustomFieldEntries,o as CustomFieldValue,E as CustomFieldsSchema,l as CustomValidator,D as backfillCustomFieldEntries,b as customFieldsSortScope,A as default,j as disableCustomFields,i as entriesValuesTextExpression,v as generateCustomFieldSearchQueryPayload,y as generateRandomString,f as runSadotMigrations,O as sadotInitState,a as supportedEntities};
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["sadotInitState: SadotInitializationState","api","initDB"],"sources":["../src/index.ts"],"sourcesContent":["import type { Application } from 'express';\nimport type { Sequelize } from 'sequelize-typescript';\nimport {\n initTables, initTestModels,\n} from './models';\nimport api from './api';\nimport initDB from './utils/db';\nimport logger, { tryAddingTraceIdMiddleware } from './utils/logger';\nimport type { CustomFieldOptions, ModelFetcher, Models } from './types';\nimport {\n addHooks, addScopes, applyCustomAssociation, removeHooks,\n} from './utils/init';\nimport { SadotInitializationState } from './init-state';\nimport { matPathState } from './mat-path-state';\nimport { resolveCustomFieldsEntriesMode, setCustomFieldsEntriesMode } from './utils/custom-fields-mode';\n\nexport { CUSTOM_FIELDS_ENTRIES_MODE } from './utils/custom-fields-mode';\n\nexport type { CustomFieldsEntriesMode } from './utils/custom-fields-mode';\n\nexport * from './utils/validations/schema/custom-fields';\n\nexport * from './utils/constants';\n\nexport * from './utils/helpers';\n\nexport { customFieldsSortScope } from './scopes/filter';\n\nexport { runSadotMigrations } from './migrations';\n\nexport { backfillCustomFieldEntries } from './repository/backfill';\n\nexport {\n CustomFieldDefinition,\n CustomFieldEntries,\n CustomFieldValue,\n CustomValidator,\n} from './models';\n\nexport const sadotInitState: SadotInitializationState = new SadotInitializationState();\n\n/**\n * Internal implementation of custom fields initialization\n * Contains all the business logic without state tracking\n */\nconst internalUseCustomFields = async (\n app: Pick<Application, 'use'> | null,\n getModel: ModelFetcher,\n options: CustomFieldOptions,\n): Promise<Sequelize> => {\n tryAddingTraceIdMiddleware();\n const {\n models,\n useModelTypeMapping,\n isMatPathEnabled,\n runLegacyMigrations,\n skipMigrations,\n } = options;\n // Resolved once here so every downstream call site receives a concrete mode.\n // The old boolean relied on `undefined` being falsy; an enum has no falsy \"off\".\n const customFieldsEntriesMode = resolveCustomFieldsEntriesMode(options);\n logger.info(`sadot custom-fields mode: ${customFieldsEntriesMode}`);\n matPathState.isMatPathEnabled = isMatPathEnabled;\n setCustomFieldsEntriesMode(customFieldsEntriesMode);\n if (app) {\n app.use('/api', api);\n }\n const sequelize = options.sequelize ?? initDB(options.databaseConfig);\n if (process.env.NODE_ENV === 'test') {\n await initTestModels(sequelize);\n }\n // The order is important\n addHooks(models, getModel, { customFieldsEntriesMode });\n await initTables(\n sequelize,\n options.getUser,\n {\n customFieldsEntriesMode,\n useModelTypeMapping,\n isMatPathEnabled,\n runLegacyMigrations,\n skipMigrations,\n }\n );\n addScopes(models, getModel, { customFieldsEntriesMode });\n applyCustomAssociation(models);\n\n logger.debug('sadot - custom fields finished initializing with models', models);\n return sequelize;\n};\n\n/**\n * Adding custom fields enrichment to the models inside the MODELS_FILE_NAME json file\n * @see {@link 'custom-fields/config'} for configurations\n */\nconst useCustomFields = async (\n app: Pick<Application, 'use'> | null,\n getModel: ModelFetcher,\n options: CustomFieldOptions,\n): Promise<Sequelize> => {\n const initPromise = internalUseCustomFields(app, getModel, options);\n\n sadotInitState.setInitPromise(initPromise, !options.sequelize);\n\n return await initPromise;\n};\n\nexport default useCustomFields;\n\nexport const disableCustomFields = (models: Models[], getModel: ModelFetcher): void => {\n removeHooks(models, getModel);\n};\n"],"mappings":"q0CAuCA,MAAaA,EAA2C,IAAI,EAMtD,EAA0B,MAC9B,EACA,EACA,IACuB,CACvB,GAA4B,CAC5B,GAAM,CACJ,SACA,sBACA,mBACA,sBACA,kBACE,EAGE,EAA0B,EAA+B,EAAQ,CACvE,EAAO,KAAK,6BAA6B,IAA0B,CACnE,EAAa,iBAAmB,EAChC,EAA2B,EAAwB,CAC/C,GACF,EAAI,IAAI,OAAQC,EAAI,CAEtB,IAAM,EAAY,EAAQ,WAAaC,EAAO,EAAQ,eAAe,CAqBrE,OApBI,QAAQ,IAAI,WAAa,QAC3B,MAAM,EAAe,EAAU,CAGjC,EAAS,EAAQ,EAAU,CAAE,0BAAyB,CAAC,CACvD,MAAM,EACJ,EACA,EAAQ,QACR,CACE,0BACA,sBACA,mBACA,sBACA,iBACD,CACF,CACD,EAAU,EAAQ,EAAU,CAAE,0BAAyB,CAAC,CACxD,EAAuB,EAAO,CAE9B,EAAO,MAAM,0DAA2D,EAAO,CACxE,GAmBT,IAAA,EAZwB,MACtB,EACA,EACA,IACuB,CACvB,IAAM,EAAc,EAAwB,EAAK,EAAU,EAAQ,CAInE,OAFA,EAAe,eAAe,EAAa,CAAC,EAAQ,UAAU,CAEvD,MAAM,GAKf,MAAa,GAAuB,EAAkB,IAAiC,CACrF,EAAY,EAAQ,EAAS"}
1
+ {"version":3,"file":"index.js","names":["sadotInitState: SadotInitializationState","api","initDB"],"sources":["../src/index.ts"],"sourcesContent":["import type { Application } from 'express';\nimport type { Sequelize } from 'sequelize-typescript';\nimport {\n initTables, initTestModels,\n} from './models';\nimport api from './api';\nimport initDB from './utils/db';\nimport logger, { tryAddingTraceIdMiddleware } from './utils/logger';\nimport type { CustomFieldOptions, ModelFetcher, Models } from './types';\nimport {\n addHooks, addScopes, applyCustomAssociation, removeHooks,\n} from './utils/init';\nimport { SadotInitializationState } from './init-state';\nimport { matPathState } from './mat-path-state';\nimport { resolveCustomFieldsEntriesMode } from './utils/custom-fields-mode';\n\nexport { CUSTOM_FIELDS_ENTRIES_MODE } from './utils/custom-fields-mode';\n\nexport type { CustomFieldsEntriesMode } from './utils/custom-fields-mode';\n\nexport * from './utils/validations/schema/custom-fields';\n\nexport * from './utils/constants';\n\nexport * from './utils/helpers';\n\nexport { customFieldsSortScope } from './scopes/filter';\n\nexport { runSadotMigrations } from './migrations';\n\nexport { backfillCustomFieldEntries } from './repository/backfill';\n\nexport {\n CustomFieldDefinition,\n CustomFieldEntries,\n CustomFieldValue,\n CustomValidator,\n} from './models';\n\nexport const sadotInitState: SadotInitializationState = new SadotInitializationState();\n\n/**\n * Internal implementation of custom fields initialization\n * Contains all the business logic without state tracking\n */\nconst internalUseCustomFields = async (\n app: Pick<Application, 'use'> | null,\n getModel: ModelFetcher,\n options: CustomFieldOptions,\n): Promise<Sequelize> => {\n tryAddingTraceIdMiddleware();\n const {\n models,\n useModelTypeMapping,\n isMatPathEnabled,\n runLegacyMigrations,\n skipMigrations,\n } = options;\n // Resolved once here so every downstream call site receives a concrete mode.\n // The old boolean relied on `undefined` being falsy; an enum has no falsy \"off\".\n const customFieldsEntriesMode = resolveCustomFieldsEntriesMode(options);\n logger.info(`sadot custom-fields mode: ${customFieldsEntriesMode}`);\n matPathState.isMatPathEnabled = isMatPathEnabled;\n if (app) {\n app.use('/api', api);\n }\n const sequelize = options.sequelize ?? initDB(options.databaseConfig);\n if (process.env.NODE_ENV === 'test') {\n await initTestModels(sequelize);\n }\n // The order is important\n addHooks(models, getModel, { customFieldsEntriesMode });\n await initTables(\n sequelize,\n options.getUser,\n {\n customFieldsEntriesMode,\n useModelTypeMapping,\n isMatPathEnabled,\n runLegacyMigrations,\n skipMigrations,\n }\n );\n addScopes(models, getModel, { customFieldsEntriesMode });\n applyCustomAssociation(models);\n\n logger.debug('sadot - custom fields finished initializing with models', models);\n return sequelize;\n};\n\n/**\n * Adding custom fields enrichment to the models inside the MODELS_FILE_NAME json file\n * @see {@link 'custom-fields/config'} for configurations\n */\nconst useCustomFields = async (\n app: Pick<Application, 'use'> | null,\n getModel: ModelFetcher,\n options: CustomFieldOptions,\n): Promise<Sequelize> => {\n const initPromise = internalUseCustomFields(app, getModel, options);\n\n sadotInitState.setInitPromise(initPromise, !options.sequelize);\n\n return await initPromise;\n};\n\nexport default useCustomFields;\n\nexport const disableCustomFields = (models: Models[], getModel: ModelFetcher): void => {\n removeHooks(models, getModel);\n};\n"],"mappings":"qyCAuCA,MAAaA,EAA2C,IAAI,EAMtD,EAA0B,MAC9B,EACA,EACA,IACuB,CACvB,GAA4B,CAC5B,GAAM,CACJ,SACA,sBACA,mBACA,sBACA,kBACE,EAGE,EAA0B,EAA+B,EAAQ,CACvE,EAAO,KAAK,6BAA6B,IAA0B,CACnE,EAAa,iBAAmB,EAC5B,GACF,EAAI,IAAI,OAAQC,EAAI,CAEtB,IAAM,EAAY,EAAQ,WAAaC,EAAO,EAAQ,eAAe,CAqBrE,OApBI,QAAQ,IAAI,WAAa,QAC3B,MAAM,EAAe,EAAU,CAGjC,EAAS,EAAQ,EAAU,CAAE,0BAAyB,CAAC,CACvD,MAAM,EACJ,EACA,EAAQ,QACR,CACE,0BACA,sBACA,mBACA,sBACA,iBACD,CACF,CACD,EAAU,EAAQ,EAAU,CAAE,0BAAyB,CAAC,CACxD,EAAuB,EAAO,CAE9B,EAAO,MAAM,0DAA2D,EAAO,CACxE,GAmBT,IAAA,EAZwB,MACtB,EACA,EACA,IACuB,CACvB,IAAM,EAAc,EAAwB,EAAK,EAAU,EAAQ,CAInE,OAFA,EAAe,eAAe,EAAa,CAAC,EAAQ,UAAU,CAEvD,MAAM,GAKf,MAAa,GAAuB,EAAkB,IAAiC,CACrF,EAAY,EAAQ,EAAS"}
@@ -4,6 +4,7 @@ import { Sequelize } from "sequelize-typescript";
4
4
  //#region src/migrations/index.d.ts
5
5
  interface MigratorOptions extends CustomFieldsEntriesModeOptions {
6
6
  useModelTypeMapping?: boolean;
7
+ useEntriesSearchIndex?: boolean;
7
8
  withLock?: boolean;
8
9
  }
9
10
  /**
@@ -19,7 +20,7 @@ interface MigratorOptions extends CustomFieldsEntriesModeOptions {
19
20
  *
20
21
  * @param options - Migration execution options.
21
22
  * @param options.withLock - Whether to acquire a PostgreSQL advisory lock before running the migrations. Defaults to true.
22
- * @returns A promise that resolves when the migrations have completed.
23
+ * @returns A promise that resolves when the migrations have completed.
23
24
  */
24
25
  declare const runSadotMigrations: (sequelize: Sequelize, options?: MigratorOptions) => Promise<void>;
25
26
  //#endregion
@@ -1,6 +1,9 @@
1
- const e=require(`../utils/constants/index.cjs`),t=async({context:t})=>{let{sequelize:n}=t;await n.query(`CREATE EXTENSION IF NOT EXISTS pg_trgm`),await n.query(`
2
- CREATE INDEX IF NOT EXISTS "idx_cfe_values_trgm"
1
+ const e=require(`../../_virtual/rolldown_runtime.cjs`),t=require(`../utils/constants/index.cjs`);let n=require(`sequelize`);const r=`idx_cfe_search_trgm`,i=async e=>{let[t]=await e.query(`SELECT i.indisvalid
2
+ FROM pg_class c
3
+ JOIN pg_index i ON i.indexrelid = c.oid
4
+ WHERE c.relname = '${r}'`,{type:n.QueryTypes.SELECT});return t?.indisvalid},a=async({context:e})=>{let{sequelize:n}=e;if(await n.query(`CREATE EXTENSION IF NOT EXISTS pg_trgm`),await i(n)===!1&&await n.query(`DROP INDEX CONCURRENTLY IF EXISTS "${r}"`),await n.query(`
5
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS "${r}"
3
6
  ON "custom_field_entries"
4
- USING gin ((${e.entriesValuesTextExpression(`"custom_fields"`)}) gin_trgm_ops)
5
- `)},n=async({context:e})=>{await e.sequelize.query(`DROP INDEX IF EXISTS "idx_cfe_values_trgm"`)};exports.down=n,exports.up=t;
7
+ USING gin ((${t.entriesValuesTextExpression(`"custom_fields"`)}) gin_trgm_ops)
8
+ `),!await i(n))throw Error(`${r} built INVALID — rerun the migration to rebuild it`);await n.query(`ANALYZE "custom_field_entries"`)},o=async({context:e})=>{await e.sequelize.query(`DROP INDEX CONCURRENTLY IF EXISTS "${r}"`)};exports.down=o,exports.up=a;
6
9
  //# sourceMappingURL=004-entries-search-trgm-index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"004-entries-search-trgm-index.cjs","names":["up: MigrationFn<QueryInterface>","entriesValuesTextExpression","down: MigrationFn<QueryInterface>"],"sources":["../../../src/migrations/004-entries-search-trgm-index.ts"],"sourcesContent":["import type { MigrationFn } from 'umzug';\nimport type { QueryInterface } from 'sequelize';\nimport { entriesValuesTextExpression } from '../utils/constants';\n\n// Trigram index powering free-text search over custom_field_entries (AF-10932).\n// The indexed expression extracts VALUES only ('$.*') — key names can never match a search.\n// It must stay character-identical to the search helper's predicate, which is why both\n// sides build it from `entriesValuesTextExpression`.\n//\n// pg_trgm ships with Postgres itself (contrib) and is allowlisted on Cloud SQL; CREATE\n// EXTENSION is idempotent and fails loudly on servers lacking it.\n//\n// Plain CREATE INDEX (not CONCURRENTLY): umzug runs these non-transactionally at service\n// init, and a failed CONCURRENTLY build leaves an INVALID index that IF NOT EXISTS would\n// then skip forever. For very large existing tables the index can be pre-created manually\n// with CONCURRENTLY under the same name before deploying.\n\nexport const up: MigrationFn<QueryInterface> = async ({ context: queryInterface }) => {\n const { sequelize } = queryInterface;\n\n await sequelize.query('CREATE EXTENSION IF NOT EXISTS pg_trgm');\n\n await sequelize.query(`\n CREATE INDEX IF NOT EXISTS \"idx_cfe_values_trgm\"\n ON \"custom_field_entries\"\n USING gin ((${entriesValuesTextExpression('\"custom_fields\"')}) gin_trgm_ops)\n `);\n};\n\nexport const down: MigrationFn<QueryInterface> = async ({ context: queryInterface }) => {\n // The extension is intentionally kept — other database objects may depend on it.\n await queryInterface.sequelize.query('DROP INDEX IF EXISTS \"idx_cfe_values_trgm\"');\n};\n"],"mappings":"gDAiBaA,EAAkC,MAAO,CAAE,QAAS,KAAqB,CACpF,GAAM,CAAE,aAAc,EAEtB,MAAM,EAAU,MAAM,yCAAyC,CAE/D,MAAM,EAAU,MAAM;;;kBAGNC,EAAAA,4BAA4B,kBAAkB,CAAC;IAC7D,EAGSC,EAAoC,MAAO,CAAE,QAAS,KAAqB,CAEtF,MAAM,EAAe,UAAU,MAAM,6CAA6C"}
1
+ {"version":3,"file":"004-entries-search-trgm-index.cjs","names":["sequelize","QueryTypes","up: MigrationFn<QueryInterface>","entriesValuesTextExpression","down: MigrationFn<QueryInterface>"],"sources":["../../../src/migrations/004-entries-search-trgm-index.ts"],"sourcesContent":["import type { MigrationFn } from 'umzug';\nimport { QueryTypes, type QueryInterface } from 'sequelize';\nimport { entriesValuesTextExpression } from '../utils/constants';\n\nconst INDEX_NAME = 'idx_cfe_search_trgm';\n\n// Trigram index for free-text search over custom_field_entries (AF-10932).\n\nconst isValid = async (sequelize: QueryInterface['sequelize']): Promise<boolean | undefined> => {\n const [index] = await sequelize.query(\n `SELECT i.indisvalid\n FROM pg_class c\n JOIN pg_index i ON i.indexrelid = c.oid\n WHERE c.relname = '${INDEX_NAME}'`,\n { type: QueryTypes.SELECT },\n ) as [{ indisvalid: boolean; } | undefined];\n return index?.indisvalid;\n};\n\nexport const up: MigrationFn<QueryInterface> = async ({ context: queryInterface }) => {\n const { sequelize } = queryInterface;\n\n await sequelize.query('CREATE EXTENSION IF NOT EXISTS pg_trgm');\n\n // A failed CONCURRENTLY build leaves an INVALID index that IF NOT EXISTS would skip forever,\n // so a retry would never rebuild it.\n if (await isValid(sequelize) === false) {\n await sequelize.query(`DROP INDEX CONCURRENTLY IF EXISTS \"${INDEX_NAME}\"`);\n }\n\n await sequelize.query(`\n CREATE INDEX CONCURRENTLY IF NOT EXISTS \"${INDEX_NAME}\"\n ON \"custom_field_entries\"\n USING gin ((${entriesValuesTextExpression('\"custom_fields\"')}) gin_trgm_ops)\n `);\n\n if (!await isValid(sequelize)) {\n throw new Error(`${INDEX_NAME} built INVALID — rerun the migration to rebuild it`);\n }\n\n // Without stats the planner won't choose an expression index.\n await sequelize.query('ANALYZE \"custom_field_entries\"');\n};\n\nexport const down: MigrationFn<QueryInterface> = async ({ context: queryInterface }) => {\n // Extension kept — other objects may depend on it.\n await queryInterface.sequelize.query(`DROP INDEX CONCURRENTLY IF EXISTS \"${INDEX_NAME}\"`);\n};\n"],"mappings":"4HAIA,MAAM,EAAa,sBAIb,EAAU,KAAO,IAAyE,CAC9F,GAAM,CAAC,GAAS,MAAMA,EAAU,MAC9B;;;0BAGsB,EAAW,GACjC,CAAE,KAAMC,EAAAA,WAAW,OAAQ,CAC5B,CACD,OAAO,GAAO,YAGHC,EAAkC,MAAO,CAAE,QAAS,KAAqB,CACpF,GAAM,CAAE,UAAA,GAAc,EAgBtB,GAdA,MAAMF,EAAU,MAAM,yCAAyC,CAI3D,MAAM,EAAQA,EAAU,GAAK,IAC/B,MAAMA,EAAU,MAAM,sCAAsC,EAAW,GAAG,CAG5E,MAAMA,EAAU,MAAM;+CACuB,EAAW;;kBAExCG,EAAAA,4BAA4B,kBAAkB,CAAC;IAC7D,CAEE,CAAC,MAAM,EAAQH,EAAU,CAC3B,MAAU,MAAM,GAAG,EAAW,oDAAoD,CAIpF,MAAMA,EAAU,MAAM,iCAAiC,EAG5CI,EAAoC,MAAO,CAAE,QAAS,KAAqB,CAEtF,MAAM,EAAe,UAAU,MAAM,sCAAsC,EAAW,GAAG"}
@@ -1,6 +1,9 @@
1
- import{entriesValuesTextExpression as e}from"../utils/constants/index.js";const t=async({context:t})=>{let{sequelize:n}=t;await n.query(`CREATE EXTENSION IF NOT EXISTS pg_trgm`),await n.query(`
2
- CREATE INDEX IF NOT EXISTS "idx_cfe_values_trgm"
1
+ import{entriesValuesTextExpression as e}from"../utils/constants/index.js";import{QueryTypes as t}from"sequelize";const n=`idx_cfe_search_trgm`,r=async e=>{let[r]=await e.query(`SELECT i.indisvalid
2
+ FROM pg_class c
3
+ JOIN pg_index i ON i.indexrelid = c.oid
4
+ WHERE c.relname = '${n}'`,{type:t.SELECT});return r?.indisvalid},i=async({context:t})=>{let{sequelize:i}=t;if(await i.query(`CREATE EXTENSION IF NOT EXISTS pg_trgm`),await r(i)===!1&&await i.query(`DROP INDEX CONCURRENTLY IF EXISTS "${n}"`),await i.query(`
5
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS "${n}"
3
6
  ON "custom_field_entries"
4
7
  USING gin ((${e(`"custom_fields"`)}) gin_trgm_ops)
5
- `)},n=async({context:e})=>{await e.sequelize.query(`DROP INDEX IF EXISTS "idx_cfe_values_trgm"`)};export{n as down,t as up};
8
+ `),!await r(i))throw Error(`${n} built INVALID — rerun the migration to rebuild it`);await i.query(`ANALYZE "custom_field_entries"`)},a=async({context:e})=>{await e.sequelize.query(`DROP INDEX CONCURRENTLY IF EXISTS "${n}"`)};export{a as down,i as up};
6
9
  //# sourceMappingURL=004-entries-search-trgm-index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"004-entries-search-trgm-index.js","names":["up: MigrationFn<QueryInterface>","down: MigrationFn<QueryInterface>"],"sources":["../../../src/migrations/004-entries-search-trgm-index.ts"],"sourcesContent":["import type { MigrationFn } from 'umzug';\nimport type { QueryInterface } from 'sequelize';\nimport { entriesValuesTextExpression } from '../utils/constants';\n\n// Trigram index powering free-text search over custom_field_entries (AF-10932).\n// The indexed expression extracts VALUES only ('$.*') — key names can never match a search.\n// It must stay character-identical to the search helper's predicate, which is why both\n// sides build it from `entriesValuesTextExpression`.\n//\n// pg_trgm ships with Postgres itself (contrib) and is allowlisted on Cloud SQL; CREATE\n// EXTENSION is idempotent and fails loudly on servers lacking it.\n//\n// Plain CREATE INDEX (not CONCURRENTLY): umzug runs these non-transactionally at service\n// init, and a failed CONCURRENTLY build leaves an INVALID index that IF NOT EXISTS would\n// then skip forever. For very large existing tables the index can be pre-created manually\n// with CONCURRENTLY under the same name before deploying.\n\nexport const up: MigrationFn<QueryInterface> = async ({ context: queryInterface }) => {\n const { sequelize } = queryInterface;\n\n await sequelize.query('CREATE EXTENSION IF NOT EXISTS pg_trgm');\n\n await sequelize.query(`\n CREATE INDEX IF NOT EXISTS \"idx_cfe_values_trgm\"\n ON \"custom_field_entries\"\n USING gin ((${entriesValuesTextExpression('\"custom_fields\"')}) gin_trgm_ops)\n `);\n};\n\nexport const down: MigrationFn<QueryInterface> = async ({ context: queryInterface }) => {\n // The extension is intentionally kept — other database objects may depend on it.\n await queryInterface.sequelize.query('DROP INDEX IF EXISTS \"idx_cfe_values_trgm\"');\n};\n"],"mappings":"0EAiBA,MAAaA,EAAkC,MAAO,CAAE,QAAS,KAAqB,CACpF,GAAM,CAAE,aAAc,EAEtB,MAAM,EAAU,MAAM,yCAAyC,CAE/D,MAAM,EAAU,MAAM;;;kBAGN,EAA4B,kBAAkB,CAAC;IAC7D,EAGSC,EAAoC,MAAO,CAAE,QAAS,KAAqB,CAEtF,MAAM,EAAe,UAAU,MAAM,6CAA6C"}
1
+ {"version":3,"file":"004-entries-search-trgm-index.js","names":["up: MigrationFn<QueryInterface>","down: MigrationFn<QueryInterface>"],"sources":["../../../src/migrations/004-entries-search-trgm-index.ts"],"sourcesContent":["import type { MigrationFn } from 'umzug';\nimport { QueryTypes, type QueryInterface } from 'sequelize';\nimport { entriesValuesTextExpression } from '../utils/constants';\n\nconst INDEX_NAME = 'idx_cfe_search_trgm';\n\n// Trigram index for free-text search over custom_field_entries (AF-10932).\n\nconst isValid = async (sequelize: QueryInterface['sequelize']): Promise<boolean | undefined> => {\n const [index] = await sequelize.query(\n `SELECT i.indisvalid\n FROM pg_class c\n JOIN pg_index i ON i.indexrelid = c.oid\n WHERE c.relname = '${INDEX_NAME}'`,\n { type: QueryTypes.SELECT },\n ) as [{ indisvalid: boolean; } | undefined];\n return index?.indisvalid;\n};\n\nexport const up: MigrationFn<QueryInterface> = async ({ context: queryInterface }) => {\n const { sequelize } = queryInterface;\n\n await sequelize.query('CREATE EXTENSION IF NOT EXISTS pg_trgm');\n\n // A failed CONCURRENTLY build leaves an INVALID index that IF NOT EXISTS would skip forever,\n // so a retry would never rebuild it.\n if (await isValid(sequelize) === false) {\n await sequelize.query(`DROP INDEX CONCURRENTLY IF EXISTS \"${INDEX_NAME}\"`);\n }\n\n await sequelize.query(`\n CREATE INDEX CONCURRENTLY IF NOT EXISTS \"${INDEX_NAME}\"\n ON \"custom_field_entries\"\n USING gin ((${entriesValuesTextExpression('\"custom_fields\"')}) gin_trgm_ops)\n `);\n\n if (!await isValid(sequelize)) {\n throw new Error(`${INDEX_NAME} built INVALID — rerun the migration to rebuild it`);\n }\n\n // Without stats the planner won't choose an expression index.\n await sequelize.query('ANALYZE \"custom_field_entries\"');\n};\n\nexport const down: MigrationFn<QueryInterface> = async ({ context: queryInterface }) => {\n // Extension kept — other objects may depend on it.\n await queryInterface.sequelize.query(`DROP INDEX CONCURRENTLY IF EXISTS \"${INDEX_NAME}\"`);\n};\n"],"mappings":"iHAIA,MAAM,EAAa,sBAIb,EAAU,KAAO,IAAyE,CAC9F,GAAM,CAAC,GAAS,MAAM,EAAU,MAC9B;;;0BAGsB,EAAW,GACjC,CAAE,KAAM,EAAW,OAAQ,CAC5B,CACD,OAAO,GAAO,YAGHA,EAAkC,MAAO,CAAE,QAAS,KAAqB,CACpF,GAAM,CAAE,aAAc,EAgBtB,GAdA,MAAM,EAAU,MAAM,yCAAyC,CAI3D,MAAM,EAAQ,EAAU,GAAK,IAC/B,MAAM,EAAU,MAAM,sCAAsC,EAAW,GAAG,CAG5E,MAAM,EAAU,MAAM;+CACuB,EAAW;;kBAExC,EAA4B,kBAAkB,CAAC;IAC7D,CAEE,CAAC,MAAM,EAAQ,EAAU,CAC3B,MAAU,MAAM,GAAG,EAAW,oDAAoD,CAIpF,MAAM,EAAU,MAAM,iCAAiC,EAG5CC,EAAoC,MAAO,CAAE,QAAS,KAAqB,CAEtF,MAAM,EAAe,UAAU,MAAM,sCAAsC,EAAW,GAAG"}
@@ -1,2 +1,2 @@
1
- const e=require(`../../_virtual/rolldown_runtime.cjs`),t=require(`../utils/logger/index.cjs`),n=require(`../utils/custom-fields-mode.cjs`),r=require(`./001-create-core-tables.cjs`),i=require(`./002-create-custom-field-entries.cjs`),a=require(`./003-create-custom-field-model-type-map.cjs`),o=require(`./004-entries-search-trgm-index.cjs`);let s=require(`node:timers/promises`);const{Umzug:c,SequelizeStorage:l}=require(`umzug`),u=e=>{let{useModelTypeMapping:t=!1}=e,s=n.resolveCustomFieldsEntriesMode(e),c=n.writesEntries(s),l=n.readsEntries(s);return[{name:`001-create-core-tables`,up:r.up,down:r.down},...c?[{name:`002-create-custom-field-entries`,up:i.up,down:i.down}]:[],...t?[{name:`003-create-custom-field-model-type-map`,up:a.up,down:a.down}]:[],...l?[{name:`004-entries-search-trgm-index`,up:o.up,down:o.down}]:[]]},d=839274628,f=async(e,n={})=>{let{withLock:r=!0}=n,i=e.getQueryInterface(),a=new l({sequelize:e,tableName:`sadot_migrations`,modelName:`SadotMigrations`}),o=new c({migrations:u(n),context:i,storage:a,logger:{info:e=>t.default.info(`[sadot] ${JSON.stringify(e)}`),warn:e=>t.default.warn(`[sadot] ${JSON.stringify(e)}`),error:e=>t.default.error(`[sadot] ${JSON.stringify(e)}`),debug:e=>t.default.debug(`[sadot] ${JSON.stringify(e)}`)}}),f=await o.pending();if(f.length===0){t.default.info(`[sadot] no pending migrations, skipping`);return}if(t.default.info(`[sadot] pending migrations detected`,{pending:f.map(e=>e.name)}),!r){await o.up();return}if((e.options?.pool?.max??5)<2){t.default.info(`[sadot] connection pool too small for advisory lock, running migrations directly`),await o.up();return}t.default.info(`[sadot] acquiring advisory lock`);let p=await e.connectionManager.getConnection({type:`write`});try{let e=!1;for(;!e;)e=(await p.query(`SELECT pg_try_advisory_lock(${d}) AS acquired`)).rows[0].acquired,e||await(0,s.setTimeout)(1e3);t.default.info(`[sadot] advisory lock acquired`),(await o.pending()).length>0?await o.up():t.default.info(`[sadot] migrations already applied by another instance, skipping`)}finally{try{await p.query(`SELECT pg_advisory_unlock(${d})`),t.default.info(`[sadot] advisory lock released`)}finally{e.connectionManager.releaseConnection(p)}}};exports.runSadotMigrations=f;
1
+ const e=require(`../../_virtual/rolldown_runtime.cjs`),t=require(`../utils/logger/index.cjs`),n=require(`../utils/custom-fields-mode.cjs`),r=require(`./001-create-core-tables.cjs`),i=require(`./002-create-custom-field-entries.cjs`),a=require(`./003-create-custom-field-model-type-map.cjs`),o=require(`./004-entries-search-trgm-index.cjs`);let s=require(`node:timers/promises`);const{Umzug:c,SequelizeStorage:l}=require(`umzug`),u=e=>{let{useModelTypeMapping:t=!1}=e,s=n.resolveCustomFieldsEntriesMode(e),c=n.writesEntries(s),l=n.readsEntries(s)&&e.useEntriesSearchIndex===!0;return[{name:`001-create-core-tables`,up:r.up,down:r.down},...c?[{name:`002-create-custom-field-entries`,up:i.up,down:i.down}]:[],...t?[{name:`003-create-custom-field-model-type-map`,up:a.up,down:a.down}]:[],...l?[{name:`004-entries-search-trgm-index`,up:o.up,down:o.down}]:[]]},d=839274628,f=async(e,n={})=>{let{withLock:r=!0}=n,i=e.getQueryInterface(),a=new l({sequelize:e,tableName:`sadot_migrations`,modelName:`SadotMigrations`}),o=new c({migrations:u(n),context:i,storage:a,logger:{info:e=>t.default.info(`[sadot] ${JSON.stringify(e)}`),warn:e=>t.default.warn(`[sadot] ${JSON.stringify(e)}`),error:e=>t.default.error(`[sadot] ${JSON.stringify(e)}`),debug:e=>t.default.debug(`[sadot] ${JSON.stringify(e)}`)}}),f=await o.pending();if(f.length===0){t.default.info(`[sadot] no pending migrations, skipping`);return}if(t.default.info(`[sadot] pending migrations detected`,{pending:f.map(e=>e.name)}),!r){await o.up();return}if((e.options?.pool?.max??5)<2){t.default.info(`[sadot] connection pool too small for advisory lock, running migrations directly`),await o.up();return}t.default.info(`[sadot] acquiring advisory lock`);let p=await e.connectionManager.getConnection({type:`write`});try{let e=!1;for(;!e;)e=(await p.query(`SELECT pg_try_advisory_lock(${d}) AS acquired`)).rows[0].acquired,e||await(0,s.setTimeout)(1e3);t.default.info(`[sadot] advisory lock acquired`),(await o.pending()).length>0?await o.up():t.default.info(`[sadot] migrations already applied by another instance, skipping`)}finally{try{await p.query(`SELECT pg_advisory_unlock(${d})`),t.default.info(`[sadot] advisory lock released`)}finally{e.connectionManager.releaseConnection(p)}}};exports.runSadotMigrations=f;
2
2
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["resolveCustomFieldsEntriesMode","writesEntries","readsEntries","logger"],"sources":["../../../src/migrations/index.ts"],"sourcesContent":["import type { InputMigrations } from 'umzug';\nimport type { QueryInterface } from 'sequelize';\nimport type { Sequelize } from 'sequelize-typescript';\nimport { setTimeout } from 'node:timers/promises';\nimport logger from '../utils/logger';\nimport { type CustomFieldsEntriesModeOptions, readsEntries, resolveCustomFieldsEntriesMode, writesEntries } from '../utils/custom-fields-mode';\nimport * as migration001 from './001-create-core-tables';\nimport * as migration002 from './002-create-custom-field-entries';\nimport * as migration003 from './003-create-custom-field-model-type-map';\nimport * as migration004 from './004-entries-search-trgm-index';\n\n// umzug is CJS. Static ESM named imports break in vitest after vi.resetModules()\n// because Vite's SSR transform re-evaluates the module outside the inline bundle.\n// require() goes through Node's native CJS loader and is stable across resets.\n\n// eslint-disable-next-line import/no-commonjs\nconst { Umzug, SequelizeStorage } = require('umzug') as typeof import('umzug');\n\ninterface MigratorOptions extends CustomFieldsEntriesModeOptions {\n useModelTypeMapping?: boolean;\n withLock?: boolean;\n}\n\nconst buildMigrationList = (options: MigratorOptions): InputMigrations<QueryInterface> => {\n const { useModelTypeMapping = false } = options;\n // The table is needed as soon as anything writes to it, and every mode that reads\n // entries also writes them, so `writesEntries` is the widest gate. Keying this off\n // `readsEntries` would leave a WRITE_ONLY service inserting into a missing table.\n //\n // Migrations only need this boolean — WRITE_ONLY and READ_AND_WRITE are the same\n // answer here. The full mode is accepted anyway so a service passes one shared\n // constant to both this and useCustomFields(), rather than keeping a separate\n // migration-only boolean in sync with the runtime mode.\n const resolvedMode = resolveCustomFieldsEntriesMode(options);\n const shouldRunEntriesTableMigration = writesEntries(resolvedMode);\n // The search index (and pg_trgm) only serves services that READ entries — search is a read\n // path. WRITE_ONLY services keep dual-writing without paying the index maintenance cost;\n // a service flipping to READ_AND_WRITE later picks 004 up as pending on its next init.\n // readsEntries ⊆ writesEntries, so 002 (the table) always precedes 004.\n const shouldRunEntriesSearchIndexMigration = readsEntries(resolvedMode);\n return [\n { name: '001-create-core-tables', up: migration001.up, down: migration001.down },\n ...(shouldRunEntriesTableMigration ? [{ name: '002-create-custom-field-entries', up: migration002.up, down: migration002.down }] : []),\n ...(useModelTypeMapping ? [{ name: '003-create-custom-field-model-type-map', up: migration003.up, down: migration003.down }] : []),\n ...(shouldRunEntriesSearchIndexMigration ? [{ name: '004-entries-search-trgm-index', up: migration004.up, down: migration004.down }] : []),\n ];\n};\n\n// Stable advisory-lock ID for sadot migrations. Chosen arbitrarily; must not\n// collide with other advisory locks in the same database.\nconst SADOT_MIGRATION_LOCK_ID = 839274628;\n\n/**\n * Runs sadot migrations via umzug. All migrations use IF NOT EXISTS / IF EXISTS,\n * so they are fully idempotent — safe to run against both fresh and existing\n * databases (including DBs previously managed by the legacy sync() path).\n *\n * When `withLock: true`, a PostgreSQL advisory lock is acquired before running\n * migrations so that when multiple pods start concurrently only one of them\n * executes the migrations. The remaining pods block until the lock is released,\n * then proceed — Umzug will see the migrations have already been applied and\n * skip them.\n *\n * @param options - Migration execution options.\n * @param options.withLock - Whether to acquire a PostgreSQL advisory lock before running the migrations. Defaults to true.\n * @returns A promise that resolves when the migrations have completed. \n */\nconst runSadotMigrations = async (sequelize: Sequelize, options: MigratorOptions = {}): Promise<void> => {\n // Need to add a new migration? full migration guide here; https://www.notion.so/autofleet/How-to-Add-a-New-Migration-to-Sadot-36db603f40b98023b239d3c61aa22b00\n const { withLock = true } = options;\n const queryInterface = sequelize.getQueryInterface() as unknown as QueryInterface;\n const storage = new SequelizeStorage({ sequelize: sequelize as never, tableName: 'sadot_migrations', modelName: 'SadotMigrations' });\n\n const umzugLogger = {\n info: (msg: Record<string, unknown>) => logger.info(`[sadot] ${JSON.stringify(msg)}`),\n warn: (msg: Record<string, unknown>) => logger.warn(`[sadot] ${JSON.stringify(msg)}`),\n error: (msg: Record<string, unknown>) => logger.error(`[sadot] ${JSON.stringify(msg)}`),\n debug: (msg: Record<string, unknown>) => logger.debug(`[sadot] ${JSON.stringify(msg)}`),\n };\n\n const migrator = new Umzug<QueryInterface>({\n migrations: buildMigrationList(options),\n context: queryInterface,\n storage,\n logger: umzugLogger,\n });\n\n const pending = await migrator.pending();\n if (pending.length === 0) {\n logger.info('[sadot] no pending migrations, skipping');\n return;\n }\n\n logger.info('[sadot] pending migrations detected', { pending: pending.map(m => m.name) });\n\n if (!withLock) {\n await migrator.up();\n return;\n }\n\n // The advisory lock holds a connection for its entire duration while\n // migrator.up() needs additional pool connections for queries. With\n // pool.max < 2 (common in test environments) this deadlocks.\n // Migrations are idempotent (IF NOT EXISTS / IF EXISTS) so running\n // without the lock is safe — just potentially duplicated work.\n const poolMax = (sequelize.options as any)?.pool?.max ?? 5;\n if (poolMax < 2) {\n logger.info('[sadot] connection pool too small for advisory lock, running migrations directly');\n await migrator.up();\n return;\n }\n\n // Use a dedicated raw connection (not a transaction) to hold the advisory lock.\n // A transaction connection sitting idle while migrator.up() runs on other pool\n // connections would be killed by idle_in_transaction_session_timeout, releasing\n // the lock prematurely and allowing a second replica to start migrations concurrently.\n logger.info('[sadot] acquiring advisory lock');\n const lockConnection = await (sequelize.connectionManager as any).getConnection({ type: 'write' });\n try {\n // Poll with pg_try_advisory_lock so the connection stays idle between attempts rather\n // than holding an active statement for the full wait duration. DDL operations that track\n // active backends between internal phases (e.g. CREATE INDEX CONCURRENTLY, VACUUM,\n // REINDEX CONCURRENTLY) wait for all active statements to finish before proceeding —\n // a long-blocking lock attempt would stall them for the entire wait.\n let lockAcquired = false;\n while (!lockAcquired) {\n const result = await lockConnection.query(`SELECT pg_try_advisory_lock(${SADOT_MIGRATION_LOCK_ID}) AS acquired`);\n lockAcquired = result.rows[0].acquired;\n if (!lockAcquired) await setTimeout(1000);\n }\n logger.info('[sadot] advisory lock acquired');\n\n // Re-check after acquiring the lock — another pod may have already\n // applied the migrations while we were waiting.\n const stillPending = await migrator.pending();\n if (stillPending.length > 0) {\n await migrator.up();\n } else {\n logger.info('[sadot] migrations already applied by another instance, skipping');\n }\n } finally {\n try {\n await lockConnection.query(`SELECT pg_advisory_unlock(${SADOT_MIGRATION_LOCK_ID})`);\n logger.info('[sadot] advisory lock released');\n } finally {\n (sequelize.connectionManager as any).releaseConnection(lockConnection);\n }\n }\n};\n\nexport { runSadotMigrations };\n"],"mappings":"yXAgBA,KAAM,CAAE,QAAO,oBAAqB,QAAQ,QAAQ,CAO9C,EAAsB,GAA8D,CACxF,GAAM,CAAE,sBAAsB,IAAU,EASlC,EAAeA,EAAAA,+BAA+B,EAAQ,CACtD,EAAiCC,EAAAA,cAAc,EAAa,CAK5D,EAAuCC,EAAAA,aAAa,EAAa,CACvE,MAAO,CACL,CAAE,KAAM,yBAA0B,GAAA,EAAA,GAAqB,KAAA,EAAA,KAAyB,CAChF,GAAI,EAAiC,CAAC,CAAE,KAAM,kCAAmC,GAAA,EAAA,GAAqB,KAAA,EAAA,KAAyB,CAAC,CAAG,EAAE,CACrI,GAAI,EAAsB,CAAC,CAAE,KAAM,yCAA0C,GAAA,EAAA,GAAqB,KAAA,EAAA,KAAyB,CAAC,CAAG,EAAE,CACjI,GAAI,EAAuC,CAAC,CAAE,KAAM,gCAAiC,GAAA,EAAA,GAAqB,KAAA,EAAA,KAAyB,CAAC,CAAG,EAAE,CAC1I,EAKG,EAA0B,UAiB1B,EAAqB,MAAO,EAAsB,EAA2B,EAAE,GAAoB,CAEvG,GAAM,CAAE,WAAW,IAAS,EACtB,EAAiB,EAAU,mBAAmB,CAC9C,EAAU,IAAI,EAAiB,CAAa,YAAoB,UAAW,mBAAoB,UAAW,kBAAmB,CAAC,CAS9H,EAAW,IAAI,EAAsB,CACzC,WAAY,EAAmB,EAAQ,CACvC,QAAS,EACT,UACA,OAXkB,CAClB,KAAO,GAAiCC,EAAAA,QAAO,KAAK,WAAW,KAAK,UAAU,EAAI,GAAG,CACrF,KAAO,GAAiCA,EAAAA,QAAO,KAAK,WAAW,KAAK,UAAU,EAAI,GAAG,CACrF,MAAQ,GAAiCA,EAAAA,QAAO,MAAM,WAAW,KAAK,UAAU,EAAI,GAAG,CACvF,MAAQ,GAAiCA,EAAAA,QAAO,MAAM,WAAW,KAAK,UAAU,EAAI,GAAG,CACxF,CAOA,CAAC,CAEI,EAAU,MAAM,EAAS,SAAS,CACxC,GAAI,EAAQ,SAAW,EAAG,CACxB,EAAA,QAAO,KAAK,0CAA0C,CACtD,OAKF,GAFA,EAAA,QAAO,KAAK,sCAAuC,CAAE,QAAS,EAAQ,IAAI,GAAK,EAAE,KAAK,CAAE,CAAC,CAErF,CAAC,EAAU,CACb,MAAM,EAAS,IAAI,CACnB,OASF,IADiB,EAAU,SAAiB,MAAM,KAAO,GAC3C,EAAG,CACf,EAAA,QAAO,KAAK,mFAAmF,CAC/F,MAAM,EAAS,IAAI,CACnB,OAOF,EAAA,QAAO,KAAK,kCAAkC,CAC9C,IAAM,EAAiB,MAAO,EAAU,kBAA0B,cAAc,CAAE,KAAM,QAAS,CAAC,CAClG,GAAI,CAMF,IAAI,EAAe,GACnB,KAAO,CAAC,GAEN,GADe,MAAM,EAAe,MAAM,+BAA+B,EAAwB,eAAe,EAC1F,KAAK,GAAG,SACzB,GAAc,MAAA,EAAA,EAAA,YAAiB,IAAK,CAE3C,EAAA,QAAO,KAAK,iCAAiC,EAIxB,MAAM,EAAS,SAAS,EAC5B,OAAS,EACxB,MAAM,EAAS,IAAI,CAEnB,EAAA,QAAO,KAAK,mEAAmE,QAEzE,CACR,GAAI,CACF,MAAM,EAAe,MAAM,6BAA6B,EAAwB,GAAG,CACnF,EAAA,QAAO,KAAK,iCAAiC,QACrC,CACP,EAAU,kBAA0B,kBAAkB,EAAe"}
1
+ {"version":3,"file":"index.cjs","names":["resolveCustomFieldsEntriesMode","writesEntries","readsEntries","logger"],"sources":["../../../src/migrations/index.ts"],"sourcesContent":["import type { InputMigrations } from 'umzug';\nimport type { QueryInterface } from 'sequelize';\nimport type { Sequelize } from 'sequelize-typescript';\nimport { setTimeout } from 'node:timers/promises';\nimport logger from '../utils/logger';\nimport { type CustomFieldsEntriesModeOptions, readsEntries, resolveCustomFieldsEntriesMode, writesEntries } from '../utils/custom-fields-mode';\nimport * as migration001 from './001-create-core-tables';\nimport * as migration002 from './002-create-custom-field-entries';\nimport * as migration003 from './003-create-custom-field-model-type-map';\nimport * as migration004 from './004-entries-search-trgm-index';\n\n// umzug is CJS. Static ESM named imports break in vitest after vi.resetModules()\n// because Vite's SSR transform re-evaluates the module outside the inline bundle.\n// require() goes through Node's native CJS loader and is stable across resets.\n\n// eslint-disable-next-line import/no-commonjs\nconst { Umzug, SequelizeStorage } = require('umzug') as typeof import('umzug');\n\ninterface MigratorOptions extends CustomFieldsEntriesModeOptions {\n useModelTypeMapping?: boolean;\n useEntriesSearchIndex?: boolean;\n withLock?: boolean;\n}\n\nconst buildMigrationList = (options: MigratorOptions): InputMigrations<QueryInterface> => {\n const { useModelTypeMapping = false } = options;\n // The table is needed as soon as anything writes to it, and every mode that reads\n // entries also writes them, so `writesEntries` is the widest gate. Keying this off\n // `readsEntries` would leave a WRITE_ONLY service inserting into a missing table.\n //\n // Migrations only need this boolean — WRITE_ONLY and READ_AND_WRITE are the same\n // answer here. The full mode is accepted anyway so a service passes one shared\n // constant to both this and useCustomFields(), rather than keeping a separate\n // migration-only boolean in sync with the runtime mode.\n const resolvedMode = resolveCustomFieldsEntriesMode(options);\n const shouldRunEntriesTableMigration = writesEntries(resolvedMode);\n // The search index is only needed for READ_AND_WRITE services that currently do expose custom field search term integration (i.e vehicle-ms)\n const shouldRunEntriesSearchIndexMigration = readsEntries(resolvedMode) && options.useEntriesSearchIndex === true;\n return [\n { name: '001-create-core-tables', up: migration001.up, down: migration001.down },\n ...(shouldRunEntriesTableMigration ? [{ name: '002-create-custom-field-entries', up: migration002.up, down: migration002.down }] : []),\n ...(useModelTypeMapping ? [{ name: '003-create-custom-field-model-type-map', up: migration003.up, down: migration003.down }] : []),\n ...(shouldRunEntriesSearchIndexMigration ? [{ name: '004-entries-search-trgm-index', up: migration004.up, down: migration004.down }] : []),\n ];\n};\n\n// Stable advisory-lock ID for sadot migrations. Chosen arbitrarily; must not\n// collide with other advisory locks in the same database.\nconst SADOT_MIGRATION_LOCK_ID = 839274628;\n\n/**\n * Runs sadot migrations via umzug. All migrations use IF NOT EXISTS / IF EXISTS,\n * so they are fully idempotent — safe to run against both fresh and existing\n * databases (including DBs previously managed by the legacy sync() path).\n *\n * When `withLock: true`, a PostgreSQL advisory lock is acquired before running\n * migrations so that when multiple pods start concurrently only one of them\n * executes the migrations. The remaining pods block until the lock is released,\n * then proceed — Umzug will see the migrations have already been applied and\n * skip them.\n *\n * @param options - Migration execution options.\n * @param options.withLock - Whether to acquire a PostgreSQL advisory lock before running the migrations. Defaults to true.\n * @returns A promise that resolves when the migrations have completed.\n */\nconst runSadotMigrations = async (sequelize: Sequelize, options: MigratorOptions = {}): Promise<void> => {\n // Need to add a new migration? full migration guide here; https://www.notion.so/autofleet/How-to-Add-a-New-Migration-to-Sadot-36db603f40b98023b239d3c61aa22b00\n const { withLock = true } = options;\n const queryInterface = sequelize.getQueryInterface() as unknown as QueryInterface;\n const storage = new SequelizeStorage({ sequelize: sequelize as never, tableName: 'sadot_migrations', modelName: 'SadotMigrations' });\n\n const umzugLogger = {\n info: (msg: Record<string, unknown>) => logger.info(`[sadot] ${JSON.stringify(msg)}`),\n warn: (msg: Record<string, unknown>) => logger.warn(`[sadot] ${JSON.stringify(msg)}`),\n error: (msg: Record<string, unknown>) => logger.error(`[sadot] ${JSON.stringify(msg)}`),\n debug: (msg: Record<string, unknown>) => logger.debug(`[sadot] ${JSON.stringify(msg)}`),\n };\n\n const migrator = new Umzug<QueryInterface>({\n migrations: buildMigrationList(options),\n context: queryInterface,\n storage,\n logger: umzugLogger,\n });\n\n const pending = await migrator.pending();\n if (pending.length === 0) {\n logger.info('[sadot] no pending migrations, skipping');\n return;\n }\n\n logger.info('[sadot] pending migrations detected', { pending: pending.map(m => m.name) });\n\n if (!withLock) {\n await migrator.up();\n return;\n }\n\n // The advisory lock holds a connection for its entire duration while\n // migrator.up() needs additional pool connections for queries. With\n // pool.max < 2 (common in test environments) this deadlocks.\n // Migrations are idempotent (IF NOT EXISTS / IF EXISTS) so running\n // without the lock is safe — just potentially duplicated work.\n const poolMax = (sequelize.options as any)?.pool?.max ?? 5;\n if (poolMax < 2) {\n logger.info('[sadot] connection pool too small for advisory lock, running migrations directly');\n await migrator.up();\n return;\n }\n\n // Use a dedicated raw connection (not a transaction) to hold the advisory lock.\n // A transaction connection sitting idle while migrator.up() runs on other pool\n // connections would be killed by idle_in_transaction_session_timeout, releasing\n // the lock prematurely and allowing a second replica to start migrations concurrently.\n logger.info('[sadot] acquiring advisory lock');\n const lockConnection = await (sequelize.connectionManager as any).getConnection({ type: 'write' });\n try {\n // Poll with pg_try_advisory_lock so the connection stays idle between attempts rather\n // than holding an active statement for the full wait duration. DDL operations that track\n // active backends between internal phases (e.g. CREATE INDEX CONCURRENTLY, VACUUM,\n // REINDEX CONCURRENTLY) wait for all active statements to finish before proceeding —\n // a long-blocking lock attempt would stall them for the entire wait.\n let lockAcquired = false;\n while (!lockAcquired) {\n const result = await lockConnection.query(`SELECT pg_try_advisory_lock(${SADOT_MIGRATION_LOCK_ID}) AS acquired`);\n lockAcquired = result.rows[0].acquired;\n if (!lockAcquired) await setTimeout(1000);\n }\n logger.info('[sadot] advisory lock acquired');\n\n // Re-check after acquiring the lock — another pod may have already\n // applied the migrations while we were waiting.\n const stillPending = await migrator.pending();\n if (stillPending.length > 0) {\n await migrator.up();\n } else {\n logger.info('[sadot] migrations already applied by another instance, skipping');\n }\n } finally {\n try {\n await lockConnection.query(`SELECT pg_advisory_unlock(${SADOT_MIGRATION_LOCK_ID})`);\n logger.info('[sadot] advisory lock released');\n } finally {\n (sequelize.connectionManager as any).releaseConnection(lockConnection);\n }\n }\n};\n\nexport { runSadotMigrations };\n"],"mappings":"yXAgBA,KAAM,CAAE,QAAO,oBAAqB,QAAQ,QAAQ,CAQ9C,EAAsB,GAA8D,CACxF,GAAM,CAAE,sBAAsB,IAAU,EASlC,EAAeA,EAAAA,+BAA+B,EAAQ,CACtD,EAAiCC,EAAAA,cAAc,EAAa,CAE5D,EAAuCC,EAAAA,aAAa,EAAa,EAAI,EAAQ,wBAA0B,GAC7G,MAAO,CACL,CAAE,KAAM,yBAA0B,GAAA,EAAA,GAAqB,KAAA,EAAA,KAAyB,CAChF,GAAI,EAAiC,CAAC,CAAE,KAAM,kCAAmC,GAAA,EAAA,GAAqB,KAAA,EAAA,KAAyB,CAAC,CAAG,EAAE,CACrI,GAAI,EAAsB,CAAC,CAAE,KAAM,yCAA0C,GAAA,EAAA,GAAqB,KAAA,EAAA,KAAyB,CAAC,CAAG,EAAE,CACjI,GAAI,EAAuC,CAAC,CAAE,KAAM,gCAAiC,GAAA,EAAA,GAAqB,KAAA,EAAA,KAAyB,CAAC,CAAG,EAAE,CAC1I,EAKG,EAA0B,UAiB1B,EAAqB,MAAO,EAAsB,EAA2B,EAAE,GAAoB,CAEvG,GAAM,CAAE,WAAW,IAAS,EACtB,EAAiB,EAAU,mBAAmB,CAC9C,EAAU,IAAI,EAAiB,CAAa,YAAoB,UAAW,mBAAoB,UAAW,kBAAmB,CAAC,CAS9H,EAAW,IAAI,EAAsB,CACzC,WAAY,EAAmB,EAAQ,CACvC,QAAS,EACT,UACA,OAXkB,CAClB,KAAO,GAAiCC,EAAAA,QAAO,KAAK,WAAW,KAAK,UAAU,EAAI,GAAG,CACrF,KAAO,GAAiCA,EAAAA,QAAO,KAAK,WAAW,KAAK,UAAU,EAAI,GAAG,CACrF,MAAQ,GAAiCA,EAAAA,QAAO,MAAM,WAAW,KAAK,UAAU,EAAI,GAAG,CACvF,MAAQ,GAAiCA,EAAAA,QAAO,MAAM,WAAW,KAAK,UAAU,EAAI,GAAG,CACxF,CAOA,CAAC,CAEI,EAAU,MAAM,EAAS,SAAS,CACxC,GAAI,EAAQ,SAAW,EAAG,CACxB,EAAA,QAAO,KAAK,0CAA0C,CACtD,OAKF,GAFA,EAAA,QAAO,KAAK,sCAAuC,CAAE,QAAS,EAAQ,IAAI,GAAK,EAAE,KAAK,CAAE,CAAC,CAErF,CAAC,EAAU,CACb,MAAM,EAAS,IAAI,CACnB,OASF,IADiB,EAAU,SAAiB,MAAM,KAAO,GAC3C,EAAG,CACf,EAAA,QAAO,KAAK,mFAAmF,CAC/F,MAAM,EAAS,IAAI,CACnB,OAOF,EAAA,QAAO,KAAK,kCAAkC,CAC9C,IAAM,EAAiB,MAAO,EAAU,kBAA0B,cAAc,CAAE,KAAM,QAAS,CAAC,CAClG,GAAI,CAMF,IAAI,EAAe,GACnB,KAAO,CAAC,GAEN,GADe,MAAM,EAAe,MAAM,+BAA+B,EAAwB,eAAe,EAC1F,KAAK,GAAG,SACzB,GAAc,MAAA,EAAA,EAAA,YAAiB,IAAK,CAE3C,EAAA,QAAO,KAAK,iCAAiC,EAIxB,MAAM,EAAS,SAAS,EAC5B,OAAS,EACxB,MAAM,EAAS,IAAI,CAEnB,EAAA,QAAO,KAAK,mEAAmE,QAEzE,CACR,GAAI,CACF,MAAM,EAAe,MAAM,6BAA6B,EAAwB,GAAG,CACnF,EAAA,QAAO,KAAK,iCAAiC,QACrC,CACP,EAAU,kBAA0B,kBAAkB,EAAe"}
@@ -4,6 +4,7 @@ import { Sequelize } from "sequelize-typescript";
4
4
  //#region src/migrations/index.d.ts
5
5
  interface MigratorOptions extends CustomFieldsEntriesModeOptions {
6
6
  useModelTypeMapping?: boolean;
7
+ useEntriesSearchIndex?: boolean;
7
8
  withLock?: boolean;
8
9
  }
9
10
  /**
@@ -19,7 +20,7 @@ interface MigratorOptions extends CustomFieldsEntriesModeOptions {
19
20
  *
20
21
  * @param options - Migration execution options.
21
22
  * @param options.withLock - Whether to acquire a PostgreSQL advisory lock before running the migrations. Defaults to true.
22
- * @returns A promise that resolves when the migrations have completed.
23
+ * @returns A promise that resolves when the migrations have completed.
23
24
  */
24
25
  declare const runSadotMigrations: (sequelize: Sequelize, options?: MigratorOptions) => Promise<void>;
25
26
  //#endregion
@@ -1,2 +1,2 @@
1
- import{__require as e}from"../../_virtual/rolldown_runtime.js";import t from"../utils/logger/index.js";import{readsEntries as n,resolveCustomFieldsEntriesMode as r,writesEntries as i}from"../utils/custom-fields-mode.js";import{down as a,up as o}from"./001-create-core-tables.js";import{down as s,up as c}from"./002-create-custom-field-entries.js";import{down as l,up as u}from"./003-create-custom-field-model-type-map.js";import{down as d,up as f}from"./004-entries-search-trgm-index.js";import{setTimeout as p}from"node:timers/promises";const{Umzug:m,SequelizeStorage:h}=e(`umzug`),g=e=>{let{useModelTypeMapping:t=!1}=e,p=r(e),m=i(p),h=n(p);return[{name:`001-create-core-tables`,up:o,down:a},...m?[{name:`002-create-custom-field-entries`,up:c,down:s}]:[],...t?[{name:`003-create-custom-field-model-type-map`,up:u,down:l}]:[],...h?[{name:`004-entries-search-trgm-index`,up:f,down:d}]:[]]},_=839274628,v=async(e,n={})=>{let{withLock:r=!0}=n,i=e.getQueryInterface(),a=new h({sequelize:e,tableName:`sadot_migrations`,modelName:`SadotMigrations`}),o=new m({migrations:g(n),context:i,storage:a,logger:{info:e=>t.info(`[sadot] ${JSON.stringify(e)}`),warn:e=>t.warn(`[sadot] ${JSON.stringify(e)}`),error:e=>t.error(`[sadot] ${JSON.stringify(e)}`),debug:e=>t.debug(`[sadot] ${JSON.stringify(e)}`)}}),s=await o.pending();if(s.length===0){t.info(`[sadot] no pending migrations, skipping`);return}if(t.info(`[sadot] pending migrations detected`,{pending:s.map(e=>e.name)}),!r){await o.up();return}if((e.options?.pool?.max??5)<2){t.info(`[sadot] connection pool too small for advisory lock, running migrations directly`),await o.up();return}t.info(`[sadot] acquiring advisory lock`);let c=await e.connectionManager.getConnection({type:`write`});try{let e=!1;for(;!e;)e=(await c.query(`SELECT pg_try_advisory_lock(${_}) AS acquired`)).rows[0].acquired,e||await p(1e3);t.info(`[sadot] advisory lock acquired`),(await o.pending()).length>0?await o.up():t.info(`[sadot] migrations already applied by another instance, skipping`)}finally{try{await c.query(`SELECT pg_advisory_unlock(${_})`),t.info(`[sadot] advisory lock released`)}finally{e.connectionManager.releaseConnection(c)}}};export{v as runSadotMigrations};
1
+ import{__require as e}from"../../_virtual/rolldown_runtime.js";import t from"../utils/logger/index.js";import{readsEntries as n,resolveCustomFieldsEntriesMode as r,writesEntries as i}from"../utils/custom-fields-mode.js";import{down as a,up as o}from"./001-create-core-tables.js";import{down as s,up as c}from"./002-create-custom-field-entries.js";import{down as l,up as u}from"./003-create-custom-field-model-type-map.js";import{down as d,up as f}from"./004-entries-search-trgm-index.js";import{setTimeout as p}from"node:timers/promises";const{Umzug:m,SequelizeStorage:h}=e(`umzug`),g=e=>{let{useModelTypeMapping:t=!1}=e,p=r(e),m=i(p),h=n(p)&&e.useEntriesSearchIndex===!0;return[{name:`001-create-core-tables`,up:o,down:a},...m?[{name:`002-create-custom-field-entries`,up:c,down:s}]:[],...t?[{name:`003-create-custom-field-model-type-map`,up:u,down:l}]:[],...h?[{name:`004-entries-search-trgm-index`,up:f,down:d}]:[]]},_=839274628,v=async(e,n={})=>{let{withLock:r=!0}=n,i=e.getQueryInterface(),a=new h({sequelize:e,tableName:`sadot_migrations`,modelName:`SadotMigrations`}),o=new m({migrations:g(n),context:i,storage:a,logger:{info:e=>t.info(`[sadot] ${JSON.stringify(e)}`),warn:e=>t.warn(`[sadot] ${JSON.stringify(e)}`),error:e=>t.error(`[sadot] ${JSON.stringify(e)}`),debug:e=>t.debug(`[sadot] ${JSON.stringify(e)}`)}}),s=await o.pending();if(s.length===0){t.info(`[sadot] no pending migrations, skipping`);return}if(t.info(`[sadot] pending migrations detected`,{pending:s.map(e=>e.name)}),!r){await o.up();return}if((e.options?.pool?.max??5)<2){t.info(`[sadot] connection pool too small for advisory lock, running migrations directly`),await o.up();return}t.info(`[sadot] acquiring advisory lock`);let c=await e.connectionManager.getConnection({type:`write`});try{let e=!1;for(;!e;)e=(await c.query(`SELECT pg_try_advisory_lock(${_}) AS acquired`)).rows[0].acquired,e||await p(1e3);t.info(`[sadot] advisory lock acquired`),(await o.pending()).length>0?await o.up():t.info(`[sadot] migrations already applied by another instance, skipping`)}finally{try{await c.query(`SELECT pg_advisory_unlock(${_})`),t.info(`[sadot] advisory lock released`)}finally{e.connectionManager.releaseConnection(c)}}};export{v as runSadotMigrations};
2
2
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["migration001.up","migration001.down","migration002.up","migration002.down","migration003.up","migration003.down","migration004.up","migration004.down","logger"],"sources":["../../../src/migrations/index.ts"],"sourcesContent":["import type { InputMigrations } from 'umzug';\nimport type { QueryInterface } from 'sequelize';\nimport type { Sequelize } from 'sequelize-typescript';\nimport { setTimeout } from 'node:timers/promises';\nimport logger from '../utils/logger';\nimport { type CustomFieldsEntriesModeOptions, readsEntries, resolveCustomFieldsEntriesMode, writesEntries } from '../utils/custom-fields-mode';\nimport * as migration001 from './001-create-core-tables';\nimport * as migration002 from './002-create-custom-field-entries';\nimport * as migration003 from './003-create-custom-field-model-type-map';\nimport * as migration004 from './004-entries-search-trgm-index';\n\n// umzug is CJS. Static ESM named imports break in vitest after vi.resetModules()\n// because Vite's SSR transform re-evaluates the module outside the inline bundle.\n// require() goes through Node's native CJS loader and is stable across resets.\n\n// eslint-disable-next-line import/no-commonjs\nconst { Umzug, SequelizeStorage } = require('umzug') as typeof import('umzug');\n\ninterface MigratorOptions extends CustomFieldsEntriesModeOptions {\n useModelTypeMapping?: boolean;\n withLock?: boolean;\n}\n\nconst buildMigrationList = (options: MigratorOptions): InputMigrations<QueryInterface> => {\n const { useModelTypeMapping = false } = options;\n // The table is needed as soon as anything writes to it, and every mode that reads\n // entries also writes them, so `writesEntries` is the widest gate. Keying this off\n // `readsEntries` would leave a WRITE_ONLY service inserting into a missing table.\n //\n // Migrations only need this boolean — WRITE_ONLY and READ_AND_WRITE are the same\n // answer here. The full mode is accepted anyway so a service passes one shared\n // constant to both this and useCustomFields(), rather than keeping a separate\n // migration-only boolean in sync with the runtime mode.\n const resolvedMode = resolveCustomFieldsEntriesMode(options);\n const shouldRunEntriesTableMigration = writesEntries(resolvedMode);\n // The search index (and pg_trgm) only serves services that READ entries — search is a read\n // path. WRITE_ONLY services keep dual-writing without paying the index maintenance cost;\n // a service flipping to READ_AND_WRITE later picks 004 up as pending on its next init.\n // readsEntries ⊆ writesEntries, so 002 (the table) always precedes 004.\n const shouldRunEntriesSearchIndexMigration = readsEntries(resolvedMode);\n return [\n { name: '001-create-core-tables', up: migration001.up, down: migration001.down },\n ...(shouldRunEntriesTableMigration ? [{ name: '002-create-custom-field-entries', up: migration002.up, down: migration002.down }] : []),\n ...(useModelTypeMapping ? [{ name: '003-create-custom-field-model-type-map', up: migration003.up, down: migration003.down }] : []),\n ...(shouldRunEntriesSearchIndexMigration ? [{ name: '004-entries-search-trgm-index', up: migration004.up, down: migration004.down }] : []),\n ];\n};\n\n// Stable advisory-lock ID for sadot migrations. Chosen arbitrarily; must not\n// collide with other advisory locks in the same database.\nconst SADOT_MIGRATION_LOCK_ID = 839274628;\n\n/**\n * Runs sadot migrations via umzug. All migrations use IF NOT EXISTS / IF EXISTS,\n * so they are fully idempotent — safe to run against both fresh and existing\n * databases (including DBs previously managed by the legacy sync() path).\n *\n * When `withLock: true`, a PostgreSQL advisory lock is acquired before running\n * migrations so that when multiple pods start concurrently only one of them\n * executes the migrations. The remaining pods block until the lock is released,\n * then proceed — Umzug will see the migrations have already been applied and\n * skip them.\n *\n * @param options - Migration execution options.\n * @param options.withLock - Whether to acquire a PostgreSQL advisory lock before running the migrations. Defaults to true.\n * @returns A promise that resolves when the migrations have completed. \n */\nconst runSadotMigrations = async (sequelize: Sequelize, options: MigratorOptions = {}): Promise<void> => {\n // Need to add a new migration? full migration guide here; https://www.notion.so/autofleet/How-to-Add-a-New-Migration-to-Sadot-36db603f40b98023b239d3c61aa22b00\n const { withLock = true } = options;\n const queryInterface = sequelize.getQueryInterface() as unknown as QueryInterface;\n const storage = new SequelizeStorage({ sequelize: sequelize as never, tableName: 'sadot_migrations', modelName: 'SadotMigrations' });\n\n const umzugLogger = {\n info: (msg: Record<string, unknown>) => logger.info(`[sadot] ${JSON.stringify(msg)}`),\n warn: (msg: Record<string, unknown>) => logger.warn(`[sadot] ${JSON.stringify(msg)}`),\n error: (msg: Record<string, unknown>) => logger.error(`[sadot] ${JSON.stringify(msg)}`),\n debug: (msg: Record<string, unknown>) => logger.debug(`[sadot] ${JSON.stringify(msg)}`),\n };\n\n const migrator = new Umzug<QueryInterface>({\n migrations: buildMigrationList(options),\n context: queryInterface,\n storage,\n logger: umzugLogger,\n });\n\n const pending = await migrator.pending();\n if (pending.length === 0) {\n logger.info('[sadot] no pending migrations, skipping');\n return;\n }\n\n logger.info('[sadot] pending migrations detected', { pending: pending.map(m => m.name) });\n\n if (!withLock) {\n await migrator.up();\n return;\n }\n\n // The advisory lock holds a connection for its entire duration while\n // migrator.up() needs additional pool connections for queries. With\n // pool.max < 2 (common in test environments) this deadlocks.\n // Migrations are idempotent (IF NOT EXISTS / IF EXISTS) so running\n // without the lock is safe — just potentially duplicated work.\n const poolMax = (sequelize.options as any)?.pool?.max ?? 5;\n if (poolMax < 2) {\n logger.info('[sadot] connection pool too small for advisory lock, running migrations directly');\n await migrator.up();\n return;\n }\n\n // Use a dedicated raw connection (not a transaction) to hold the advisory lock.\n // A transaction connection sitting idle while migrator.up() runs on other pool\n // connections would be killed by idle_in_transaction_session_timeout, releasing\n // the lock prematurely and allowing a second replica to start migrations concurrently.\n logger.info('[sadot] acquiring advisory lock');\n const lockConnection = await (sequelize.connectionManager as any).getConnection({ type: 'write' });\n try {\n // Poll with pg_try_advisory_lock so the connection stays idle between attempts rather\n // than holding an active statement for the full wait duration. DDL operations that track\n // active backends between internal phases (e.g. CREATE INDEX CONCURRENTLY, VACUUM,\n // REINDEX CONCURRENTLY) wait for all active statements to finish before proceeding —\n // a long-blocking lock attempt would stall them for the entire wait.\n let lockAcquired = false;\n while (!lockAcquired) {\n const result = await lockConnection.query(`SELECT pg_try_advisory_lock(${SADOT_MIGRATION_LOCK_ID}) AS acquired`);\n lockAcquired = result.rows[0].acquired;\n if (!lockAcquired) await setTimeout(1000);\n }\n logger.info('[sadot] advisory lock acquired');\n\n // Re-check after acquiring the lock — another pod may have already\n // applied the migrations while we were waiting.\n const stillPending = await migrator.pending();\n if (stillPending.length > 0) {\n await migrator.up();\n } else {\n logger.info('[sadot] migrations already applied by another instance, skipping');\n }\n } finally {\n try {\n await lockConnection.query(`SELECT pg_advisory_unlock(${SADOT_MIGRATION_LOCK_ID})`);\n logger.info('[sadot] advisory lock released');\n } finally {\n (sequelize.connectionManager as any).releaseConnection(lockConnection);\n }\n }\n};\n\nexport { runSadotMigrations };\n"],"mappings":"0hBAgBA,KAAM,CAAE,QAAO,oBAAA,EAA6B,QAAQ,CAO9C,EAAsB,GAA8D,CACxF,GAAM,CAAE,sBAAsB,IAAU,EASlC,EAAe,EAA+B,EAAQ,CACtD,EAAiC,EAAc,EAAa,CAK5D,EAAuC,EAAa,EAAa,CACvE,MAAO,CACL,CAAE,KAAM,yBAA8BA,KAAuBC,OAAmB,CAChF,GAAI,EAAiC,CAAC,CAAE,KAAM,kCAAmC,GAAIC,EAAiB,KAAMC,EAAmB,CAAC,CAAG,EAAE,CACrI,GAAI,EAAsB,CAAC,CAAE,KAAM,yCAA0C,GAAIC,EAAiB,KAAMC,EAAmB,CAAC,CAAG,EAAE,CACjI,GAAI,EAAuC,CAAC,CAAE,KAAM,gCAAiC,GAAIC,EAAiB,KAAMC,EAAmB,CAAC,CAAG,EAAE,CAC1I,EAKG,EAA0B,UAiB1B,EAAqB,MAAO,EAAsB,EAA2B,EAAE,GAAoB,CAEvG,GAAM,CAAE,WAAW,IAAS,EACtB,EAAiB,EAAU,mBAAmB,CAC9C,EAAU,IAAI,EAAiB,CAAa,YAAoB,UAAW,mBAAoB,UAAW,kBAAmB,CAAC,CAS9H,EAAW,IAAI,EAAsB,CACzC,WAAY,EAAmB,EAAQ,CACvC,QAAS,EACT,UACA,OAXkB,CAClB,KAAO,GAAiCC,EAAO,KAAK,WAAW,KAAK,UAAU,EAAI,GAAG,CACrF,KAAO,GAAiCA,EAAO,KAAK,WAAW,KAAK,UAAU,EAAI,GAAG,CACrF,MAAQ,GAAiCA,EAAO,MAAM,WAAW,KAAK,UAAU,EAAI,GAAG,CACvF,MAAQ,GAAiCA,EAAO,MAAM,WAAW,KAAK,UAAU,EAAI,GAAG,CACxF,CAOA,CAAC,CAEI,EAAU,MAAM,EAAS,SAAS,CACxC,GAAI,EAAQ,SAAW,EAAG,CACxB,EAAO,KAAK,0CAA0C,CACtD,OAKF,GAFA,EAAO,KAAK,sCAAuC,CAAE,QAAS,EAAQ,IAAI,GAAK,EAAE,KAAK,CAAE,CAAC,CAErF,CAAC,EAAU,CACb,MAAM,EAAS,IAAI,CACnB,OASF,IADiB,EAAU,SAAiB,MAAM,KAAO,GAC3C,EAAG,CACf,EAAO,KAAK,mFAAmF,CAC/F,MAAM,EAAS,IAAI,CACnB,OAOF,EAAO,KAAK,kCAAkC,CAC9C,IAAM,EAAiB,MAAO,EAAU,kBAA0B,cAAc,CAAE,KAAM,QAAS,CAAC,CAClG,GAAI,CAMF,IAAI,EAAe,GACnB,KAAO,CAAC,GAEN,GADe,MAAM,EAAe,MAAM,+BAA+B,EAAwB,eAAe,EAC1F,KAAK,GAAG,SACzB,GAAc,MAAM,EAAW,IAAK,CAE3C,EAAO,KAAK,iCAAiC,EAIxB,MAAM,EAAS,SAAS,EAC5B,OAAS,EACxB,MAAM,EAAS,IAAI,CAEnB,EAAO,KAAK,mEAAmE,QAEzE,CACR,GAAI,CACF,MAAM,EAAe,MAAM,6BAA6B,EAAwB,GAAG,CACnF,EAAO,KAAK,iCAAiC,QACrC,CACP,EAAU,kBAA0B,kBAAkB,EAAe"}
1
+ {"version":3,"file":"index.js","names":["migration001.up","migration001.down","migration002.up","migration002.down","migration003.up","migration003.down","migration004.up","migration004.down","logger"],"sources":["../../../src/migrations/index.ts"],"sourcesContent":["import type { InputMigrations } from 'umzug';\nimport type { QueryInterface } from 'sequelize';\nimport type { Sequelize } from 'sequelize-typescript';\nimport { setTimeout } from 'node:timers/promises';\nimport logger from '../utils/logger';\nimport { type CustomFieldsEntriesModeOptions, readsEntries, resolveCustomFieldsEntriesMode, writesEntries } from '../utils/custom-fields-mode';\nimport * as migration001 from './001-create-core-tables';\nimport * as migration002 from './002-create-custom-field-entries';\nimport * as migration003 from './003-create-custom-field-model-type-map';\nimport * as migration004 from './004-entries-search-trgm-index';\n\n// umzug is CJS. Static ESM named imports break in vitest after vi.resetModules()\n// because Vite's SSR transform re-evaluates the module outside the inline bundle.\n// require() goes through Node's native CJS loader and is stable across resets.\n\n// eslint-disable-next-line import/no-commonjs\nconst { Umzug, SequelizeStorage } = require('umzug') as typeof import('umzug');\n\ninterface MigratorOptions extends CustomFieldsEntriesModeOptions {\n useModelTypeMapping?: boolean;\n useEntriesSearchIndex?: boolean;\n withLock?: boolean;\n}\n\nconst buildMigrationList = (options: MigratorOptions): InputMigrations<QueryInterface> => {\n const { useModelTypeMapping = false } = options;\n // The table is needed as soon as anything writes to it, and every mode that reads\n // entries also writes them, so `writesEntries` is the widest gate. Keying this off\n // `readsEntries` would leave a WRITE_ONLY service inserting into a missing table.\n //\n // Migrations only need this boolean — WRITE_ONLY and READ_AND_WRITE are the same\n // answer here. The full mode is accepted anyway so a service passes one shared\n // constant to both this and useCustomFields(), rather than keeping a separate\n // migration-only boolean in sync with the runtime mode.\n const resolvedMode = resolveCustomFieldsEntriesMode(options);\n const shouldRunEntriesTableMigration = writesEntries(resolvedMode);\n // The search index is only needed for READ_AND_WRITE services that currently do expose custom field search term integration (i.e vehicle-ms)\n const shouldRunEntriesSearchIndexMigration = readsEntries(resolvedMode) && options.useEntriesSearchIndex === true;\n return [\n { name: '001-create-core-tables', up: migration001.up, down: migration001.down },\n ...(shouldRunEntriesTableMigration ? [{ name: '002-create-custom-field-entries', up: migration002.up, down: migration002.down }] : []),\n ...(useModelTypeMapping ? [{ name: '003-create-custom-field-model-type-map', up: migration003.up, down: migration003.down }] : []),\n ...(shouldRunEntriesSearchIndexMigration ? [{ name: '004-entries-search-trgm-index', up: migration004.up, down: migration004.down }] : []),\n ];\n};\n\n// Stable advisory-lock ID for sadot migrations. Chosen arbitrarily; must not\n// collide with other advisory locks in the same database.\nconst SADOT_MIGRATION_LOCK_ID = 839274628;\n\n/**\n * Runs sadot migrations via umzug. All migrations use IF NOT EXISTS / IF EXISTS,\n * so they are fully idempotent — safe to run against both fresh and existing\n * databases (including DBs previously managed by the legacy sync() path).\n *\n * When `withLock: true`, a PostgreSQL advisory lock is acquired before running\n * migrations so that when multiple pods start concurrently only one of them\n * executes the migrations. The remaining pods block until the lock is released,\n * then proceed — Umzug will see the migrations have already been applied and\n * skip them.\n *\n * @param options - Migration execution options.\n * @param options.withLock - Whether to acquire a PostgreSQL advisory lock before running the migrations. Defaults to true.\n * @returns A promise that resolves when the migrations have completed.\n */\nconst runSadotMigrations = async (sequelize: Sequelize, options: MigratorOptions = {}): Promise<void> => {\n // Need to add a new migration? full migration guide here; https://www.notion.so/autofleet/How-to-Add-a-New-Migration-to-Sadot-36db603f40b98023b239d3c61aa22b00\n const { withLock = true } = options;\n const queryInterface = sequelize.getQueryInterface() as unknown as QueryInterface;\n const storage = new SequelizeStorage({ sequelize: sequelize as never, tableName: 'sadot_migrations', modelName: 'SadotMigrations' });\n\n const umzugLogger = {\n info: (msg: Record<string, unknown>) => logger.info(`[sadot] ${JSON.stringify(msg)}`),\n warn: (msg: Record<string, unknown>) => logger.warn(`[sadot] ${JSON.stringify(msg)}`),\n error: (msg: Record<string, unknown>) => logger.error(`[sadot] ${JSON.stringify(msg)}`),\n debug: (msg: Record<string, unknown>) => logger.debug(`[sadot] ${JSON.stringify(msg)}`),\n };\n\n const migrator = new Umzug<QueryInterface>({\n migrations: buildMigrationList(options),\n context: queryInterface,\n storage,\n logger: umzugLogger,\n });\n\n const pending = await migrator.pending();\n if (pending.length === 0) {\n logger.info('[sadot] no pending migrations, skipping');\n return;\n }\n\n logger.info('[sadot] pending migrations detected', { pending: pending.map(m => m.name) });\n\n if (!withLock) {\n await migrator.up();\n return;\n }\n\n // The advisory lock holds a connection for its entire duration while\n // migrator.up() needs additional pool connections for queries. With\n // pool.max < 2 (common in test environments) this deadlocks.\n // Migrations are idempotent (IF NOT EXISTS / IF EXISTS) so running\n // without the lock is safe — just potentially duplicated work.\n const poolMax = (sequelize.options as any)?.pool?.max ?? 5;\n if (poolMax < 2) {\n logger.info('[sadot] connection pool too small for advisory lock, running migrations directly');\n await migrator.up();\n return;\n }\n\n // Use a dedicated raw connection (not a transaction) to hold the advisory lock.\n // A transaction connection sitting idle while migrator.up() runs on other pool\n // connections would be killed by idle_in_transaction_session_timeout, releasing\n // the lock prematurely and allowing a second replica to start migrations concurrently.\n logger.info('[sadot] acquiring advisory lock');\n const lockConnection = await (sequelize.connectionManager as any).getConnection({ type: 'write' });\n try {\n // Poll with pg_try_advisory_lock so the connection stays idle between attempts rather\n // than holding an active statement for the full wait duration. DDL operations that track\n // active backends between internal phases (e.g. CREATE INDEX CONCURRENTLY, VACUUM,\n // REINDEX CONCURRENTLY) wait for all active statements to finish before proceeding —\n // a long-blocking lock attempt would stall them for the entire wait.\n let lockAcquired = false;\n while (!lockAcquired) {\n const result = await lockConnection.query(`SELECT pg_try_advisory_lock(${SADOT_MIGRATION_LOCK_ID}) AS acquired`);\n lockAcquired = result.rows[0].acquired;\n if (!lockAcquired) await setTimeout(1000);\n }\n logger.info('[sadot] advisory lock acquired');\n\n // Re-check after acquiring the lock — another pod may have already\n // applied the migrations while we were waiting.\n const stillPending = await migrator.pending();\n if (stillPending.length > 0) {\n await migrator.up();\n } else {\n logger.info('[sadot] migrations already applied by another instance, skipping');\n }\n } finally {\n try {\n await lockConnection.query(`SELECT pg_advisory_unlock(${SADOT_MIGRATION_LOCK_ID})`);\n logger.info('[sadot] advisory lock released');\n } finally {\n (sequelize.connectionManager as any).releaseConnection(lockConnection);\n }\n }\n};\n\nexport { runSadotMigrations };\n"],"mappings":"0hBAgBA,KAAM,CAAE,QAAO,oBAAA,EAA6B,QAAQ,CAQ9C,EAAsB,GAA8D,CACxF,GAAM,CAAE,sBAAsB,IAAU,EASlC,EAAe,EAA+B,EAAQ,CACtD,EAAiC,EAAc,EAAa,CAE5D,EAAuC,EAAa,EAAa,EAAI,EAAQ,wBAA0B,GAC7G,MAAO,CACL,CAAE,KAAM,yBAA8BA,KAAuBC,OAAmB,CAChF,GAAI,EAAiC,CAAC,CAAE,KAAM,kCAAmC,GAAIC,EAAiB,KAAMC,EAAmB,CAAC,CAAG,EAAE,CACrI,GAAI,EAAsB,CAAC,CAAE,KAAM,yCAA0C,GAAIC,EAAiB,KAAMC,EAAmB,CAAC,CAAG,EAAE,CACjI,GAAI,EAAuC,CAAC,CAAE,KAAM,gCAAiC,GAAIC,EAAiB,KAAMC,EAAmB,CAAC,CAAG,EAAE,CAC1I,EAKG,EAA0B,UAiB1B,EAAqB,MAAO,EAAsB,EAA2B,EAAE,GAAoB,CAEvG,GAAM,CAAE,WAAW,IAAS,EACtB,EAAiB,EAAU,mBAAmB,CAC9C,EAAU,IAAI,EAAiB,CAAa,YAAoB,UAAW,mBAAoB,UAAW,kBAAmB,CAAC,CAS9H,EAAW,IAAI,EAAsB,CACzC,WAAY,EAAmB,EAAQ,CACvC,QAAS,EACT,UACA,OAXkB,CAClB,KAAO,GAAiCC,EAAO,KAAK,WAAW,KAAK,UAAU,EAAI,GAAG,CACrF,KAAO,GAAiCA,EAAO,KAAK,WAAW,KAAK,UAAU,EAAI,GAAG,CACrF,MAAQ,GAAiCA,EAAO,MAAM,WAAW,KAAK,UAAU,EAAI,GAAG,CACvF,MAAQ,GAAiCA,EAAO,MAAM,WAAW,KAAK,UAAU,EAAI,GAAG,CACxF,CAOA,CAAC,CAEI,EAAU,MAAM,EAAS,SAAS,CACxC,GAAI,EAAQ,SAAW,EAAG,CACxB,EAAO,KAAK,0CAA0C,CACtD,OAKF,GAFA,EAAO,KAAK,sCAAuC,CAAE,QAAS,EAAQ,IAAI,GAAK,EAAE,KAAK,CAAE,CAAC,CAErF,CAAC,EAAU,CACb,MAAM,EAAS,IAAI,CACnB,OASF,IADiB,EAAU,SAAiB,MAAM,KAAO,GAC3C,EAAG,CACf,EAAO,KAAK,mFAAmF,CAC/F,MAAM,EAAS,IAAI,CACnB,OAOF,EAAO,KAAK,kCAAkC,CAC9C,IAAM,EAAiB,MAAO,EAAU,kBAA0B,cAAc,CAAE,KAAM,QAAS,CAAC,CAClG,GAAI,CAMF,IAAI,EAAe,GACnB,KAAO,CAAC,GAEN,GADe,MAAM,EAAe,MAAM,+BAA+B,EAAwB,eAAe,EAC1F,KAAK,GAAG,SACzB,GAAc,MAAM,EAAW,IAAK,CAE3C,EAAO,KAAK,iCAAiC,EAIxB,MAAM,EAAS,SAAS,EAC5B,OAAS,EACxB,MAAM,EAAS,IAAI,CAEnB,EAAO,KAAK,mEAAmE,QAEzE,CACR,GAAI,CACF,MAAM,EAAe,MAAM,6BAA6B,EAAwB,GAAG,CACnF,EAAO,KAAK,iCAAiC,QACrC,CACP,EAAU,kBAA0B,kBAAkB,EAAe"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["CUSTOM_FIELDS_FILTER_SCOPE: typeof customFields.CUSTOM_FIELDS_FILTER_SCOPE","customFields"],"sources":["../../../../src/utils/constants/index.ts"],"sourcesContent":["import { customFields } from '@autofleet/common-types';\n\nexport const supportedEntities = ['businessModelId', 'fleetId', 'demandSourceId'] as const;\n\n/**\n * Supported custom field types\n */\nexport enum CustomFieldDefinitionType {\n NUMBER = 'number',\n BOOLEAN = 'boolean',\n DATE = 'date',\n DATETIME = 'datetime',\n TEXT = 'text',\n IMAGE = 'image',\n SELECT = 'select',\n STATUS = 'status',\n FILE = 'file',\n MULTI_SELECT_STATUS = 'multi-select-status',\n MULTI_SELECT_DROPDOWN = 'multi-select-dropdown',\n}\n\n/**\n * SQL expression extracting every top-level VALUE of a custom-fields JSONB blob as one text\n * string (`'$.*'` selects values only — key names are never included). The trigram index\n * (migration 004) is built on this expression, and Postgres uses an expression index only\n * when the query repeats the indexed expression exactly — so the query side and the\n * migration side must both come from this single source.\n */\nexport const entriesValuesTextExpression = (columnRef: string): string => `jsonb_path_query_array(${columnRef}, '$.*')::text`;\n\nconst CUSTOM_FIELDS_FILTER_SCOPE: typeof customFields.CUSTOM_FIELDS_FILTER_SCOPE = customFields.CUSTOM_FIELDS_FILTER_SCOPE;\n\nexport {\n /** @deprecated Use the value from `@autofleet/common-types` instead */\n CUSTOM_FIELDS_FILTER_SCOPE,\n};\n"],"mappings":"mGAEA,MAAa,EAAoB,CAAC,kBAAmB,UAAW,iBAAiB,CAKjF,IAAY,EAAA,SAAA,EAAL,OACL,GAAA,OAAA,SACA,EAAA,QAAA,UACA,EAAA,KAAA,OACA,EAAA,SAAA,WACA,EAAA,KAAA,OACA,EAAA,MAAA,QACA,EAAA,OAAA,SACA,EAAA,OAAA,SACA,EAAA,KAAA,OACA,EAAA,oBAAA,sBACA,EAAA,sBAAA,+BAUF,MAAa,EAA+B,GAA8B,0BAA0B,EAAU,gBAExGA,EAA6EC,EAAAA,aAAa"}
1
+ {"version":3,"file":"index.cjs","names":["CUSTOM_FIELDS_FILTER_SCOPE: typeof customFields.CUSTOM_FIELDS_FILTER_SCOPE","customFields"],"sources":["../../../../src/utils/constants/index.ts"],"sourcesContent":["import { customFields } from '@autofleet/common-types';\n\nexport const supportedEntities = ['businessModelId', 'fleetId', 'demandSourceId'] as const;\n\n/**\n * Supported custom field types\n */\nexport enum CustomFieldDefinitionType {\n NUMBER = 'number',\n BOOLEAN = 'boolean',\n DATE = 'date',\n DATETIME = 'datetime',\n TEXT = 'text',\n IMAGE = 'image',\n SELECT = 'select',\n STATUS = 'status',\n FILE = 'file',\n MULTI_SELECT_STATUS = 'multi-select-status',\n MULTI_SELECT_DROPDOWN = 'multi-select-dropdown',\n}\n\nexport const entriesValuesTextExpression = (columnRef: string): string => `jsonb_path_query_array(${columnRef}, '$.*')::text`;\n\nconst CUSTOM_FIELDS_FILTER_SCOPE: typeof customFields.CUSTOM_FIELDS_FILTER_SCOPE = customFields.CUSTOM_FIELDS_FILTER_SCOPE;\n\nexport {\n /** @deprecated Use the value from `@autofleet/common-types` instead */\n CUSTOM_FIELDS_FILTER_SCOPE,\n};\n"],"mappings":"mGAEA,MAAa,EAAoB,CAAC,kBAAmB,UAAW,iBAAiB,CAKjF,IAAY,EAAA,SAAA,EAAL,OACL,GAAA,OAAA,SACA,EAAA,QAAA,UACA,EAAA,KAAA,OACA,EAAA,SAAA,WACA,EAAA,KAAA,OACA,EAAA,MAAA,QACA,EAAA,OAAA,SACA,EAAA,OAAA,SACA,EAAA,KAAA,OACA,EAAA,oBAAA,sBACA,EAAA,sBAAA,+BAGF,MAAa,EAA+B,GAA8B,0BAA0B,EAAU,gBAExGA,EAA6EC,EAAAA,aAAa"}
@@ -18,13 +18,6 @@ declare enum CustomFieldDefinitionType {
18
18
  MULTI_SELECT_STATUS = "multi-select-status",
19
19
  MULTI_SELECT_DROPDOWN = "multi-select-dropdown",
20
20
  }
21
- /**
22
- * SQL expression extracting every top-level VALUE of a custom-fields JSONB blob as one text
23
- * string (`'$.*'` selects values only — key names are never included). The trigram index
24
- * (migration 004) is built on this expression, and Postgres uses an expression index only
25
- * when the query repeats the indexed expression exactly — so the query side and the
26
- * migration side must both come from this single source.
27
- */
28
21
  declare const entriesValuesTextExpression: (columnRef: string) => string;
29
22
  declare const CUSTOM_FIELDS_FILTER_SCOPE: typeof customFields.CUSTOM_FIELDS_FILTER_SCOPE;
30
23
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["CUSTOM_FIELDS_FILTER_SCOPE: typeof customFields.CUSTOM_FIELDS_FILTER_SCOPE"],"sources":["../../../../src/utils/constants/index.ts"],"sourcesContent":["import { customFields } from '@autofleet/common-types';\n\nexport const supportedEntities = ['businessModelId', 'fleetId', 'demandSourceId'] as const;\n\n/**\n * Supported custom field types\n */\nexport enum CustomFieldDefinitionType {\n NUMBER = 'number',\n BOOLEAN = 'boolean',\n DATE = 'date',\n DATETIME = 'datetime',\n TEXT = 'text',\n IMAGE = 'image',\n SELECT = 'select',\n STATUS = 'status',\n FILE = 'file',\n MULTI_SELECT_STATUS = 'multi-select-status',\n MULTI_SELECT_DROPDOWN = 'multi-select-dropdown',\n}\n\n/**\n * SQL expression extracting every top-level VALUE of a custom-fields JSONB blob as one text\n * string (`'$.*'` selects values only — key names are never included). The trigram index\n * (migration 004) is built on this expression, and Postgres uses an expression index only\n * when the query repeats the indexed expression exactly — so the query side and the\n * migration side must both come from this single source.\n */\nexport const entriesValuesTextExpression = (columnRef: string): string => `jsonb_path_query_array(${columnRef}, '$.*')::text`;\n\nconst CUSTOM_FIELDS_FILTER_SCOPE: typeof customFields.CUSTOM_FIELDS_FILTER_SCOPE = customFields.CUSTOM_FIELDS_FILTER_SCOPE;\n\nexport {\n /** @deprecated Use the value from `@autofleet/common-types` instead */\n CUSTOM_FIELDS_FILTER_SCOPE,\n};\n"],"mappings":"uDAEA,MAAa,EAAoB,CAAC,kBAAmB,UAAW,iBAAiB,CAKjF,IAAY,EAAA,SAAA,EAAL,OACL,GAAA,OAAA,SACA,EAAA,QAAA,UACA,EAAA,KAAA,OACA,EAAA,SAAA,WACA,EAAA,KAAA,OACA,EAAA,MAAA,QACA,EAAA,OAAA,SACA,EAAA,OAAA,SACA,EAAA,KAAA,OACA,EAAA,oBAAA,sBACA,EAAA,sBAAA,+BAUF,MAAa,EAA+B,GAA8B,0BAA0B,EAAU,gBAExGA,EAA6E,EAAa"}
1
+ {"version":3,"file":"index.js","names":["CUSTOM_FIELDS_FILTER_SCOPE: typeof customFields.CUSTOM_FIELDS_FILTER_SCOPE"],"sources":["../../../../src/utils/constants/index.ts"],"sourcesContent":["import { customFields } from '@autofleet/common-types';\n\nexport const supportedEntities = ['businessModelId', 'fleetId', 'demandSourceId'] as const;\n\n/**\n * Supported custom field types\n */\nexport enum CustomFieldDefinitionType {\n NUMBER = 'number',\n BOOLEAN = 'boolean',\n DATE = 'date',\n DATETIME = 'datetime',\n TEXT = 'text',\n IMAGE = 'image',\n SELECT = 'select',\n STATUS = 'status',\n FILE = 'file',\n MULTI_SELECT_STATUS = 'multi-select-status',\n MULTI_SELECT_DROPDOWN = 'multi-select-dropdown',\n}\n\nexport const entriesValuesTextExpression = (columnRef: string): string => `jsonb_path_query_array(${columnRef}, '$.*')::text`;\n\nconst CUSTOM_FIELDS_FILTER_SCOPE: typeof customFields.CUSTOM_FIELDS_FILTER_SCOPE = customFields.CUSTOM_FIELDS_FILTER_SCOPE;\n\nexport {\n /** @deprecated Use the value from `@autofleet/common-types` instead */\n CUSTOM_FIELDS_FILTER_SCOPE,\n};\n"],"mappings":"uDAEA,MAAa,EAAoB,CAAC,kBAAmB,UAAW,iBAAiB,CAKjF,IAAY,EAAA,SAAA,EAAL,OACL,GAAA,OAAA,SACA,EAAA,QAAA,UACA,EAAA,KAAA,OACA,EAAA,SAAA,WACA,EAAA,KAAA,OACA,EAAA,MAAA,QACA,EAAA,OAAA,SACA,EAAA,OAAA,SACA,EAAA,KAAA,OACA,EAAA,oBAAA,sBACA,EAAA,sBAAA,+BAGF,MAAa,EAA+B,GAA8B,0BAA0B,EAAU,gBAExGA,EAA6E,EAAa"}
@@ -1,2 +1,2 @@
1
- const e=require(`./logger/index.cjs`),t={DISABLED:`disabled`,WRITE_ONLY:`write-only`,READ_AND_WRITE:`read-and-write`},n=Object.values(t),r=e=>n.includes(e),i=`sadot: \`useCustomFieldsEntries\` is deprecated, use \`customFieldsEntriesMode\` with one of ${Object.values(t).map(e=>`'${e}'`).join(` | `)}`,a=[void 0,null,``,!1],o=(o={})=>{let{useCustomFieldsEntries:s}=o,c=o.customFieldsEntriesMode,l=a.includes(c)?void 0:c;if(l!==void 0){if(!r(l))throw Error(`sadot: unknown customFieldsEntriesMode ${JSON.stringify(l)}. Expected one of ${n.map(e=>`'${e}'`).join(` | `)}`);return s!==void 0&&e.default.warn(`${i}. Both were passed; \`useCustomFieldsEntries\` is ignored`),l}return s===void 0?t.DISABLED:(e.default.warn(i),s?t.READ_AND_WRITE:t.DISABLED)},s=[t.WRITE_ONLY,t.READ_AND_WRITE],c=[t.READ_AND_WRITE],l=e=>s.includes(e),u=e=>c.includes(e);let d;const f=e=>{d=e},p=()=>d;exports.CUSTOM_FIELDS_ENTRIES_MODE=t,exports.getCustomFieldsEntriesMode=p,exports.readsEntries=u,exports.resolveCustomFieldsEntriesMode=o,exports.setCustomFieldsEntriesMode=f,exports.writesEntries=l;
1
+ const e=require(`./logger/index.cjs`),t={DISABLED:`disabled`,WRITE_ONLY:`write-only`,READ_AND_WRITE:`read-and-write`},n=Object.values(t),r=e=>n.includes(e),i=`sadot: \`useCustomFieldsEntries\` is deprecated, use \`customFieldsEntriesMode\` with one of ${Object.values(t).map(e=>`'${e}'`).join(` | `)}`,a=[void 0,null,``,!1],o=(o={})=>{let{useCustomFieldsEntries:s}=o,c=o.customFieldsEntriesMode,l=a.includes(c)?void 0:c;if(l!==void 0){if(!r(l))throw Error(`sadot: unknown customFieldsEntriesMode ${JSON.stringify(l)}. Expected one of ${n.map(e=>`'${e}'`).join(` | `)}`);return s!==void 0&&e.default.warn(`${i}. Both were passed; \`useCustomFieldsEntries\` is ignored`),l}return s===void 0?t.DISABLED:(e.default.warn(i),s?t.READ_AND_WRITE:t.DISABLED)},s=e=>e===t.WRITE_ONLY||e===t.READ_AND_WRITE,c=e=>e===t.READ_AND_WRITE;exports.CUSTOM_FIELDS_ENTRIES_MODE=t,exports.readsEntries=c,exports.resolveCustomFieldsEntriesMode=o,exports.writesEntries=s;
2
2
  //# sourceMappingURL=custom-fields-mode.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"custom-fields-mode.cjs","names":["CUSTOM_FIELDS_ENTRIES_MODES: readonly CustomFieldsEntriesMode[]","UNSET_MODE_VALUES: readonly unknown[]","customFieldsEntriesMode","ENTRIES_WRITING_MODES: readonly CustomFieldsEntriesMode[]","ENTRIES_READING_MODES: readonly CustomFieldsEntriesMode[]","customFieldsEntriesMode: CustomFieldsEntriesMode | undefined"],"sources":["../../../src/utils/custom-fields-mode.ts"],"sourcesContent":["import logger from './logger';\n\n/**\n * How a service uses `custom_field_entries`.\n *\n * - `DISABLED` — values only. The default when the option is omitted.\n * - `WRITE_ONLY` — write both tables, still read from `custom_field_values`.\n * The safe middle stage: entries fills up and stays current while reads are\n * unaffected, so a backfill only has to cover history.\n * - `READ_AND_WRITE` — write both tables, read from `custom_field_entries`.\n *\n * `custom_field_values` is written in every mode, which is what makes moving\n * between stages reversible.\n */\nexport const CUSTOM_FIELDS_ENTRIES_MODE = {\n DISABLED: 'disabled',\n WRITE_ONLY: 'write-only',\n READ_AND_WRITE: 'read-and-write',\n} as const;\n\nexport type CustomFieldsEntriesMode = typeof CUSTOM_FIELDS_ENTRIES_MODE[keyof typeof CUSTOM_FIELDS_ENTRIES_MODE];\n\nconst CUSTOM_FIELDS_ENTRIES_MODES: readonly CustomFieldsEntriesMode[] = Object.values(CUSTOM_FIELDS_ENTRIES_MODE);\n\nconst isCustomFieldsEntriesMode = (value: unknown): value is CustomFieldsEntriesMode =>\n CUSTOM_FIELDS_ENTRIES_MODES.includes(value as CustomFieldsEntriesMode);\n\nexport interface CustomFieldsEntriesModeOptions {\n customFieldsEntriesMode?: CustomFieldsEntriesMode;\n /** @deprecated Use `customFieldsEntriesMode` instead. */\n useCustomFieldsEntries?: boolean;\n}\n\nconst DEPRECATION_MESSAGE = 'sadot: `useCustomFieldsEntries` is deprecated, use `customFieldsEntriesMode`'\n + ` with one of ${Object.values(CUSTOM_FIELDS_ENTRIES_MODE).map(mode => `'${mode}'`).join(' | ')}`;\n\n/**\n * Resolves the effective mode from the new option and the deprecated boolean.\n *\n * The legacy boolean keeps working so services can bump the package before they\n * rename the option, but an unrecognised mode is a typo and throws rather than\n * silently degrading to `DISABLED` — which would stop entries being written.\n */\n// `null` and an empty string mean \"not configured\" rather than \"typo\" — a settings\n// lookup or an env var that came back empty. The public option type does not admit\n// them, but a JS caller can still pass them.\nconst UNSET_MODE_VALUES: readonly unknown[] = [undefined, null, '', false];\n\nexport const resolveCustomFieldsEntriesMode = (options: CustomFieldsEntriesModeOptions = {}): CustomFieldsEntriesMode => {\n const { useCustomFieldsEntries } = options;\n\n const rawMode = options.customFieldsEntriesMode as unknown;\n const customFieldsEntriesMode = UNSET_MODE_VALUES.includes(rawMode) ? undefined : rawMode;\n\n if (customFieldsEntriesMode !== undefined) {\n if (!isCustomFieldsEntriesMode(customFieldsEntriesMode)) {\n throw new Error(\n `sadot: unknown customFieldsEntriesMode ${JSON.stringify(customFieldsEntriesMode)}.`\n + ` Expected one of ${CUSTOM_FIELDS_ENTRIES_MODES.map(mode => `'${mode}'`).join(' | ')}`,\n );\n }\n if (useCustomFieldsEntries !== undefined) {\n logger.warn(`${DEPRECATION_MESSAGE}. Both were passed; \\`useCustomFieldsEntries\\` is ignored`);\n }\n return customFieldsEntriesMode;\n }\n\n if (useCustomFieldsEntries !== undefined) {\n logger.warn(DEPRECATION_MESSAGE);\n return useCustomFieldsEntries\n ? CUSTOM_FIELDS_ENTRIES_MODE.READ_AND_WRITE\n : CUSTOM_FIELDS_ENTRIES_MODE.DISABLED;\n }\n\n return CUSTOM_FIELDS_ENTRIES_MODE.DISABLED;\n};\n\n// Both predicates accept `undefined` so the \"no mode means DISABLED\" rule lives here\n// rather than being repeated at every call site. Publicly exported scopes can be\n// invoked with no options at all.\n\nconst ENTRIES_WRITING_MODES: readonly CustomFieldsEntriesMode[] = [\n CUSTOM_FIELDS_ENTRIES_MODE.WRITE_ONLY,\n CUSTOM_FIELDS_ENTRIES_MODE.READ_AND_WRITE,\n];\nconst ENTRIES_READING_MODES: readonly CustomFieldsEntriesMode[] = [CUSTOM_FIELDS_ENTRIES_MODE.READ_AND_WRITE];\n\n/** True when `custom_field_entries` should be written alongside `custom_field_values`. */\nexport const writesEntries = (mode: CustomFieldsEntriesMode | undefined): boolean =>\n ENTRIES_WRITING_MODES.includes(mode as CustomFieldsEntriesMode);\n\n/** True when reads, filtering and sorting should go to `custom_field_entries`. */\nexport const readsEntries = (mode: CustomFieldsEntriesMode | undefined): boolean =>\n ENTRIES_READING_MODES.includes(mode as CustomFieldsEntriesMode);\n\n// The package-level resolved mode. Set once during `useCustomFields()` so free functions\n// (like the search helper) can read it without every caller threading the option through.\n// Stays undefined in services that never initialize sadot — treated as DISABLED everywhere.\nlet customFieldsEntriesMode: CustomFieldsEntriesMode | undefined;\n\nexport const setCustomFieldsEntriesMode = (mode: CustomFieldsEntriesMode | undefined): void => {\n customFieldsEntriesMode = mode;\n};\n\nexport const getCustomFieldsEntriesMode = (): CustomFieldsEntriesMode | undefined => customFieldsEntriesMode;\n"],"mappings":"sCAca,EAA6B,CACxC,SAAU,WACV,WAAY,aACZ,eAAgB,iBACjB,CAIKA,EAAkE,OAAO,OAAO,EAA2B,CAE3G,EAA6B,GACjC,EAA4B,SAAS,EAAiC,CAQlE,EAAsB,gGACR,OAAO,OAAO,EAA2B,CAAC,IAAI,GAAQ,IAAI,EAAK,GAAG,CAAC,KAAK,MAAM,GAY5FC,EAAwC,CAAC,IAAA,GAAW,KAAM,GAAI,GAAM,CAE7D,GAAkC,EAA0C,EAAE,GAA8B,CACvH,GAAM,CAAE,0BAA2B,EAE7B,EAAU,EAAQ,wBAClBC,EAA0B,EAAkB,SAAS,EAAQ,CAAG,IAAA,GAAY,EAElF,GAAIA,IAA4B,IAAA,GAAW,CACzC,GAAI,CAAC,EAA0BA,EAAwB,CACrD,MAAU,MACR,0CAA0C,KAAK,UAAUA,EAAwB,CAAC,oBAC5D,EAA4B,IAAI,GAAQ,IAAI,EAAK,GAAG,CAAC,KAAK,MAAM,GACvF,CAKH,OAHI,IAA2B,IAAA,IAC7B,EAAA,QAAO,KAAK,GAAG,EAAoB,2DAA2D,CAEzFA,EAUT,OAPI,IAA2B,IAAA,GAOxB,EAA2B,UANhC,EAAA,QAAO,KAAK,EAAoB,CACzB,EACH,EAA2B,eAC3B,EAA2B,WAU7BC,EAA4D,CAChE,EAA2B,WAC3B,EAA2B,eAC5B,CACKC,EAA4D,CAAC,EAA2B,eAAe,CAGhG,EAAiB,GAC5B,EAAsB,SAAS,EAAgC,CAGpD,EAAgB,GAC3B,EAAsB,SAAS,EAAgC,CAKjE,IAAIC,EAEJ,MAAa,EAA8B,GAAoD,CAC7F,EAA0B,GAGf,MAAwE"}
1
+ {"version":3,"file":"custom-fields-mode.cjs","names":["CUSTOM_FIELDS_ENTRIES_MODES: readonly CustomFieldsEntriesMode[]","UNSET_MODE_VALUES: readonly unknown[]"],"sources":["../../../src/utils/custom-fields-mode.ts"],"sourcesContent":["import logger from './logger';\n\n/**\n * How a service uses `custom_field_entries`.\n *\n * - `DISABLED` — values only. The default when the option is omitted.\n * - `WRITE_ONLY` — write both tables, still read from `custom_field_values`.\n * The safe middle stage: entries fills up and stays current while reads are\n * unaffected, so a backfill only has to cover history.\n * - `READ_AND_WRITE` — write both tables, read from `custom_field_entries`.\n *\n * `custom_field_values` is written in every mode, which is what makes moving\n * between stages reversible.\n */\nexport const CUSTOM_FIELDS_ENTRIES_MODE = {\n DISABLED: 'disabled',\n WRITE_ONLY: 'write-only',\n READ_AND_WRITE: 'read-and-write',\n} as const;\n\nexport type CustomFieldsEntriesMode = typeof CUSTOM_FIELDS_ENTRIES_MODE[keyof typeof CUSTOM_FIELDS_ENTRIES_MODE];\n\nconst CUSTOM_FIELDS_ENTRIES_MODES: readonly CustomFieldsEntriesMode[] = Object.values(CUSTOM_FIELDS_ENTRIES_MODE);\n\nconst isCustomFieldsEntriesMode = (value: unknown): value is CustomFieldsEntriesMode =>\n CUSTOM_FIELDS_ENTRIES_MODES.includes(value as CustomFieldsEntriesMode);\n\nexport interface CustomFieldsEntriesModeOptions {\n customFieldsEntriesMode?: CustomFieldsEntriesMode;\n /** @deprecated Use `customFieldsEntriesMode` instead. */\n useCustomFieldsEntries?: boolean;\n}\n\nconst DEPRECATION_MESSAGE = 'sadot: `useCustomFieldsEntries` is deprecated, use `customFieldsEntriesMode`'\n + ` with one of ${Object.values(CUSTOM_FIELDS_ENTRIES_MODE).map(mode => `'${mode}'`).join(' | ')}`;\n\n/**\n * Resolves the effective mode from the new option and the deprecated boolean.\n *\n * The legacy boolean keeps working so services can bump the package before they\n * rename the option, but an unrecognised mode is a typo and throws rather than\n * silently degrading to `DISABLED` — which would stop entries being written.\n */\n// `null` and an empty string mean \"not configured\" rather than \"typo\" — a settings\n// lookup or an env var that came back empty. The public option type does not admit\n// them, but a JS caller can still pass them.\nconst UNSET_MODE_VALUES: readonly unknown[] = [undefined, null, '', false];\n\nexport const resolveCustomFieldsEntriesMode = (options: CustomFieldsEntriesModeOptions = {}): CustomFieldsEntriesMode => {\n const { useCustomFieldsEntries } = options;\n\n const rawMode = options.customFieldsEntriesMode as unknown;\n const customFieldsEntriesMode = UNSET_MODE_VALUES.includes(rawMode) ? undefined : rawMode;\n\n if (customFieldsEntriesMode !== undefined) {\n if (!isCustomFieldsEntriesMode(customFieldsEntriesMode)) {\n throw new Error(\n `sadot: unknown customFieldsEntriesMode ${JSON.stringify(customFieldsEntriesMode)}.`\n + ` Expected one of ${CUSTOM_FIELDS_ENTRIES_MODES.map(mode => `'${mode}'`).join(' | ')}`,\n );\n }\n if (useCustomFieldsEntries !== undefined) {\n logger.warn(`${DEPRECATION_MESSAGE}. Both were passed; \\`useCustomFieldsEntries\\` is ignored`);\n }\n return customFieldsEntriesMode;\n }\n\n if (useCustomFieldsEntries !== undefined) {\n logger.warn(DEPRECATION_MESSAGE);\n return useCustomFieldsEntries\n ? CUSTOM_FIELDS_ENTRIES_MODE.READ_AND_WRITE\n : CUSTOM_FIELDS_ENTRIES_MODE.DISABLED;\n }\n\n return CUSTOM_FIELDS_ENTRIES_MODE.DISABLED;\n};\n\n// Both predicates accept `undefined` so the \"no mode means DISABLED\" rule lives here\n// rather than being repeated at every call site.\n\n/** True when `custom_field_entries` should be written alongside `custom_field_values`. */\nexport const writesEntries = (mode: CustomFieldsEntriesMode | undefined): boolean =>\n mode === CUSTOM_FIELDS_ENTRIES_MODE.WRITE_ONLY || mode === CUSTOM_FIELDS_ENTRIES_MODE.READ_AND_WRITE;\n\n/** True when reads, filtering, sorting and search should go to `custom_field_entries`. */\nexport const readsEntries = (mode: CustomFieldsEntriesMode | undefined): boolean =>\n mode === CUSTOM_FIELDS_ENTRIES_MODE.READ_AND_WRITE;\n"],"mappings":"sCAca,EAA6B,CACxC,SAAU,WACV,WAAY,aACZ,eAAgB,iBACjB,CAIKA,EAAkE,OAAO,OAAO,EAA2B,CAE3G,EAA6B,GACjC,EAA4B,SAAS,EAAiC,CAQlE,EAAsB,gGACR,OAAO,OAAO,EAA2B,CAAC,IAAI,GAAQ,IAAI,EAAK,GAAG,CAAC,KAAK,MAAM,GAY5FC,EAAwC,CAAC,IAAA,GAAW,KAAM,GAAI,GAAM,CAE7D,GAAkC,EAA0C,EAAE,GAA8B,CACvH,GAAM,CAAE,0BAA2B,EAE7B,EAAU,EAAQ,wBAClB,EAA0B,EAAkB,SAAS,EAAQ,CAAG,IAAA,GAAY,EAElF,GAAI,IAA4B,IAAA,GAAW,CACzC,GAAI,CAAC,EAA0B,EAAwB,CACrD,MAAU,MACR,0CAA0C,KAAK,UAAU,EAAwB,CAAC,oBAC5D,EAA4B,IAAI,GAAQ,IAAI,EAAK,GAAG,CAAC,KAAK,MAAM,GACvF,CAKH,OAHI,IAA2B,IAAA,IAC7B,EAAA,QAAO,KAAK,GAAG,EAAoB,2DAA2D,CAEzF,EAUT,OAPI,IAA2B,IAAA,GAOxB,EAA2B,UANhC,EAAA,QAAO,KAAK,EAAoB,CACzB,EACH,EAA2B,eAC3B,EAA2B,WAUtB,EAAiB,GAC5B,IAAS,EAA2B,YAAc,IAAS,EAA2B,eAG3E,EAAgB,GAC3B,IAAS,EAA2B"}
@@ -1,2 +1,2 @@
1
- import e from"./logger/index.js";const t={DISABLED:`disabled`,WRITE_ONLY:`write-only`,READ_AND_WRITE:`read-and-write`},n=Object.values(t),r=e=>n.includes(e),i=`sadot: \`useCustomFieldsEntries\` is deprecated, use \`customFieldsEntriesMode\` with one of ${Object.values(t).map(e=>`'${e}'`).join(` | `)}`,a=[void 0,null,``,!1],o=(o={})=>{let{useCustomFieldsEntries:s}=o,c=o.customFieldsEntriesMode,l=a.includes(c)?void 0:c;if(l!==void 0){if(!r(l))throw Error(`sadot: unknown customFieldsEntriesMode ${JSON.stringify(l)}. Expected one of ${n.map(e=>`'${e}'`).join(` | `)}`);return s!==void 0&&e.warn(`${i}. Both were passed; \`useCustomFieldsEntries\` is ignored`),l}return s===void 0?t.DISABLED:(e.warn(i),s?t.READ_AND_WRITE:t.DISABLED)},s=[t.WRITE_ONLY,t.READ_AND_WRITE],c=[t.READ_AND_WRITE],l=e=>s.includes(e),u=e=>c.includes(e);let d;const f=e=>{d=e},p=()=>d;export{t as CUSTOM_FIELDS_ENTRIES_MODE,p as getCustomFieldsEntriesMode,u as readsEntries,o as resolveCustomFieldsEntriesMode,f as setCustomFieldsEntriesMode,l as writesEntries};
1
+ import e from"./logger/index.js";const t={DISABLED:`disabled`,WRITE_ONLY:`write-only`,READ_AND_WRITE:`read-and-write`},n=Object.values(t),r=e=>n.includes(e),i=`sadot: \`useCustomFieldsEntries\` is deprecated, use \`customFieldsEntriesMode\` with one of ${Object.values(t).map(e=>`'${e}'`).join(` | `)}`,a=[void 0,null,``,!1],o=(o={})=>{let{useCustomFieldsEntries:s}=o,c=o.customFieldsEntriesMode,l=a.includes(c)?void 0:c;if(l!==void 0){if(!r(l))throw Error(`sadot: unknown customFieldsEntriesMode ${JSON.stringify(l)}. Expected one of ${n.map(e=>`'${e}'`).join(` | `)}`);return s!==void 0&&e.warn(`${i}. Both were passed; \`useCustomFieldsEntries\` is ignored`),l}return s===void 0?t.DISABLED:(e.warn(i),s?t.READ_AND_WRITE:t.DISABLED)},s=e=>e===t.WRITE_ONLY||e===t.READ_AND_WRITE,c=e=>e===t.READ_AND_WRITE;export{t as CUSTOM_FIELDS_ENTRIES_MODE,c as readsEntries,o as resolveCustomFieldsEntriesMode,s as writesEntries};
2
2
  //# sourceMappingURL=custom-fields-mode.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"custom-fields-mode.js","names":["CUSTOM_FIELDS_ENTRIES_MODES: readonly CustomFieldsEntriesMode[]","UNSET_MODE_VALUES: readonly unknown[]","customFieldsEntriesMode","ENTRIES_WRITING_MODES: readonly CustomFieldsEntriesMode[]","ENTRIES_READING_MODES: readonly CustomFieldsEntriesMode[]","customFieldsEntriesMode: CustomFieldsEntriesMode | undefined"],"sources":["../../../src/utils/custom-fields-mode.ts"],"sourcesContent":["import logger from './logger';\n\n/**\n * How a service uses `custom_field_entries`.\n *\n * - `DISABLED` — values only. The default when the option is omitted.\n * - `WRITE_ONLY` — write both tables, still read from `custom_field_values`.\n * The safe middle stage: entries fills up and stays current while reads are\n * unaffected, so a backfill only has to cover history.\n * - `READ_AND_WRITE` — write both tables, read from `custom_field_entries`.\n *\n * `custom_field_values` is written in every mode, which is what makes moving\n * between stages reversible.\n */\nexport const CUSTOM_FIELDS_ENTRIES_MODE = {\n DISABLED: 'disabled',\n WRITE_ONLY: 'write-only',\n READ_AND_WRITE: 'read-and-write',\n} as const;\n\nexport type CustomFieldsEntriesMode = typeof CUSTOM_FIELDS_ENTRIES_MODE[keyof typeof CUSTOM_FIELDS_ENTRIES_MODE];\n\nconst CUSTOM_FIELDS_ENTRIES_MODES: readonly CustomFieldsEntriesMode[] = Object.values(CUSTOM_FIELDS_ENTRIES_MODE);\n\nconst isCustomFieldsEntriesMode = (value: unknown): value is CustomFieldsEntriesMode =>\n CUSTOM_FIELDS_ENTRIES_MODES.includes(value as CustomFieldsEntriesMode);\n\nexport interface CustomFieldsEntriesModeOptions {\n customFieldsEntriesMode?: CustomFieldsEntriesMode;\n /** @deprecated Use `customFieldsEntriesMode` instead. */\n useCustomFieldsEntries?: boolean;\n}\n\nconst DEPRECATION_MESSAGE = 'sadot: `useCustomFieldsEntries` is deprecated, use `customFieldsEntriesMode`'\n + ` with one of ${Object.values(CUSTOM_FIELDS_ENTRIES_MODE).map(mode => `'${mode}'`).join(' | ')}`;\n\n/**\n * Resolves the effective mode from the new option and the deprecated boolean.\n *\n * The legacy boolean keeps working so services can bump the package before they\n * rename the option, but an unrecognised mode is a typo and throws rather than\n * silently degrading to `DISABLED` — which would stop entries being written.\n */\n// `null` and an empty string mean \"not configured\" rather than \"typo\" — a settings\n// lookup or an env var that came back empty. The public option type does not admit\n// them, but a JS caller can still pass them.\nconst UNSET_MODE_VALUES: readonly unknown[] = [undefined, null, '', false];\n\nexport const resolveCustomFieldsEntriesMode = (options: CustomFieldsEntriesModeOptions = {}): CustomFieldsEntriesMode => {\n const { useCustomFieldsEntries } = options;\n\n const rawMode = options.customFieldsEntriesMode as unknown;\n const customFieldsEntriesMode = UNSET_MODE_VALUES.includes(rawMode) ? undefined : rawMode;\n\n if (customFieldsEntriesMode !== undefined) {\n if (!isCustomFieldsEntriesMode(customFieldsEntriesMode)) {\n throw new Error(\n `sadot: unknown customFieldsEntriesMode ${JSON.stringify(customFieldsEntriesMode)}.`\n + ` Expected one of ${CUSTOM_FIELDS_ENTRIES_MODES.map(mode => `'${mode}'`).join(' | ')}`,\n );\n }\n if (useCustomFieldsEntries !== undefined) {\n logger.warn(`${DEPRECATION_MESSAGE}. Both were passed; \\`useCustomFieldsEntries\\` is ignored`);\n }\n return customFieldsEntriesMode;\n }\n\n if (useCustomFieldsEntries !== undefined) {\n logger.warn(DEPRECATION_MESSAGE);\n return useCustomFieldsEntries\n ? CUSTOM_FIELDS_ENTRIES_MODE.READ_AND_WRITE\n : CUSTOM_FIELDS_ENTRIES_MODE.DISABLED;\n }\n\n return CUSTOM_FIELDS_ENTRIES_MODE.DISABLED;\n};\n\n// Both predicates accept `undefined` so the \"no mode means DISABLED\" rule lives here\n// rather than being repeated at every call site. Publicly exported scopes can be\n// invoked with no options at all.\n\nconst ENTRIES_WRITING_MODES: readonly CustomFieldsEntriesMode[] = [\n CUSTOM_FIELDS_ENTRIES_MODE.WRITE_ONLY,\n CUSTOM_FIELDS_ENTRIES_MODE.READ_AND_WRITE,\n];\nconst ENTRIES_READING_MODES: readonly CustomFieldsEntriesMode[] = [CUSTOM_FIELDS_ENTRIES_MODE.READ_AND_WRITE];\n\n/** True when `custom_field_entries` should be written alongside `custom_field_values`. */\nexport const writesEntries = (mode: CustomFieldsEntriesMode | undefined): boolean =>\n ENTRIES_WRITING_MODES.includes(mode as CustomFieldsEntriesMode);\n\n/** True when reads, filtering and sorting should go to `custom_field_entries`. */\nexport const readsEntries = (mode: CustomFieldsEntriesMode | undefined): boolean =>\n ENTRIES_READING_MODES.includes(mode as CustomFieldsEntriesMode);\n\n// The package-level resolved mode. Set once during `useCustomFields()` so free functions\n// (like the search helper) can read it without every caller threading the option through.\n// Stays undefined in services that never initialize sadot — treated as DISABLED everywhere.\nlet customFieldsEntriesMode: CustomFieldsEntriesMode | undefined;\n\nexport const setCustomFieldsEntriesMode = (mode: CustomFieldsEntriesMode | undefined): void => {\n customFieldsEntriesMode = mode;\n};\n\nexport const getCustomFieldsEntriesMode = (): CustomFieldsEntriesMode | undefined => customFieldsEntriesMode;\n"],"mappings":"iCAcA,MAAa,EAA6B,CACxC,SAAU,WACV,WAAY,aACZ,eAAgB,iBACjB,CAIKA,EAAkE,OAAO,OAAO,EAA2B,CAE3G,EAA6B,GACjC,EAA4B,SAAS,EAAiC,CAQlE,EAAsB,gGACR,OAAO,OAAO,EAA2B,CAAC,IAAI,GAAQ,IAAI,EAAK,GAAG,CAAC,KAAK,MAAM,GAY5FC,EAAwC,CAAC,IAAA,GAAW,KAAM,GAAI,GAAM,CAE7D,GAAkC,EAA0C,EAAE,GAA8B,CACvH,GAAM,CAAE,0BAA2B,EAE7B,EAAU,EAAQ,wBAClBC,EAA0B,EAAkB,SAAS,EAAQ,CAAG,IAAA,GAAY,EAElF,GAAIA,IAA4B,IAAA,GAAW,CACzC,GAAI,CAAC,EAA0BA,EAAwB,CACrD,MAAU,MACR,0CAA0C,KAAK,UAAUA,EAAwB,CAAC,oBAC5D,EAA4B,IAAI,GAAQ,IAAI,EAAK,GAAG,CAAC,KAAK,MAAM,GACvF,CAKH,OAHI,IAA2B,IAAA,IAC7B,EAAO,KAAK,GAAG,EAAoB,2DAA2D,CAEzFA,EAUT,OAPI,IAA2B,IAAA,GAOxB,EAA2B,UANhC,EAAO,KAAK,EAAoB,CACzB,EACH,EAA2B,eAC3B,EAA2B,WAU7BC,EAA4D,CAChE,EAA2B,WAC3B,EAA2B,eAC5B,CACKC,EAA4D,CAAC,EAA2B,eAAe,CAGhG,EAAiB,GAC5B,EAAsB,SAAS,EAAgC,CAGpD,EAAgB,GAC3B,EAAsB,SAAS,EAAgC,CAKjE,IAAIC,EAEJ,MAAa,EAA8B,GAAoD,CAC7F,EAA0B,GAGf,MAAwE"}
1
+ {"version":3,"file":"custom-fields-mode.js","names":["CUSTOM_FIELDS_ENTRIES_MODES: readonly CustomFieldsEntriesMode[]","UNSET_MODE_VALUES: readonly unknown[]"],"sources":["../../../src/utils/custom-fields-mode.ts"],"sourcesContent":["import logger from './logger';\n\n/**\n * How a service uses `custom_field_entries`.\n *\n * - `DISABLED` — values only. The default when the option is omitted.\n * - `WRITE_ONLY` — write both tables, still read from `custom_field_values`.\n * The safe middle stage: entries fills up and stays current while reads are\n * unaffected, so a backfill only has to cover history.\n * - `READ_AND_WRITE` — write both tables, read from `custom_field_entries`.\n *\n * `custom_field_values` is written in every mode, which is what makes moving\n * between stages reversible.\n */\nexport const CUSTOM_FIELDS_ENTRIES_MODE = {\n DISABLED: 'disabled',\n WRITE_ONLY: 'write-only',\n READ_AND_WRITE: 'read-and-write',\n} as const;\n\nexport type CustomFieldsEntriesMode = typeof CUSTOM_FIELDS_ENTRIES_MODE[keyof typeof CUSTOM_FIELDS_ENTRIES_MODE];\n\nconst CUSTOM_FIELDS_ENTRIES_MODES: readonly CustomFieldsEntriesMode[] = Object.values(CUSTOM_FIELDS_ENTRIES_MODE);\n\nconst isCustomFieldsEntriesMode = (value: unknown): value is CustomFieldsEntriesMode =>\n CUSTOM_FIELDS_ENTRIES_MODES.includes(value as CustomFieldsEntriesMode);\n\nexport interface CustomFieldsEntriesModeOptions {\n customFieldsEntriesMode?: CustomFieldsEntriesMode;\n /** @deprecated Use `customFieldsEntriesMode` instead. */\n useCustomFieldsEntries?: boolean;\n}\n\nconst DEPRECATION_MESSAGE = 'sadot: `useCustomFieldsEntries` is deprecated, use `customFieldsEntriesMode`'\n + ` with one of ${Object.values(CUSTOM_FIELDS_ENTRIES_MODE).map(mode => `'${mode}'`).join(' | ')}`;\n\n/**\n * Resolves the effective mode from the new option and the deprecated boolean.\n *\n * The legacy boolean keeps working so services can bump the package before they\n * rename the option, but an unrecognised mode is a typo and throws rather than\n * silently degrading to `DISABLED` — which would stop entries being written.\n */\n// `null` and an empty string mean \"not configured\" rather than \"typo\" — a settings\n// lookup or an env var that came back empty. The public option type does not admit\n// them, but a JS caller can still pass them.\nconst UNSET_MODE_VALUES: readonly unknown[] = [undefined, null, '', false];\n\nexport const resolveCustomFieldsEntriesMode = (options: CustomFieldsEntriesModeOptions = {}): CustomFieldsEntriesMode => {\n const { useCustomFieldsEntries } = options;\n\n const rawMode = options.customFieldsEntriesMode as unknown;\n const customFieldsEntriesMode = UNSET_MODE_VALUES.includes(rawMode) ? undefined : rawMode;\n\n if (customFieldsEntriesMode !== undefined) {\n if (!isCustomFieldsEntriesMode(customFieldsEntriesMode)) {\n throw new Error(\n `sadot: unknown customFieldsEntriesMode ${JSON.stringify(customFieldsEntriesMode)}.`\n + ` Expected one of ${CUSTOM_FIELDS_ENTRIES_MODES.map(mode => `'${mode}'`).join(' | ')}`,\n );\n }\n if (useCustomFieldsEntries !== undefined) {\n logger.warn(`${DEPRECATION_MESSAGE}. Both were passed; \\`useCustomFieldsEntries\\` is ignored`);\n }\n return customFieldsEntriesMode;\n }\n\n if (useCustomFieldsEntries !== undefined) {\n logger.warn(DEPRECATION_MESSAGE);\n return useCustomFieldsEntries\n ? CUSTOM_FIELDS_ENTRIES_MODE.READ_AND_WRITE\n : CUSTOM_FIELDS_ENTRIES_MODE.DISABLED;\n }\n\n return CUSTOM_FIELDS_ENTRIES_MODE.DISABLED;\n};\n\n// Both predicates accept `undefined` so the \"no mode means DISABLED\" rule lives here\n// rather than being repeated at every call site.\n\n/** True when `custom_field_entries` should be written alongside `custom_field_values`. */\nexport const writesEntries = (mode: CustomFieldsEntriesMode | undefined): boolean =>\n mode === CUSTOM_FIELDS_ENTRIES_MODE.WRITE_ONLY || mode === CUSTOM_FIELDS_ENTRIES_MODE.READ_AND_WRITE;\n\n/** True when reads, filtering, sorting and search should go to `custom_field_entries`. */\nexport const readsEntries = (mode: CustomFieldsEntriesMode | undefined): boolean =>\n mode === CUSTOM_FIELDS_ENTRIES_MODE.READ_AND_WRITE;\n"],"mappings":"iCAcA,MAAa,EAA6B,CACxC,SAAU,WACV,WAAY,aACZ,eAAgB,iBACjB,CAIKA,EAAkE,OAAO,OAAO,EAA2B,CAE3G,EAA6B,GACjC,EAA4B,SAAS,EAAiC,CAQlE,EAAsB,gGACR,OAAO,OAAO,EAA2B,CAAC,IAAI,GAAQ,IAAI,EAAK,GAAG,CAAC,KAAK,MAAM,GAY5FC,EAAwC,CAAC,IAAA,GAAW,KAAM,GAAI,GAAM,CAE7D,GAAkC,EAA0C,EAAE,GAA8B,CACvH,GAAM,CAAE,0BAA2B,EAE7B,EAAU,EAAQ,wBAClB,EAA0B,EAAkB,SAAS,EAAQ,CAAG,IAAA,GAAY,EAElF,GAAI,IAA4B,IAAA,GAAW,CACzC,GAAI,CAAC,EAA0B,EAAwB,CACrD,MAAU,MACR,0CAA0C,KAAK,UAAU,EAAwB,CAAC,oBAC5D,EAA4B,IAAI,GAAQ,IAAI,EAAK,GAAG,CAAC,KAAK,MAAM,GACvF,CAKH,OAHI,IAA2B,IAAA,IAC7B,EAAO,KAAK,GAAG,EAAoB,2DAA2D,CAEzF,EAUT,OAPI,IAA2B,IAAA,GAOxB,EAA2B,UANhC,EAAO,KAAK,EAAoB,CACzB,EACH,EAA2B,eAC3B,EAA2B,WAUtB,EAAiB,GAC5B,IAAS,EAA2B,YAAc,IAAS,EAA2B,eAG3E,EAAgB,GAC3B,IAAS,EAA2B"}
@@ -24,5 +24,5 @@ const e=require(`../../../_virtual/rolldown_runtime.cjs`),t=require(`../constant
24
24
  AND cd.deleted_at IS NULL${s(e)}
25
25
  WHERE kv.value ILIKE :searchTerm
26
26
  )
27
- `,u=(e,a,o,s=[t.CustomFieldDefinitionType.DATETIME,t.CustomFieldDefinitionType.DATE])=>{let u=s.length>0,d=n.readsEntries(n.getCustomFieldsEntriesMode())?l(u):c(u);return{where:{id:{[r.Op.in]:i.Sequelize.literal(`(${d})`)}},replacements:{searchTerm:`%${e}%`,customFieldsSearchEntityId:o,customFieldsSearchModelType:a.name,...u?{customFieldsSearchExcludedTypes:s}:{}}}};exports.generateCustomFieldSearchQueryPayload=u,exports.generateRandomString=o;
27
+ `,u=(e,a,o,s=[t.CustomFieldDefinitionType.DATETIME,t.CustomFieldDefinitionType.DATE],u)=>{let d=s.length>0,f=n.readsEntries(u?.customFieldsEntriesMode)?l(d):c(d);return{where:{id:{[r.Op.in]:i.Sequelize.literal(`(${f})`)}},replacements:{searchTerm:`%${e}%`,customFieldsSearchEntityId:o,customFieldsSearchModelType:a.name,...d?{customFieldsSearchExcludedTypes:s}:{}}}};exports.generateCustomFieldSearchQueryPayload=u,exports.generateRandomString=o;
28
28
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["entriesValuesTextExpression","CustomFieldDefinitionType","readsEntries","getCustomFieldsEntriesMode","Op","Sequelize"],"sources":["../../../../src/utils/helpers/index.ts"],"sourcesContent":["import { type WhereOptions, Op, type BindOrReplacements } from 'sequelize';\nimport { type ModelStatic, Sequelize } from 'sequelize-typescript';\nimport { randomInt } from 'node:crypto';\nimport { CustomFieldDefinitionType, entriesValuesTextExpression } from '../constants';\nimport { getCustomFieldsEntriesMode, readsEntries } from '../custom-fields-mode';\n\n/**\n * Builds a WHERE clause and replacements for free-text search by custom fields.\n *\n * This function constructs a WHERE clause and replacement bindings that allow searching\n * for a given term within custom fields associated with a specific model type and entity ID.\n * The WHERE clause and replacements are designed to be added to the main query.\n *\n * The subquery reads `custom_field_entries` when the service's entries mode (captured at\n * `useCustomFields()` init) reads entries, and `custom_field_values` otherwise.\n *\n * @param {string} searchTerm - The term to search for within custom fields.\n * @param {ModelStatic} model - The Sequelize model representing the entity type to search for.\n * @param {string} entityId - The entity ID to filter the custom fields by.\n * @param {CustomFieldDefinitionType[]} excludedCustomFieldsTypes - An array of custom field types\n * to exclude from the search\n *\n * @returns {CustomFieldsSearchPayload} - An object containing the WHERE clause and replacements\n * for Sequelize.\n */\n\ninterface CustomFieldsSearchPayload {\n where: WhereOptions;\n replacements: BindOrReplacements;\n}\n\nexport const generateRandomString = (length = 5): string => {\n const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\n return Array.from({ length }, () => characters.charAt(randomInt(characters.length))).join('');\n};\n\nconst excludedTypesPredicate = (hasExcludedTypes: boolean): string =>\n (hasExcludedTypes ? ' AND cd.field_type NOT IN (:customFieldsSearchExcludedTypes)' : '');\n\nconst buildValuesSubQuery = (hasExcludedTypes: boolean): string => `\n SELECT cv.model_id\n FROM \"custom_field_values\" AS cv\n INNER JOIN custom_field_definitions AS cd\n ON cv.custom_field_definition_id = cd.id\n AND cd.entity_id = :customFieldsSearchEntityId\n AND cd.model_type = :customFieldsSearchModelType\n AND cd.deleted_at IS NULL${excludedTypesPredicate(hasExcludedTypes)}\n WHERE cv.deleted_at IS NULL\n AND CAST(cv.value AS TEXT) ILIKE :searchTerm\n`;\n\n// The first ILIKE is served by the trigram expression index (migration 004). The EXISTS is\n// the exact per-value check: the indexed predicate alone can match a term straddling two\n// adjacent values in the extracted text. The definitions join keeps parity with the values\n// path — a key matches only when backed by a live definition whose type is not excluded.\nconst buildEntriesSubQuery = (hasExcludedTypes: boolean): string => `\n SELECT ce.model_id\n FROM custom_field_entries AS ce\n WHERE ce.model_type = :customFieldsSearchModelType\n AND ce.entity_id = :customFieldsSearchEntityId\n AND ${entriesValuesTextExpression('ce.custom_fields')} ILIKE :searchTerm\n AND EXISTS (\n SELECT 1\n FROM jsonb_each_text(ce.custom_fields) AS kv\n INNER JOIN custom_field_definitions AS cd\n ON cd.name = kv.key\n AND cd.entity_id = :customFieldsSearchEntityId\n AND cd.model_type = :customFieldsSearchModelType\n AND cd.deleted_at IS NULL${excludedTypesPredicate(hasExcludedTypes)}\n WHERE kv.value ILIKE :searchTerm\n )\n`;\n\nexport const generateCustomFieldSearchQueryPayload = (\n searchTerm: string,\n model: ModelStatic,\n entityId: string,\n customFieldsTypesToExclude: CustomFieldDefinitionType[] = [\n CustomFieldDefinitionType.DATETIME,\n CustomFieldDefinitionType.DATE,\n ],\n): CustomFieldsSearchPayload => {\n const hasExcludedTypes = customFieldsTypesToExclude.length > 0;\n const subQuery = readsEntries(getCustomFieldsEntriesMode())\n ? buildEntriesSubQuery(hasExcludedTypes)\n : buildValuesSubQuery(hasExcludedTypes);\n\n return {\n where: {\n id: {\n [Op.in]: Sequelize.literal(`(${subQuery})`),\n },\n },\n replacements: {\n searchTerm: `%${searchTerm}%`,\n customFieldsSearchEntityId: entityId,\n customFieldsSearchModelType: model.name,\n ...(hasExcludedTypes\n ? { customFieldsSearchExcludedTypes: customFieldsTypesToExclude }\n : {}),\n },\n };\n};\n"],"mappings":"2NA+BA,MAAa,GAAwB,EAAS,IAErC,MAAM,KAAK,CAAE,SAAQ,KAAQ,uDAAW,QAAA,EAAA,EAAA,WAAiB,GAAkB,CAAC,CAAC,CAAC,KAAK,GAAG,CAGzF,EAA0B,GAC7B,EAAmB,+DAAiE,GAEjF,EAAuB,GAAsC;;;;;;;8BAOrC,EAAuB,EAAiB,CAAC;;;EASjE,EAAwB,GAAsC;;;;;UAK1DA,EAAAA,4BAA4B,mBAAmB,CAAC;;;;;;;;kCAQxB,EAAuB,EAAiB,CAAC;;;EAK9D,GACX,EACA,EACA,EACA,EAA0D,CACxDC,EAAAA,0BAA0B,SAC1BA,EAAAA,0BAA0B,KAC3B,GAC6B,CAC9B,IAAM,EAAmB,EAA2B,OAAS,EACvD,EAAWC,EAAAA,aAAaC,EAAAA,4BAA4B,CAAC,CACvD,EAAqB,EAAiB,CACtC,EAAoB,EAAiB,CAEzC,MAAO,CACL,MAAO,CACL,GAAI,EACDC,EAAAA,GAAG,IAAKC,EAAAA,UAAU,QAAQ,IAAI,EAAS,GAAG,CAC5C,CACF,CACD,aAAc,CACZ,WAAY,IAAI,EAAW,GAC3B,2BAA4B,EAC5B,4BAA6B,EAAM,KACnC,GAAI,EACA,CAAE,gCAAiC,EAA4B,CAC/D,EAAE,CACP,CACF"}
1
+ {"version":3,"file":"index.cjs","names":["entriesValuesTextExpression","CustomFieldDefinitionType","readsEntries","Op","Sequelize"],"sources":["../../../../src/utils/helpers/index.ts"],"sourcesContent":["import { type WhereOptions, Op, type BindOrReplacements } from 'sequelize';\nimport { type ModelStatic, Sequelize } from 'sequelize-typescript';\nimport { randomInt } from 'node:crypto';\nimport { CustomFieldDefinitionType, entriesValuesTextExpression } from '../constants';\nimport { readsEntries } from '../custom-fields-mode';\nimport type { CustomFieldOptions } from '../../types';\n\n/**\n * Builds a WHERE clause and replacements for free-text search by custom fields.\n *\n * This function constructs a WHERE clause and replacement bindings that allow searching\n * for a given term within custom fields associated with a specific model type and entity ID.\n * The WHERE clause and replacements are designed to be added to the main query.\n *\n * The subquery reads `custom_field_entries` when the caller's entries mode reads entries, and\n * `custom_field_values` otherwise — the same contract the filter and sort scopes follow. A\n * caller that passes no mode keeps the legacy values behavior.\n *\n * The entries subquery is served by the trigram index from migration 004, which only builds for\n * callers that pass `useEntriesSearchIndex` to `runSadotMigrations`. Without it the query is\n * still correct, just a sequential scan.\n *\n * @param {string} searchTerm - The term to search for within custom fields.\n * @param {ModelStatic} model - The Sequelize model representing the entity type to search for.\n * @param {string} entityId - The entity ID to filter the custom fields by.\n * @param {CustomFieldDefinitionType[]} excludedCustomFieldsTypes - An array of custom field types\n * to exclude from the search\n * @param {CustomFieldOptions} options - Carries the caller's `customFieldsEntriesMode`.\n *\n * @returns {CustomFieldsSearchPayload} - An object containing the WHERE clause and replacements\n * for Sequelize.\n */\n\ninterface CustomFieldsSearchPayload {\n where: WhereOptions;\n replacements: BindOrReplacements;\n}\n\nexport const generateRandomString = (length = 5): string => {\n const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\n return Array.from({ length }, () => characters.charAt(randomInt(characters.length))).join('');\n};\n\nconst excludedTypesPredicate = (hasExcludedTypes: boolean): string =>\n (hasExcludedTypes ? ' AND cd.field_type NOT IN (:customFieldsSearchExcludedTypes)' : '');\n\nconst buildValuesSubQuery = (hasExcludedTypes: boolean): string => `\n SELECT cv.model_id\n FROM \"custom_field_values\" AS cv\n INNER JOIN custom_field_definitions AS cd\n ON cv.custom_field_definition_id = cd.id\n AND cd.entity_id = :customFieldsSearchEntityId\n AND cd.model_type = :customFieldsSearchModelType\n AND cd.deleted_at IS NULL${excludedTypesPredicate(hasExcludedTypes)}\n WHERE cv.deleted_at IS NULL\n AND CAST(cv.value AS TEXT) ILIKE :searchTerm\n`;\n\nconst buildEntriesSubQuery = (hasExcludedTypes: boolean): string => `\n SELECT ce.model_id\n FROM custom_field_entries AS ce\n WHERE ce.model_type = :customFieldsSearchModelType\n AND ce.entity_id = :customFieldsSearchEntityId\n AND ${entriesValuesTextExpression('ce.custom_fields')} ILIKE :searchTerm\n AND EXISTS (\n SELECT 1\n FROM jsonb_each_text(ce.custom_fields) AS kv\n INNER JOIN custom_field_definitions AS cd\n ON cd.name = kv.key\n AND cd.entity_id = :customFieldsSearchEntityId\n AND cd.model_type = :customFieldsSearchModelType\n AND cd.deleted_at IS NULL${excludedTypesPredicate(hasExcludedTypes)}\n WHERE kv.value ILIKE :searchTerm\n )\n`;\n\nexport const generateCustomFieldSearchQueryPayload = (\n searchTerm: string,\n model: ModelStatic,\n entityId: string,\n customFieldsTypesToExclude: CustomFieldDefinitionType[] = [\n CustomFieldDefinitionType.DATETIME,\n CustomFieldDefinitionType.DATE,\n ],\n options?: Pick<CustomFieldOptions, 'customFieldsEntriesMode'>,\n): CustomFieldsSearchPayload => {\n const hasExcludedTypes = customFieldsTypesToExclude.length > 0;\n const subQuery = readsEntries(options?.customFieldsEntriesMode)\n ? buildEntriesSubQuery(hasExcludedTypes)\n : buildValuesSubQuery(hasExcludedTypes);\n\n return {\n where: {\n id: {\n [Op.in]: Sequelize.literal(`(${subQuery})`),\n },\n },\n replacements: {\n searchTerm: `%${searchTerm}%`,\n customFieldsSearchEntityId: entityId,\n customFieldsSearchModelType: model.name,\n ...(hasExcludedTypes\n ? { customFieldsSearchExcludedTypes: customFieldsTypesToExclude }\n : {}),\n },\n };\n};\n"],"mappings":"2NAsCA,MAAa,GAAwB,EAAS,IAErC,MAAM,KAAK,CAAE,SAAQ,KAAQ,uDAAW,QAAA,EAAA,EAAA,WAAiB,GAAkB,CAAC,CAAC,CAAC,KAAK,GAAG,CAGzF,EAA0B,GAC7B,EAAmB,+DAAiE,GAEjF,EAAuB,GAAsC;;;;;;;8BAOrC,EAAuB,EAAiB,CAAC;;;EAKjE,EAAwB,GAAsC;;;;;UAK1DA,EAAAA,4BAA4B,mBAAmB,CAAC;;;;;;;;kCAQxB,EAAuB,EAAiB,CAAC;;;EAK9D,GACX,EACA,EACA,EACA,EAA0D,CACxDC,EAAAA,0BAA0B,SAC1BA,EAAAA,0BAA0B,KAC3B,CACD,IAC8B,CAC9B,IAAM,EAAmB,EAA2B,OAAS,EACvD,EAAWC,EAAAA,aAAa,GAAS,wBAAwB,CAC3D,EAAqB,EAAiB,CACtC,EAAoB,EAAiB,CAEzC,MAAO,CACL,MAAO,CACL,GAAI,EACDC,EAAAA,GAAG,IAAKC,EAAAA,UAAU,QAAQ,IAAI,EAAS,GAAG,CAC5C,CACF,CACD,aAAc,CACZ,WAAY,IAAI,EAAW,GAC3B,2BAA4B,EAC5B,4BAA6B,EAAM,KACnC,GAAI,EACA,CAAE,gCAAiC,EAA4B,CAC/D,EAAE,CACP,CACF"}
@@ -1,4 +1,5 @@
1
1
  import { CustomFieldDefinitionType } from "../constants/index.js";
2
+ import { CustomFieldOptions } from "../../types/index.js";
2
3
  import { BindOrReplacements, WhereOptions } from "sequelize";
3
4
  import { ModelStatic } from "sequelize-typescript";
4
5
 
@@ -11,14 +12,20 @@ import { ModelStatic } from "sequelize-typescript";
11
12
  * for a given term within custom fields associated with a specific model type and entity ID.
12
13
  * The WHERE clause and replacements are designed to be added to the main query.
13
14
  *
14
- * The subquery reads `custom_field_entries` when the service's entries mode (captured at
15
- * `useCustomFields()` init) reads entries, and `custom_field_values` otherwise.
15
+ * The subquery reads `custom_field_entries` when the caller's entries mode reads entries, and
16
+ * `custom_field_values` otherwise — the same contract the filter and sort scopes follow. A
17
+ * caller that passes no mode keeps the legacy values behavior.
18
+ *
19
+ * The entries subquery is served by the trigram index from migration 004, which only builds for
20
+ * callers that pass `useEntriesSearchIndex` to `runSadotMigrations`. Without it the query is
21
+ * still correct, just a sequential scan.
16
22
  *
17
23
  * @param {string} searchTerm - The term to search for within custom fields.
18
24
  * @param {ModelStatic} model - The Sequelize model representing the entity type to search for.
19
25
  * @param {string} entityId - The entity ID to filter the custom fields by.
20
26
  * @param {CustomFieldDefinitionType[]} excludedCustomFieldsTypes - An array of custom field types
21
27
  * to exclude from the search
28
+ * @param {CustomFieldOptions} options - Carries the caller's `customFieldsEntriesMode`.
22
29
  *
23
30
  * @returns {CustomFieldsSearchPayload} - An object containing the WHERE clause and replacements
24
31
  * for Sequelize.
@@ -28,7 +35,7 @@ interface CustomFieldsSearchPayload {
28
35
  replacements: BindOrReplacements;
29
36
  }
30
37
  declare const generateRandomString: (length?: number) => string;
31
- declare const generateCustomFieldSearchQueryPayload: (searchTerm: string, model: ModelStatic, entityId: string, customFieldsTypesToExclude?: CustomFieldDefinitionType[]) => CustomFieldsSearchPayload;
38
+ declare const generateCustomFieldSearchQueryPayload: (searchTerm: string, model: ModelStatic, entityId: string, customFieldsTypesToExclude?: CustomFieldDefinitionType[], options?: Pick<CustomFieldOptions, "customFieldsEntriesMode">) => CustomFieldsSearchPayload;
32
39
  //#endregion
33
40
  export { generateCustomFieldSearchQueryPayload, generateRandomString };
34
41
  //# sourceMappingURL=index.d.ts.map
@@ -1,14 +1,14 @@
1
- import{CustomFieldDefinitionType as e,entriesValuesTextExpression as t}from"../constants/index.js";import{getCustomFieldsEntriesMode as n,readsEntries as r}from"../custom-fields-mode.js";import{Op as i}from"sequelize";import{Sequelize as a}from"sequelize-typescript";import{randomInt as o}from"node:crypto";const s=(e=5)=>Array.from({length:e},()=>`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`.charAt(o(52))).join(``),c=e=>e?` AND cd.field_type NOT IN (:customFieldsSearchExcludedTypes)`:``,l=e=>`
1
+ import{CustomFieldDefinitionType as e,entriesValuesTextExpression as t}from"../constants/index.js";import{readsEntries as n}from"../custom-fields-mode.js";import{Op as r}from"sequelize";import{Sequelize as i}from"sequelize-typescript";import{randomInt as a}from"node:crypto";const o=(e=5)=>Array.from({length:e},()=>`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`.charAt(a(52))).join(``),s=e=>e?` AND cd.field_type NOT IN (:customFieldsSearchExcludedTypes)`:``,c=e=>`
2
2
  SELECT cv.model_id
3
3
  FROM "custom_field_values" AS cv
4
4
  INNER JOIN custom_field_definitions AS cd
5
5
  ON cv.custom_field_definition_id = cd.id
6
6
  AND cd.entity_id = :customFieldsSearchEntityId
7
7
  AND cd.model_type = :customFieldsSearchModelType
8
- AND cd.deleted_at IS NULL${c(e)}
8
+ AND cd.deleted_at IS NULL${s(e)}
9
9
  WHERE cv.deleted_at IS NULL
10
10
  AND CAST(cv.value AS TEXT) ILIKE :searchTerm
11
- `,u=e=>`
11
+ `,l=e=>`
12
12
  SELECT ce.model_id
13
13
  FROM custom_field_entries AS ce
14
14
  WHERE ce.model_type = :customFieldsSearchModelType
@@ -21,8 +21,8 @@ import{CustomFieldDefinitionType as e,entriesValuesTextExpression as t}from"../c
21
21
  ON cd.name = kv.key
22
22
  AND cd.entity_id = :customFieldsSearchEntityId
23
23
  AND cd.model_type = :customFieldsSearchModelType
24
- AND cd.deleted_at IS NULL${c(e)}
24
+ AND cd.deleted_at IS NULL${s(e)}
25
25
  WHERE kv.value ILIKE :searchTerm
26
26
  )
27
- `,d=(t,o,s,c=[e.DATETIME,e.DATE])=>{let d=c.length>0,f=r(n())?u(d):l(d);return{where:{id:{[i.in]:a.literal(`(${f})`)}},replacements:{searchTerm:`%${t}%`,customFieldsSearchEntityId:s,customFieldsSearchModelType:o.name,...d?{customFieldsSearchExcludedTypes:c}:{}}}};export{d as generateCustomFieldSearchQueryPayload,s as generateRandomString};
27
+ `,u=(t,a,o,s=[e.DATETIME,e.DATE],u)=>{let d=s.length>0,f=n(u?.customFieldsEntriesMode)?l(d):c(d);return{where:{id:{[r.in]:i.literal(`(${f})`)}},replacements:{searchTerm:`%${t}%`,customFieldsSearchEntityId:o,customFieldsSearchModelType:a.name,...d?{customFieldsSearchExcludedTypes:s}:{}}}};export{u as generateCustomFieldSearchQueryPayload,o as generateRandomString};
28
28
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["Sequelize"],"sources":["../../../../src/utils/helpers/index.ts"],"sourcesContent":["import { type WhereOptions, Op, type BindOrReplacements } from 'sequelize';\nimport { type ModelStatic, Sequelize } from 'sequelize-typescript';\nimport { randomInt } from 'node:crypto';\nimport { CustomFieldDefinitionType, entriesValuesTextExpression } from '../constants';\nimport { getCustomFieldsEntriesMode, readsEntries } from '../custom-fields-mode';\n\n/**\n * Builds a WHERE clause and replacements for free-text search by custom fields.\n *\n * This function constructs a WHERE clause and replacement bindings that allow searching\n * for a given term within custom fields associated with a specific model type and entity ID.\n * The WHERE clause and replacements are designed to be added to the main query.\n *\n * The subquery reads `custom_field_entries` when the service's entries mode (captured at\n * `useCustomFields()` init) reads entries, and `custom_field_values` otherwise.\n *\n * @param {string} searchTerm - The term to search for within custom fields.\n * @param {ModelStatic} model - The Sequelize model representing the entity type to search for.\n * @param {string} entityId - The entity ID to filter the custom fields by.\n * @param {CustomFieldDefinitionType[]} excludedCustomFieldsTypes - An array of custom field types\n * to exclude from the search\n *\n * @returns {CustomFieldsSearchPayload} - An object containing the WHERE clause and replacements\n * for Sequelize.\n */\n\ninterface CustomFieldsSearchPayload {\n where: WhereOptions;\n replacements: BindOrReplacements;\n}\n\nexport const generateRandomString = (length = 5): string => {\n const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\n return Array.from({ length }, () => characters.charAt(randomInt(characters.length))).join('');\n};\n\nconst excludedTypesPredicate = (hasExcludedTypes: boolean): string =>\n (hasExcludedTypes ? ' AND cd.field_type NOT IN (:customFieldsSearchExcludedTypes)' : '');\n\nconst buildValuesSubQuery = (hasExcludedTypes: boolean): string => `\n SELECT cv.model_id\n FROM \"custom_field_values\" AS cv\n INNER JOIN custom_field_definitions AS cd\n ON cv.custom_field_definition_id = cd.id\n AND cd.entity_id = :customFieldsSearchEntityId\n AND cd.model_type = :customFieldsSearchModelType\n AND cd.deleted_at IS NULL${excludedTypesPredicate(hasExcludedTypes)}\n WHERE cv.deleted_at IS NULL\n AND CAST(cv.value AS TEXT) ILIKE :searchTerm\n`;\n\n// The first ILIKE is served by the trigram expression index (migration 004). The EXISTS is\n// the exact per-value check: the indexed predicate alone can match a term straddling two\n// adjacent values in the extracted text. The definitions join keeps parity with the values\n// path — a key matches only when backed by a live definition whose type is not excluded.\nconst buildEntriesSubQuery = (hasExcludedTypes: boolean): string => `\n SELECT ce.model_id\n FROM custom_field_entries AS ce\n WHERE ce.model_type = :customFieldsSearchModelType\n AND ce.entity_id = :customFieldsSearchEntityId\n AND ${entriesValuesTextExpression('ce.custom_fields')} ILIKE :searchTerm\n AND EXISTS (\n SELECT 1\n FROM jsonb_each_text(ce.custom_fields) AS kv\n INNER JOIN custom_field_definitions AS cd\n ON cd.name = kv.key\n AND cd.entity_id = :customFieldsSearchEntityId\n AND cd.model_type = :customFieldsSearchModelType\n AND cd.deleted_at IS NULL${excludedTypesPredicate(hasExcludedTypes)}\n WHERE kv.value ILIKE :searchTerm\n )\n`;\n\nexport const generateCustomFieldSearchQueryPayload = (\n searchTerm: string,\n model: ModelStatic,\n entityId: string,\n customFieldsTypesToExclude: CustomFieldDefinitionType[] = [\n CustomFieldDefinitionType.DATETIME,\n CustomFieldDefinitionType.DATE,\n ],\n): CustomFieldsSearchPayload => {\n const hasExcludedTypes = customFieldsTypesToExclude.length > 0;\n const subQuery = readsEntries(getCustomFieldsEntriesMode())\n ? buildEntriesSubQuery(hasExcludedTypes)\n : buildValuesSubQuery(hasExcludedTypes);\n\n return {\n where: {\n id: {\n [Op.in]: Sequelize.literal(`(${subQuery})`),\n },\n },\n replacements: {\n searchTerm: `%${searchTerm}%`,\n customFieldsSearchEntityId: entityId,\n customFieldsSearchModelType: model.name,\n ...(hasExcludedTypes\n ? { customFieldsSearchExcludedTypes: customFieldsTypesToExclude }\n : {}),\n },\n };\n};\n"],"mappings":"mTA+BA,MAAa,GAAwB,EAAS,IAErC,MAAM,KAAK,CAAE,SAAQ,KAAQ,uDAAW,OAAO,EAAU,GAAkB,CAAC,CAAC,CAAC,KAAK,GAAG,CAGzF,EAA0B,GAC7B,EAAmB,+DAAiE,GAEjF,EAAuB,GAAsC;;;;;;;8BAOrC,EAAuB,EAAiB,CAAC;;;EASjE,EAAwB,GAAsC;;;;;UAK1D,EAA4B,mBAAmB,CAAC;;;;;;;;kCAQxB,EAAuB,EAAiB,CAAC;;;EAK9D,GACX,EACA,EACA,EACA,EAA0D,CACxD,EAA0B,SAC1B,EAA0B,KAC3B,GAC6B,CAC9B,IAAM,EAAmB,EAA2B,OAAS,EACvD,EAAW,EAAa,GAA4B,CAAC,CACvD,EAAqB,EAAiB,CACtC,EAAoB,EAAiB,CAEzC,MAAO,CACL,MAAO,CACL,GAAI,EACD,EAAG,IAAKA,EAAU,QAAQ,IAAI,EAAS,GAAG,CAC5C,CACF,CACD,aAAc,CACZ,WAAY,IAAI,EAAW,GAC3B,2BAA4B,EAC5B,4BAA6B,EAAM,KACnC,GAAI,EACA,CAAE,gCAAiC,EAA4B,CAC/D,EAAE,CACP,CACF"}
1
+ {"version":3,"file":"index.js","names":["Sequelize"],"sources":["../../../../src/utils/helpers/index.ts"],"sourcesContent":["import { type WhereOptions, Op, type BindOrReplacements } from 'sequelize';\nimport { type ModelStatic, Sequelize } from 'sequelize-typescript';\nimport { randomInt } from 'node:crypto';\nimport { CustomFieldDefinitionType, entriesValuesTextExpression } from '../constants';\nimport { readsEntries } from '../custom-fields-mode';\nimport type { CustomFieldOptions } from '../../types';\n\n/**\n * Builds a WHERE clause and replacements for free-text search by custom fields.\n *\n * This function constructs a WHERE clause and replacement bindings that allow searching\n * for a given term within custom fields associated with a specific model type and entity ID.\n * The WHERE clause and replacements are designed to be added to the main query.\n *\n * The subquery reads `custom_field_entries` when the caller's entries mode reads entries, and\n * `custom_field_values` otherwise — the same contract the filter and sort scopes follow. A\n * caller that passes no mode keeps the legacy values behavior.\n *\n * The entries subquery is served by the trigram index from migration 004, which only builds for\n * callers that pass `useEntriesSearchIndex` to `runSadotMigrations`. Without it the query is\n * still correct, just a sequential scan.\n *\n * @param {string} searchTerm - The term to search for within custom fields.\n * @param {ModelStatic} model - The Sequelize model representing the entity type to search for.\n * @param {string} entityId - The entity ID to filter the custom fields by.\n * @param {CustomFieldDefinitionType[]} excludedCustomFieldsTypes - An array of custom field types\n * to exclude from the search\n * @param {CustomFieldOptions} options - Carries the caller's `customFieldsEntriesMode`.\n *\n * @returns {CustomFieldsSearchPayload} - An object containing the WHERE clause and replacements\n * for Sequelize.\n */\n\ninterface CustomFieldsSearchPayload {\n where: WhereOptions;\n replacements: BindOrReplacements;\n}\n\nexport const generateRandomString = (length = 5): string => {\n const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\n return Array.from({ length }, () => characters.charAt(randomInt(characters.length))).join('');\n};\n\nconst excludedTypesPredicate = (hasExcludedTypes: boolean): string =>\n (hasExcludedTypes ? ' AND cd.field_type NOT IN (:customFieldsSearchExcludedTypes)' : '');\n\nconst buildValuesSubQuery = (hasExcludedTypes: boolean): string => `\n SELECT cv.model_id\n FROM \"custom_field_values\" AS cv\n INNER JOIN custom_field_definitions AS cd\n ON cv.custom_field_definition_id = cd.id\n AND cd.entity_id = :customFieldsSearchEntityId\n AND cd.model_type = :customFieldsSearchModelType\n AND cd.deleted_at IS NULL${excludedTypesPredicate(hasExcludedTypes)}\n WHERE cv.deleted_at IS NULL\n AND CAST(cv.value AS TEXT) ILIKE :searchTerm\n`;\n\nconst buildEntriesSubQuery = (hasExcludedTypes: boolean): string => `\n SELECT ce.model_id\n FROM custom_field_entries AS ce\n WHERE ce.model_type = :customFieldsSearchModelType\n AND ce.entity_id = :customFieldsSearchEntityId\n AND ${entriesValuesTextExpression('ce.custom_fields')} ILIKE :searchTerm\n AND EXISTS (\n SELECT 1\n FROM jsonb_each_text(ce.custom_fields) AS kv\n INNER JOIN custom_field_definitions AS cd\n ON cd.name = kv.key\n AND cd.entity_id = :customFieldsSearchEntityId\n AND cd.model_type = :customFieldsSearchModelType\n AND cd.deleted_at IS NULL${excludedTypesPredicate(hasExcludedTypes)}\n WHERE kv.value ILIKE :searchTerm\n )\n`;\n\nexport const generateCustomFieldSearchQueryPayload = (\n searchTerm: string,\n model: ModelStatic,\n entityId: string,\n customFieldsTypesToExclude: CustomFieldDefinitionType[] = [\n CustomFieldDefinitionType.DATETIME,\n CustomFieldDefinitionType.DATE,\n ],\n options?: Pick<CustomFieldOptions, 'customFieldsEntriesMode'>,\n): CustomFieldsSearchPayload => {\n const hasExcludedTypes = customFieldsTypesToExclude.length > 0;\n const subQuery = readsEntries(options?.customFieldsEntriesMode)\n ? buildEntriesSubQuery(hasExcludedTypes)\n : buildValuesSubQuery(hasExcludedTypes);\n\n return {\n where: {\n id: {\n [Op.in]: Sequelize.literal(`(${subQuery})`),\n },\n },\n replacements: {\n searchTerm: `%${searchTerm}%`,\n customFieldsSearchEntityId: entityId,\n customFieldsSearchModelType: model.name,\n ...(hasExcludedTypes\n ? { customFieldsSearchExcludedTypes: customFieldsTypesToExclude }\n : {}),\n },\n };\n};\n"],"mappings":"mRAsCA,MAAa,GAAwB,EAAS,IAErC,MAAM,KAAK,CAAE,SAAQ,KAAQ,uDAAW,OAAO,EAAU,GAAkB,CAAC,CAAC,CAAC,KAAK,GAAG,CAGzF,EAA0B,GAC7B,EAAmB,+DAAiE,GAEjF,EAAuB,GAAsC;;;;;;;8BAOrC,EAAuB,EAAiB,CAAC;;;EAKjE,EAAwB,GAAsC;;;;;UAK1D,EAA4B,mBAAmB,CAAC;;;;;;;;kCAQxB,EAAuB,EAAiB,CAAC;;;EAK9D,GACX,EACA,EACA,EACA,EAA0D,CACxD,EAA0B,SAC1B,EAA0B,KAC3B,CACD,IAC8B,CAC9B,IAAM,EAAmB,EAA2B,OAAS,EACvD,EAAW,EAAa,GAAS,wBAAwB,CAC3D,EAAqB,EAAiB,CACtC,EAAoB,EAAiB,CAEzC,MAAO,CACL,MAAO,CACL,GAAI,EACD,EAAG,IAAKA,EAAU,QAAQ,IAAI,EAAS,GAAG,CAC5C,CACF,CACD,aAAc,CACZ,WAAY,IAAI,EAAW,GAC3B,2BAA4B,EAC5B,4BAA6B,EAAM,KACnC,GAAI,EACA,CAAE,gCAAiC,EAA4B,CAC/D,EAAE,CACP,CACF"}
@@ -18,13 +18,6 @@ declare enum CustomFieldDefinitionType {
18
18
  MULTI_SELECT_STATUS = "multi-select-status",
19
19
  MULTI_SELECT_DROPDOWN = "multi-select-dropdown",
20
20
  }
21
- /**
22
- * SQL expression extracting every top-level VALUE of a custom-fields JSONB blob as one text
23
- * string (`'$.*'` selects values only — key names are never included). The trigram index
24
- * (migration 004) is built on this expression, and Postgres uses an expression index only
25
- * when the query repeats the indexed expression exactly — so the query side and the
26
- * migration side must both come from this single source.
27
- */
28
21
  declare const entriesValuesTextExpression: (columnRef: string) => string;
29
22
  declare const CUSTOM_FIELDS_FILTER_SCOPE: typeof customFields.CUSTOM_FIELDS_FILTER_SCOPE;
30
23
  //#endregion
@@ -1,4 +1,5 @@
1
1
  import { CustomFieldDefinitionType } from "../constants/index.cjs";
2
+ import { CustomFieldOptions } from "../../types/index.cjs";
2
3
  import { ModelStatic } from "sequelize-typescript";
3
4
  import { BindOrReplacements, WhereOptions } from "sequelize";
4
5
 
@@ -11,14 +12,20 @@ import { BindOrReplacements, WhereOptions } from "sequelize";
11
12
  * for a given term within custom fields associated with a specific model type and entity ID.
12
13
  * The WHERE clause and replacements are designed to be added to the main query.
13
14
  *
14
- * The subquery reads `custom_field_entries` when the service's entries mode (captured at
15
- * `useCustomFields()` init) reads entries, and `custom_field_values` otherwise.
15
+ * The subquery reads `custom_field_entries` when the caller's entries mode reads entries, and
16
+ * `custom_field_values` otherwise — the same contract the filter and sort scopes follow. A
17
+ * caller that passes no mode keeps the legacy values behavior.
18
+ *
19
+ * The entries subquery is served by the trigram index from migration 004, which only builds for
20
+ * callers that pass `useEntriesSearchIndex` to `runSadotMigrations`. Without it the query is
21
+ * still correct, just a sequential scan.
16
22
  *
17
23
  * @param {string} searchTerm - The term to search for within custom fields.
18
24
  * @param {ModelStatic} model - The Sequelize model representing the entity type to search for.
19
25
  * @param {string} entityId - The entity ID to filter the custom fields by.
20
26
  * @param {CustomFieldDefinitionType[]} excludedCustomFieldsTypes - An array of custom field types
21
27
  * to exclude from the search
28
+ * @param {CustomFieldOptions} options - Carries the caller's `customFieldsEntriesMode`.
22
29
  *
23
30
  * @returns {CustomFieldsSearchPayload} - An object containing the WHERE clause and replacements
24
31
  * for Sequelize.
@@ -28,7 +35,7 @@ interface CustomFieldsSearchPayload {
28
35
  replacements: BindOrReplacements;
29
36
  }
30
37
  declare const generateRandomString: (length?: number) => string;
31
- declare const generateCustomFieldSearchQueryPayload: (searchTerm: string, model: ModelStatic, entityId: string, customFieldsTypesToExclude?: CustomFieldDefinitionType[]) => CustomFieldsSearchPayload;
38
+ declare const generateCustomFieldSearchQueryPayload: (searchTerm: string, model: ModelStatic, entityId: string, customFieldsTypesToExclude?: CustomFieldDefinitionType[], options?: Pick<CustomFieldOptions, "customFieldsEntriesMode">) => CustomFieldsSearchPayload;
32
39
  //#endregion
33
40
  export { generateCustomFieldSearchQueryPayload, generateRandomString };
34
41
  //# sourceMappingURL=index.d.cts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autofleet/sadot",
3
- "version": "1.11.1-beta-484bfc0a.0",
3
+ "version": "1.12.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -35,7 +35,7 @@
35
35
  }
36
36
  },
37
37
  "dependencies": {
38
- "@autofleet/common-types": "6.34.0",
38
+ "@autofleet/common-types": "6.37.0",
39
39
  "ajv": "^8.12.0",
40
40
  "ajv-errors": "^3.0.0",
41
41
  "ajv-formats": "^3.0.1",