reqon-dsl 0.2.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.
Files changed (388) hide show
  1. package/.claude/settings.local.json +31 -0
  2. package/.claude/skills/api-integration.md +125 -0
  3. package/.claude/skills/database-schema.md +51 -0
  4. package/.claude/skills/dsl-design.md +80 -0
  5. package/.claude/skills/property-testing.md +143 -0
  6. package/.claude/skills/reqon/SKILL.md +44 -0
  7. package/.claude/skills/reqon/references/examples.md +206 -0
  8. package/.claude/skills/reqon/references/syntax.md +263 -0
  9. package/.claude/skills/vscode-extension.md +113 -0
  10. package/.github/dependabot.yml +32 -0
  11. package/.github/pull_request_template.md +21 -0
  12. package/.github/workflows/ci.yml +174 -0
  13. package/.github/workflows/release.yml +73 -0
  14. package/CLAUDE.md +72 -0
  15. package/CONTRIBUTING.md +161 -0
  16. package/README.md +235 -0
  17. package/TODO.md +51 -0
  18. package/dist/ast/index.d.ts +1 -0
  19. package/dist/ast/index.js +1 -0
  20. package/dist/ast/nodes.d.ts +237 -0
  21. package/dist/ast/nodes.js +12 -0
  22. package/dist/auth/auth.test.d.ts +1 -0
  23. package/dist/auth/auth.test.js +255 -0
  24. package/dist/auth/circuit-breaker.d.ts +115 -0
  25. package/dist/auth/circuit-breaker.js +267 -0
  26. package/dist/auth/credentials.d.ts +91 -0
  27. package/dist/auth/credentials.js +169 -0
  28. package/dist/auth/index.d.ts +5 -0
  29. package/dist/auth/index.js +8 -0
  30. package/dist/auth/oauth2-provider.d.ts +41 -0
  31. package/dist/auth/oauth2-provider.js +131 -0
  32. package/dist/auth/rate-limiter.d.ts +61 -0
  33. package/dist/auth/rate-limiter.js +380 -0
  34. package/dist/auth/token-store.d.ts +30 -0
  35. package/dist/auth/token-store.js +148 -0
  36. package/dist/auth/types.d.ts +142 -0
  37. package/dist/auth/types.js +1 -0
  38. package/dist/cli.d.ts +2 -0
  39. package/dist/cli.js +270 -0
  40. package/dist/errors/errors.test.d.ts +1 -0
  41. package/dist/errors/errors.test.js +165 -0
  42. package/dist/errors/index.d.ts +83 -0
  43. package/dist/errors/index.js +159 -0
  44. package/dist/execution/execution.test.d.ts +1 -0
  45. package/dist/execution/execution.test.js +246 -0
  46. package/dist/execution/index.d.ts +4 -0
  47. package/dist/execution/index.js +2 -0
  48. package/dist/execution/state.d.ts +136 -0
  49. package/dist/execution/state.js +82 -0
  50. package/dist/execution/store.d.ts +52 -0
  51. package/dist/execution/store.js +120 -0
  52. package/dist/index.d.ts +27 -0
  53. package/dist/index.js +57 -0
  54. package/dist/integration.test.d.ts +1 -0
  55. package/dist/integration.test.js +168 -0
  56. package/dist/interpreter/context.d.ts +15 -0
  57. package/dist/interpreter/context.js +29 -0
  58. package/dist/interpreter/evaluator.d.ts +5 -0
  59. package/dist/interpreter/evaluator.js +223 -0
  60. package/dist/interpreter/evaluator.test.d.ts +1 -0
  61. package/dist/interpreter/evaluator.test.js +512 -0
  62. package/dist/interpreter/executor.d.ts +131 -0
  63. package/dist/interpreter/executor.js +663 -0
  64. package/dist/interpreter/fetch-handler.d.ts +43 -0
  65. package/dist/interpreter/fetch-handler.js +203 -0
  66. package/dist/interpreter/http.d.ts +57 -0
  67. package/dist/interpreter/http.js +210 -0
  68. package/dist/interpreter/http.test.d.ts +1 -0
  69. package/dist/interpreter/http.test.js +299 -0
  70. package/dist/interpreter/index.d.ts +7 -0
  71. package/dist/interpreter/index.js +7 -0
  72. package/dist/interpreter/pagination.d.ts +63 -0
  73. package/dist/interpreter/pagination.js +155 -0
  74. package/dist/interpreter/progress.test.d.ts +1 -0
  75. package/dist/interpreter/progress.test.js +216 -0
  76. package/dist/interpreter/schema-matcher.d.ts +16 -0
  77. package/dist/interpreter/schema-matcher.js +136 -0
  78. package/dist/interpreter/schema-matcher.test.d.ts +1 -0
  79. package/dist/interpreter/schema-matcher.test.js +122 -0
  80. package/dist/interpreter/signals.d.ts +57 -0
  81. package/dist/interpreter/signals.js +73 -0
  82. package/dist/interpreter/step-handlers/for-handler.d.ts +17 -0
  83. package/dist/interpreter/step-handlers/for-handler.js +51 -0
  84. package/dist/interpreter/step-handlers/index.d.ts +8 -0
  85. package/dist/interpreter/step-handlers/index.js +8 -0
  86. package/dist/interpreter/step-handlers/map-handler.d.ts +10 -0
  87. package/dist/interpreter/step-handlers/map-handler.js +20 -0
  88. package/dist/interpreter/step-handlers/match-handler.d.ts +27 -0
  89. package/dist/interpreter/step-handlers/match-handler.js +61 -0
  90. package/dist/interpreter/step-handlers/store-handler.d.ts +13 -0
  91. package/dist/interpreter/step-handlers/store-handler.js +66 -0
  92. package/dist/interpreter/step-handlers/types.d.ts +15 -0
  93. package/dist/interpreter/step-handlers/types.js +1 -0
  94. package/dist/interpreter/step-handlers/validate-handler.d.ts +10 -0
  95. package/dist/interpreter/step-handlers/validate-handler.js +26 -0
  96. package/dist/interpreter/step-handlers/webhook-handler.d.ts +36 -0
  97. package/dist/interpreter/step-handlers/webhook-handler.js +104 -0
  98. package/dist/lexer/index.d.ts +10 -0
  99. package/dist/lexer/index.js +12 -0
  100. package/dist/lexer/lexer.d.ts +24 -0
  101. package/dist/lexer/lexer.js +264 -0
  102. package/dist/lexer/lexer.test.d.ts +1 -0
  103. package/dist/lexer/lexer.test.js +259 -0
  104. package/dist/lexer/tokens.d.ts +69 -0
  105. package/dist/lexer/tokens.js +146 -0
  106. package/dist/loader/index.d.ts +36 -0
  107. package/dist/loader/index.js +220 -0
  108. package/dist/loader/loader.test.d.ts +1 -0
  109. package/dist/loader/loader.test.js +287 -0
  110. package/dist/oas/index.d.ts +4 -0
  111. package/dist/oas/index.js +2 -0
  112. package/dist/oas/loader.d.ts +21 -0
  113. package/dist/oas/loader.js +82 -0
  114. package/dist/oas/oas.test.d.ts +1 -0
  115. package/dist/oas/oas.test.js +218 -0
  116. package/dist/oas/validator.d.ts +12 -0
  117. package/dist/oas/validator.js +227 -0
  118. package/dist/parser/base.d.ts +33 -0
  119. package/dist/parser/base.js +97 -0
  120. package/dist/parser/expressions.d.ts +27 -0
  121. package/dist/parser/expressions.js +248 -0
  122. package/dist/parser/expressions.test.d.ts +1 -0
  123. package/dist/parser/expressions.test.js +378 -0
  124. package/dist/parser/index.d.ts +3 -0
  125. package/dist/parser/index.js +3 -0
  126. package/dist/parser/match.test.d.ts +1 -0
  127. package/dist/parser/match.test.js +254 -0
  128. package/dist/parser/parser.d.ts +68 -0
  129. package/dist/parser/parser.js +1229 -0
  130. package/dist/parser/parser.test.d.ts +1 -0
  131. package/dist/parser/parser.test.js +333 -0
  132. package/dist/parser/schedule.test.d.ts +1 -0
  133. package/dist/parser/schedule.test.js +241 -0
  134. package/dist/plugin.d.ts +35 -0
  135. package/dist/plugin.js +68 -0
  136. package/dist/scheduler/cron-parser.d.ts +32 -0
  137. package/dist/scheduler/cron-parser.js +198 -0
  138. package/dist/scheduler/cron-parser.test.d.ts +1 -0
  139. package/dist/scheduler/cron-parser.test.js +188 -0
  140. package/dist/scheduler/index.d.ts +3 -0
  141. package/dist/scheduler/index.js +2 -0
  142. package/dist/scheduler/scheduler.d.ts +81 -0
  143. package/dist/scheduler/scheduler.js +376 -0
  144. package/dist/scheduler/types.d.ts +65 -0
  145. package/dist/scheduler/types.js +1 -0
  146. package/dist/stores/factory.d.ts +36 -0
  147. package/dist/stores/factory.js +73 -0
  148. package/dist/stores/file.d.ts +60 -0
  149. package/dist/stores/file.js +173 -0
  150. package/dist/stores/file.test.d.ts +1 -0
  151. package/dist/stores/file.test.js +165 -0
  152. package/dist/stores/index.d.ts +6 -0
  153. package/dist/stores/index.js +5 -0
  154. package/dist/stores/memory.d.ts +19 -0
  155. package/dist/stores/memory.js +51 -0
  156. package/dist/stores/memory.test.d.ts +1 -0
  157. package/dist/stores/memory.test.js +157 -0
  158. package/dist/stores/postgrest.d.ts +55 -0
  159. package/dist/stores/postgrest.js +217 -0
  160. package/dist/stores/stores.test.d.ts +1 -0
  161. package/dist/stores/stores.test.js +158 -0
  162. package/dist/stores/types.d.ts +31 -0
  163. package/dist/stores/types.js +26 -0
  164. package/dist/sync/index.d.ts +4 -0
  165. package/dist/sync/index.js +2 -0
  166. package/dist/sync/state.d.ts +69 -0
  167. package/dist/sync/state.js +66 -0
  168. package/dist/sync/store.d.ts +49 -0
  169. package/dist/sync/store.js +93 -0
  170. package/dist/sync/sync.test.d.ts +1 -0
  171. package/dist/sync/sync.test.js +221 -0
  172. package/dist/utils/async.d.ts +7 -0
  173. package/dist/utils/async.js +9 -0
  174. package/dist/utils/file.d.ts +38 -0
  175. package/dist/utils/file.js +92 -0
  176. package/dist/utils/index.d.ts +4 -0
  177. package/dist/utils/index.js +4 -0
  178. package/dist/utils/logger.d.ts +34 -0
  179. package/dist/utils/logger.js +39 -0
  180. package/dist/utils/path.d.ts +12 -0
  181. package/dist/utils/path.js +41 -0
  182. package/dist/webhook/index.d.ts +8 -0
  183. package/dist/webhook/index.js +7 -0
  184. package/dist/webhook/server.d.ts +84 -0
  185. package/dist/webhook/server.js +319 -0
  186. package/dist/webhook/store.d.ts +67 -0
  187. package/dist/webhook/store.js +193 -0
  188. package/dist/webhook/types.d.ts +88 -0
  189. package/dist/webhook/types.js +6 -0
  190. package/docusaurus/README.md +41 -0
  191. package/docusaurus/docs/advanced/execution-state.md +283 -0
  192. package/docusaurus/docs/advanced/extending-reqon.md +388 -0
  193. package/docusaurus/docs/advanced/multi-file-missions.md +250 -0
  194. package/docusaurus/docs/advanced/parallel-execution.md +353 -0
  195. package/docusaurus/docs/api-reference.md +443 -0
  196. package/docusaurus/docs/authentication/api-key.md +339 -0
  197. package/docusaurus/docs/authentication/basic.md +276 -0
  198. package/docusaurus/docs/authentication/bearer.md +282 -0
  199. package/docusaurus/docs/authentication/oauth2.md +317 -0
  200. package/docusaurus/docs/authentication/overview.md +251 -0
  201. package/docusaurus/docs/cli.md +229 -0
  202. package/docusaurus/docs/core-concepts/actions.md +286 -0
  203. package/docusaurus/docs/core-concepts/missions.md +264 -0
  204. package/docusaurus/docs/core-concepts/schemas.md +353 -0
  205. package/docusaurus/docs/core-concepts/sources.md +339 -0
  206. package/docusaurus/docs/core-concepts/stores.md +332 -0
  207. package/docusaurus/docs/dsl-syntax/expressions.md +361 -0
  208. package/docusaurus/docs/dsl-syntax/fetch.md +293 -0
  209. package/docusaurus/docs/dsl-syntax/for-loops.md +324 -0
  210. package/docusaurus/docs/dsl-syntax/map.md +345 -0
  211. package/docusaurus/docs/dsl-syntax/match.md +387 -0
  212. package/docusaurus/docs/dsl-syntax/pipelines.md +397 -0
  213. package/docusaurus/docs/dsl-syntax/validate.md +401 -0
  214. package/docusaurus/docs/error-handling/dead-letter-queues.md +399 -0
  215. package/docusaurus/docs/error-handling/flow-control.md +337 -0
  216. package/docusaurus/docs/error-handling/retry-strategies.md +368 -0
  217. package/docusaurus/docs/examples.md +488 -0
  218. package/docusaurus/docs/getting-started.md +256 -0
  219. package/docusaurus/docs/http/circuit-breaker.md +401 -0
  220. package/docusaurus/docs/http/incremental-sync.md +394 -0
  221. package/docusaurus/docs/http/pagination.md +361 -0
  222. package/docusaurus/docs/http/rate-limiting.md +383 -0
  223. package/docusaurus/docs/http/requests.md +328 -0
  224. package/docusaurus/docs/http/retry.md +402 -0
  225. package/docusaurus/docs/intro.md +90 -0
  226. package/docusaurus/docs/openapi/loading-specs.md +305 -0
  227. package/docusaurus/docs/openapi/operation-calls.md +314 -0
  228. package/docusaurus/docs/openapi/overview.md +212 -0
  229. package/docusaurus/docs/openapi/response-validation.md +344 -0
  230. package/docusaurus/docs/scheduling/cron.md +305 -0
  231. package/docusaurus/docs/scheduling/daemon-mode.md +317 -0
  232. package/docusaurus/docs/scheduling/intervals.md +289 -0
  233. package/docusaurus/docs/scheduling/overview.md +231 -0
  234. package/docusaurus/docs/stores/custom-adapters.md +376 -0
  235. package/docusaurus/docs/stores/file.md +236 -0
  236. package/docusaurus/docs/stores/memory.md +193 -0
  237. package/docusaurus/docs/stores/overview.md +274 -0
  238. package/docusaurus/docs/stores/postgrest.md +316 -0
  239. package/docusaurus/docusaurus.config.ts +148 -0
  240. package/docusaurus/package-lock.json +18029 -0
  241. package/docusaurus/package.json +47 -0
  242. package/docusaurus/sidebars.ts +155 -0
  243. package/docusaurus/src/components/HomepageFeatures/index.tsx +105 -0
  244. package/docusaurus/src/components/HomepageFeatures/styles.module.css +12 -0
  245. package/docusaurus/src/css/custom.css +169 -0
  246. package/docusaurus/src/pages/index.module.css +48 -0
  247. package/docusaurus/src/pages/index.tsx +110 -0
  248. package/docusaurus/src/pages/markdown-page.md +7 -0
  249. package/docusaurus/static/.nojekyll +0 -0
  250. package/docusaurus/static/img/docusaurus-social-card.jpg +0 -0
  251. package/docusaurus/static/img/docusaurus.png +0 -0
  252. package/docusaurus/static/img/favicon.ico +0 -0
  253. package/docusaurus/static/img/logo.svg +10 -0
  254. package/docusaurus/static/img/undraw_docusaurus_mountain.svg +171 -0
  255. package/docusaurus/static/img/undraw_docusaurus_react.svg +170 -0
  256. package/docusaurus/static/img/undraw_docusaurus_tree.svg +40 -0
  257. package/docusaurus/tsconfig.json +8 -0
  258. package/examples/README.md +112 -0
  259. package/examples/error-handling/README.md +150 -0
  260. package/examples/error-handling/payment-processor.vague +287 -0
  261. package/examples/github-sync/README.md +74 -0
  262. package/examples/github-sync/fetch-issues.vague +47 -0
  263. package/examples/github-sync/fetch-prs.vague +40 -0
  264. package/examples/github-sync/mission.vague +101 -0
  265. package/examples/github-sync/normalize.vague +70 -0
  266. package/examples/jsonplaceholder/README.md +28 -0
  267. package/examples/jsonplaceholder/posts.vague +48 -0
  268. package/examples/petstore/README.md +35 -0
  269. package/examples/petstore/openapi.yaml +97 -0
  270. package/examples/petstore/sync.vague +52 -0
  271. package/examples/temporal-comparison/README.md +297 -0
  272. package/examples/temporal-comparison/reconciliation.vague +355 -0
  273. package/examples/temporal-comparison/temporal/activities/index.ts +8 -0
  274. package/examples/temporal-comparison/temporal/activities/shipstation.ts +225 -0
  275. package/examples/temporal-comparison/temporal/activities/shopify.ts +257 -0
  276. package/examples/temporal-comparison/temporal/activities/storage.ts +198 -0
  277. package/examples/temporal-comparison/temporal/activities/stripe.ts +169 -0
  278. package/examples/temporal-comparison/temporal/activities/validation.ts +205 -0
  279. package/examples/temporal-comparison/temporal/client/schedule.ts +218 -0
  280. package/examples/temporal-comparison/temporal/config/retry.ts +63 -0
  281. package/examples/temporal-comparison/temporal/types/index.ts +129 -0
  282. package/examples/temporal-comparison/temporal/workers/main.ts +130 -0
  283. package/examples/temporal-comparison/temporal/workflows/orderReconciliation.ts +262 -0
  284. package/examples/xero/README.md +88 -0
  285. package/examples/xero/invoices.vague +189 -0
  286. package/package.json +40 -0
  287. package/src/api-integration.test.ts +954 -0
  288. package/src/ast/index.ts +1 -0
  289. package/src/ast/nodes.ts +310 -0
  290. package/src/auth/auth.test.ts +326 -0
  291. package/src/auth/circuit-breaker.test.ts +390 -0
  292. package/src/auth/circuit-breaker.ts +379 -0
  293. package/src/auth/credentials.test.ts +273 -0
  294. package/src/auth/credentials.ts +246 -0
  295. package/src/auth/index.ts +40 -0
  296. package/src/auth/oauth2-provider.ts +177 -0
  297. package/src/auth/rate-limiter.ts +459 -0
  298. package/src/auth/token-store.ts +177 -0
  299. package/src/auth/types.ts +159 -0
  300. package/src/benchmark/e2e.bench.ts +288 -0
  301. package/src/benchmark/evaluator.bench.ts +331 -0
  302. package/src/benchmark/fixtures.ts +295 -0
  303. package/src/benchmark/index.ts +108 -0
  304. package/src/benchmark/lexer.bench.ts +69 -0
  305. package/src/benchmark/parser.bench.ts +103 -0
  306. package/src/benchmark/resilience.bench.ts +193 -0
  307. package/src/benchmark/store.bench.ts +147 -0
  308. package/src/benchmark/utils.ts +230 -0
  309. package/src/cli.ts +313 -0
  310. package/src/errors/errors.test.ts +234 -0
  311. package/src/errors/index.ts +223 -0
  312. package/src/execution/execution.test.ts +307 -0
  313. package/src/execution/index.ts +21 -0
  314. package/src/execution/state.ts +207 -0
  315. package/src/execution/store.ts +188 -0
  316. package/src/index.ts +169 -0
  317. package/src/integration.test.ts +192 -0
  318. package/src/interpreter/context.ts +57 -0
  319. package/src/interpreter/evaluator.test.ts +796 -0
  320. package/src/interpreter/evaluator.ts +245 -0
  321. package/src/interpreter/executor.ts +946 -0
  322. package/src/interpreter/fetch-handler.ts +302 -0
  323. package/src/interpreter/http.test.ts +423 -0
  324. package/src/interpreter/http.ts +308 -0
  325. package/src/interpreter/index.ts +32 -0
  326. package/src/interpreter/pagination.ts +207 -0
  327. package/src/interpreter/progress.test.ts +276 -0
  328. package/src/interpreter/schema-matcher.test.ts +160 -0
  329. package/src/interpreter/schema-matcher.ts +168 -0
  330. package/src/interpreter/signals.ts +73 -0
  331. package/src/interpreter/step-handlers/for-handler.ts +65 -0
  332. package/src/interpreter/step-handlers/index.ts +17 -0
  333. package/src/interpreter/step-handlers/map-handler.ts +24 -0
  334. package/src/interpreter/step-handlers/match-handler.ts +101 -0
  335. package/src/interpreter/step-handlers/store-handler.ts +78 -0
  336. package/src/interpreter/step-handlers/types.ts +17 -0
  337. package/src/interpreter/step-handlers/validate-handler.ts +30 -0
  338. package/src/interpreter/step-handlers/webhook-handler.ts +142 -0
  339. package/src/lexer/index.ts +18 -0
  340. package/src/lexer/lexer.test.ts +316 -0
  341. package/src/lexer/tokens.ts +179 -0
  342. package/src/loader/index.ts +288 -0
  343. package/src/loader/loader.test.ts +360 -0
  344. package/src/oas/index.ts +4 -0
  345. package/src/oas/loader.ts +126 -0
  346. package/src/oas/oas.test.ts +254 -0
  347. package/src/oas/validator.ts +299 -0
  348. package/src/parser/base.ts +124 -0
  349. package/src/parser/expressions.test.ts +525 -0
  350. package/src/parser/expressions.ts +314 -0
  351. package/src/parser/index.ts +3 -0
  352. package/src/parser/match.test.ts +296 -0
  353. package/src/parser/parser.test.ts +739 -0
  354. package/src/parser/parser.ts +1469 -0
  355. package/src/parser/schedule.test.ts +287 -0
  356. package/src/parser/webhook.test.ts +248 -0
  357. package/src/plugin.ts +83 -0
  358. package/src/scheduler/cron-parser.test.ts +236 -0
  359. package/src/scheduler/cron-parser.ts +236 -0
  360. package/src/scheduler/index.ts +10 -0
  361. package/src/scheduler/scheduler.ts +443 -0
  362. package/src/scheduler/types.ts +71 -0
  363. package/src/stores/factory.ts +104 -0
  364. package/src/stores/file.test.ts +276 -0
  365. package/src/stores/file.ts +211 -0
  366. package/src/stores/index.ts +6 -0
  367. package/src/stores/memory.test.ts +238 -0
  368. package/src/stores/memory.ts +63 -0
  369. package/src/stores/postgrest.test.ts +488 -0
  370. package/src/stores/postgrest.ts +263 -0
  371. package/src/stores/stores.test.ts +197 -0
  372. package/src/stores/types.ts +58 -0
  373. package/src/sync/index.ts +16 -0
  374. package/src/sync/state.ts +126 -0
  375. package/src/sync/store.ts +139 -0
  376. package/src/sync/sync.test.ts +271 -0
  377. package/src/utils/async.ts +10 -0
  378. package/src/utils/file.ts +106 -0
  379. package/src/utils/index.ts +14 -0
  380. package/src/utils/logger.ts +53 -0
  381. package/src/utils/path.ts +47 -0
  382. package/src/webhook/index.ts +15 -0
  383. package/src/webhook/server.test.ts +253 -0
  384. package/src/webhook/server.ts +389 -0
  385. package/src/webhook/store.ts +239 -0
  386. package/src/webhook/types.ts +93 -0
  387. package/tsconfig.json +17 -0
  388. package/vitest.config.ts +39 -0
@@ -0,0 +1,217 @@
1
+ /**
2
+ * PostgREST-compatible store adapter.
3
+ * Works with Supabase, standalone PostgREST, or any PostgREST-compatible API.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * const store = new PostgRESTStore({
8
+ * url: 'https://xxx.supabase.co/rest/v1',
9
+ * apiKey: process.env.SUPABASE_ANON_KEY,
10
+ * table: 'users',
11
+ * });
12
+ * ```
13
+ */
14
+ export class PostgRESTStore {
15
+ options;
16
+ baseUrl;
17
+ headers;
18
+ primaryKey;
19
+ constructor(options) {
20
+ this.options = options;
21
+ // Normalize URL (remove trailing slash)
22
+ const url = options.url.replace(/\/$/, '');
23
+ this.baseUrl = `${url}/${options.table}`;
24
+ this.primaryKey = options.primaryKey ?? 'id';
25
+ this.headers = {
26
+ 'Content-Type': 'application/json',
27
+ 'apikey': options.apiKey,
28
+ 'Authorization': `Bearer ${options.apiKey}`,
29
+ 'Prefer': 'return=representation',
30
+ };
31
+ if (options.schema) {
32
+ this.headers['Accept-Profile'] = options.schema;
33
+ this.headers['Content-Profile'] = options.schema;
34
+ }
35
+ }
36
+ async get(key) {
37
+ const url = `${this.baseUrl}?${this.primaryKey}=eq.${encodeURIComponent(key)}&limit=1`;
38
+ const response = await fetch(url, {
39
+ method: 'GET',
40
+ headers: this.headers,
41
+ });
42
+ if (!response.ok) {
43
+ throw new PostgRESTError(`Failed to get record: ${response.statusText}`, response.status);
44
+ }
45
+ const data = await response.json();
46
+ return Array.isArray(data) && data.length > 0 ? data[0] : null;
47
+ }
48
+ async set(key, value) {
49
+ // Upsert using PostgREST's on_conflict resolution
50
+ const record = { ...value, [this.primaryKey]: key };
51
+ const response = await fetch(this.baseUrl, {
52
+ method: 'POST',
53
+ headers: {
54
+ ...this.headers,
55
+ 'Prefer': 'resolution=merge-duplicates,return=representation',
56
+ },
57
+ body: JSON.stringify(record),
58
+ });
59
+ if (!response.ok) {
60
+ const error = await this.parseError(response);
61
+ throw new PostgRESTError(`Failed to set record: ${error}`, response.status);
62
+ }
63
+ }
64
+ async update(key, value) {
65
+ const url = `${this.baseUrl}?${this.primaryKey}=eq.${encodeURIComponent(key)}`;
66
+ const response = await fetch(url, {
67
+ method: 'PATCH',
68
+ headers: this.headers,
69
+ body: JSON.stringify(value),
70
+ });
71
+ if (!response.ok) {
72
+ const error = await this.parseError(response);
73
+ throw new PostgRESTError(`Failed to update record: ${error}`, response.status);
74
+ }
75
+ }
76
+ async delete(key) {
77
+ const url = `${this.baseUrl}?${this.primaryKey}=eq.${encodeURIComponent(key)}`;
78
+ const response = await fetch(url, {
79
+ method: 'DELETE',
80
+ headers: this.headers,
81
+ });
82
+ if (!response.ok) {
83
+ const error = await this.parseError(response);
84
+ throw new PostgRESTError(`Failed to delete record: ${error}`, response.status);
85
+ }
86
+ }
87
+ async list(filter) {
88
+ const params = new URLSearchParams();
89
+ // Apply where clause
90
+ if (filter?.where) {
91
+ for (const [field, value] of Object.entries(filter.where)) {
92
+ if (value === null) {
93
+ params.append(field, 'is.null');
94
+ }
95
+ else if (typeof value === 'string') {
96
+ params.append(field, `eq.${value}`);
97
+ }
98
+ else if (typeof value === 'number' || typeof value === 'boolean') {
99
+ params.append(field, `eq.${value}`);
100
+ }
101
+ else {
102
+ // For complex values, try JSON
103
+ params.append(field, `eq.${JSON.stringify(value)}`);
104
+ }
105
+ }
106
+ }
107
+ // Apply pagination
108
+ if (filter?.limit) {
109
+ params.append('limit', String(filter.limit));
110
+ }
111
+ if (filter?.offset) {
112
+ params.append('offset', String(filter.offset));
113
+ }
114
+ const queryString = params.toString();
115
+ const url = queryString ? `${this.baseUrl}?${queryString}` : this.baseUrl;
116
+ const response = await fetch(url, {
117
+ method: 'GET',
118
+ headers: this.headers,
119
+ });
120
+ if (!response.ok) {
121
+ const error = await this.parseError(response);
122
+ throw new PostgRESTError(`Failed to list records: ${error}`, response.status);
123
+ }
124
+ return response.json();
125
+ }
126
+ async clear() {
127
+ // Delete all records - PostgREST requires a filter, so we use a always-true condition
128
+ // This deletes where primary key is not null (i.e., all records)
129
+ const url = `${this.baseUrl}?${this.primaryKey}=not.is.null`;
130
+ const response = await fetch(url, {
131
+ method: 'DELETE',
132
+ headers: this.headers,
133
+ });
134
+ if (!response.ok) {
135
+ const error = await this.parseError(response);
136
+ throw new PostgRESTError(`Failed to clear records: ${error}`, response.status);
137
+ }
138
+ }
139
+ /**
140
+ * Bulk insert records (more efficient than individual sets)
141
+ */
142
+ async bulkInsert(records) {
143
+ if (records.length === 0)
144
+ return;
145
+ const response = await fetch(this.baseUrl, {
146
+ method: 'POST',
147
+ headers: {
148
+ ...this.headers,
149
+ 'Prefer': 'resolution=merge-duplicates',
150
+ },
151
+ body: JSON.stringify(records),
152
+ });
153
+ if (!response.ok) {
154
+ const error = await this.parseError(response);
155
+ throw new PostgRESTError(`Failed to bulk insert: ${error}`, response.status);
156
+ }
157
+ }
158
+ /**
159
+ * Count records matching a filter
160
+ */
161
+ async count(filter) {
162
+ const params = new URLSearchParams();
163
+ params.append('select', 'count');
164
+ if (filter?.where) {
165
+ for (const [field, value] of Object.entries(filter.where)) {
166
+ if (value === null) {
167
+ params.append(field, 'is.null');
168
+ }
169
+ else {
170
+ params.append(field, `eq.${value}`);
171
+ }
172
+ }
173
+ }
174
+ const url = `${this.baseUrl}?${params.toString()}`;
175
+ const response = await fetch(url, {
176
+ method: 'GET',
177
+ headers: {
178
+ ...this.headers,
179
+ 'Prefer': 'count=exact',
180
+ },
181
+ });
182
+ if (!response.ok) {
183
+ throw new PostgRESTError(`Failed to count records: ${response.statusText}`, response.status);
184
+ }
185
+ // Count is in the Content-Range header: "0-24/100"
186
+ const range = response.headers.get('Content-Range');
187
+ if (range) {
188
+ const match = range.match(/\/(\d+|\*)/);
189
+ if (match && match[1] !== '*') {
190
+ return parseInt(match[1], 10);
191
+ }
192
+ }
193
+ // Fallback: count the results
194
+ const data = await response.json();
195
+ return Array.isArray(data) ? data.length : 0;
196
+ }
197
+ async parseError(response) {
198
+ try {
199
+ const body = await response.json();
200
+ return body.message || body.error || response.statusText;
201
+ }
202
+ catch {
203
+ return response.statusText;
204
+ }
205
+ }
206
+ }
207
+ /**
208
+ * Error class for PostgREST operations
209
+ */
210
+ export class PostgRESTError extends Error {
211
+ statusCode;
212
+ constructor(message, statusCode) {
213
+ super(message);
214
+ this.statusCode = statusCode;
215
+ this.name = 'PostgRESTError';
216
+ }
217
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,158 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { rmSync, existsSync } from 'node:fs';
3
+ import { FileStore } from './file.js';
4
+ import { createStore, resolveStoreType } from './factory.js';
5
+ const TEST_DIR = '.reqon-test-data';
6
+ describe('FileStore', () => {
7
+ let store;
8
+ beforeEach(() => {
9
+ // Clean up test directory
10
+ if (existsSync(TEST_DIR)) {
11
+ rmSync(TEST_DIR, { recursive: true, force: true });
12
+ }
13
+ store = new FileStore('test-store', { baseDir: TEST_DIR });
14
+ });
15
+ afterEach(() => {
16
+ // Clean up after tests
17
+ if (existsSync(TEST_DIR)) {
18
+ rmSync(TEST_DIR, { recursive: true, force: true });
19
+ }
20
+ });
21
+ it('creates data directory and .gitignore', () => {
22
+ expect(existsSync(TEST_DIR)).toBe(true);
23
+ expect(existsSync(`${TEST_DIR}/.gitignore`)).toBe(true);
24
+ });
25
+ it('stores and retrieves data', async () => {
26
+ await store.set('item-1', { id: '1', name: 'Test Item' });
27
+ const retrieved = await store.get('item-1');
28
+ expect(retrieved).toEqual({ id: '1', name: 'Test Item' });
29
+ });
30
+ it('persists data to disk', async () => {
31
+ await store.set('item-1', { id: '1', name: 'Persisted' });
32
+ // Create new store instance pointing to same file
33
+ const newStore = new FileStore('test-store', { baseDir: TEST_DIR });
34
+ const retrieved = await newStore.get('item-1');
35
+ expect(retrieved).toEqual({ id: '1', name: 'Persisted' });
36
+ });
37
+ it('returns null for missing keys', async () => {
38
+ const result = await store.get('nonexistent');
39
+ expect(result).toBeNull();
40
+ });
41
+ it('updates existing records', async () => {
42
+ await store.set('item-1', { id: '1', name: 'Original', count: 0 });
43
+ await store.update('item-1', { count: 5 });
44
+ const retrieved = await store.get('item-1');
45
+ expect(retrieved).toEqual({ id: '1', name: 'Original', count: 5 });
46
+ });
47
+ it('deletes records', async () => {
48
+ await store.set('item-1', { id: '1' });
49
+ await store.delete('item-1');
50
+ const result = await store.get('item-1');
51
+ expect(result).toBeNull();
52
+ });
53
+ it('lists all records', async () => {
54
+ await store.set('a', { id: 'a', type: 'x' });
55
+ await store.set('b', { id: 'b', type: 'y' });
56
+ await store.set('c', { id: 'c', type: 'x' });
57
+ const all = await store.list();
58
+ expect(all).toHaveLength(3);
59
+ });
60
+ it('filters records with where clause', async () => {
61
+ await store.set('a', { id: 'a', type: 'x' });
62
+ await store.set('b', { id: 'b', type: 'y' });
63
+ await store.set('c', { id: 'c', type: 'x' });
64
+ const filtered = await store.list({ where: { type: 'x' } });
65
+ expect(filtered).toHaveLength(2);
66
+ expect(filtered.every((r) => r.type === 'x')).toBe(true);
67
+ });
68
+ it('clears all records', async () => {
69
+ await store.set('a', { id: 'a' });
70
+ await store.set('b', { id: 'b' });
71
+ await store.clear();
72
+ const all = await store.list();
73
+ expect(all).toHaveLength(0);
74
+ });
75
+ it('supports batch mode with flush', async () => {
76
+ const batchStore = new FileStore('batch-test', {
77
+ baseDir: TEST_DIR,
78
+ persist: 'batch',
79
+ });
80
+ await batchStore.set('item-1', { id: '1' });
81
+ await batchStore.set('item-2', { id: '2' });
82
+ // Before flush, new instance won't see changes
83
+ const beforeFlush = new FileStore('batch-test', { baseDir: TEST_DIR });
84
+ expect(await beforeFlush.get('item-1')).toBeNull();
85
+ // After flush, changes are persisted
86
+ batchStore.flush();
87
+ const afterFlush = new FileStore('batch-test', { baseDir: TEST_DIR });
88
+ expect(await afterFlush.get('item-1')).toEqual({ id: '1' });
89
+ });
90
+ });
91
+ describe('createStore factory', () => {
92
+ beforeEach(() => {
93
+ if (existsSync(TEST_DIR)) {
94
+ rmSync(TEST_DIR, { recursive: true, force: true });
95
+ }
96
+ });
97
+ afterEach(() => {
98
+ if (existsSync(TEST_DIR)) {
99
+ rmSync(TEST_DIR, { recursive: true, force: true });
100
+ }
101
+ });
102
+ it('creates memory store', async () => {
103
+ const store = createStore({ type: 'memory', name: 'test' });
104
+ await store.set('key', { value: 1 });
105
+ expect(await store.get('key')).toEqual({ value: 1 });
106
+ });
107
+ it('creates file store', async () => {
108
+ const store = createStore({
109
+ type: 'file',
110
+ name: 'test',
111
+ baseDir: TEST_DIR,
112
+ });
113
+ await store.set('key', { value: 1 });
114
+ // Verify persistence
115
+ const newStore = createStore({
116
+ type: 'file',
117
+ name: 'test',
118
+ baseDir: TEST_DIR,
119
+ });
120
+ expect(await newStore.get('key')).toEqual({ value: 1 });
121
+ });
122
+ it('falls back to file for sql in dev mode', async () => {
123
+ const store = createStore({
124
+ type: 'sql',
125
+ name: 'test',
126
+ baseDir: TEST_DIR,
127
+ });
128
+ await store.set('key', { value: 1 });
129
+ expect(await store.get('key')).toEqual({ value: 1 });
130
+ });
131
+ it('falls back to file for nosql in dev mode', async () => {
132
+ const store = createStore({
133
+ type: 'nosql',
134
+ name: 'test',
135
+ baseDir: TEST_DIR,
136
+ });
137
+ await store.set('key', { value: 1 });
138
+ expect(await store.get('key')).toEqual({ value: 1 });
139
+ });
140
+ });
141
+ describe('resolveStoreType', () => {
142
+ it('keeps memory as memory', () => {
143
+ expect(resolveStoreType('memory', true)).toBe('memory');
144
+ expect(resolveStoreType('memory', false)).toBe('memory');
145
+ });
146
+ it('maps sql to file in dev mode', () => {
147
+ expect(resolveStoreType('sql', true)).toBe('file');
148
+ });
149
+ it('maps nosql to file in dev mode', () => {
150
+ expect(resolveStoreType('nosql', true)).toBe('file');
151
+ });
152
+ it('keeps sql as sql in production mode', () => {
153
+ expect(resolveStoreType('sql', false)).toBe('sql');
154
+ });
155
+ it('keeps nosql as nosql in production mode', () => {
156
+ expect(resolveStoreType('nosql', false)).toBe('nosql');
157
+ });
158
+ });
@@ -0,0 +1,31 @@
1
+ export interface StoreAdapter {
2
+ get(key: string): Promise<Record<string, unknown> | null>;
3
+ set(key: string, value: Record<string, unknown>): Promise<void>;
4
+ /** Bulk set multiple records at once - more efficient than individual sets */
5
+ bulkSet?(records: Array<{
6
+ key: string;
7
+ value: Record<string, unknown>;
8
+ }>): Promise<void>;
9
+ update(key: string, value: Partial<Record<string, unknown>>): Promise<void>;
10
+ delete(key: string): Promise<void>;
11
+ list(filter?: StoreFilter): Promise<Record<string, unknown>[]>;
12
+ count(filter?: StoreFilter): Promise<number>;
13
+ clear(): Promise<void>;
14
+ }
15
+ export interface StoreFilter {
16
+ where?: Record<string, unknown>;
17
+ limit?: number;
18
+ offset?: number;
19
+ }
20
+ export interface StoreConfig {
21
+ type: 'nosql' | 'sql' | 'memory' | 'file';
22
+ target: string;
23
+ connection?: string;
24
+ /** For file stores: 'json' or 'csv' */
25
+ format?: 'json' | 'csv';
26
+ }
27
+ /**
28
+ * Apply filter criteria to a list of records.
29
+ * Handles where clause, offset, and limit.
30
+ */
31
+ export declare function applyStoreFilter<T extends Record<string, unknown>>(records: T[], filter?: StoreFilter): T[];
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Apply filter criteria to a list of records.
3
+ * Handles where clause, offset, and limit.
4
+ */
5
+ export function applyStoreFilter(records, filter) {
6
+ if (!filter)
7
+ return records;
8
+ let results = records;
9
+ if (filter.where) {
10
+ const whereClause = filter.where;
11
+ results = results.filter((item) => {
12
+ for (const [key, value] of Object.entries(whereClause)) {
13
+ if (item[key] !== value)
14
+ return false;
15
+ }
16
+ return true;
17
+ });
18
+ }
19
+ if (filter.offset) {
20
+ results = results.slice(filter.offset);
21
+ }
22
+ if (filter.limit) {
23
+ results = results.slice(0, filter.limit);
24
+ }
25
+ return results;
26
+ }
@@ -0,0 +1,4 @@
1
+ export type { SyncCheckpoint, SyncState, SinceResolution, SinceDateFormat, } from './state.js';
2
+ export { generateCheckpointKey, formatSinceDate, parseSinceDate, EPOCH, } from './state.js';
3
+ export type { SyncStore } from './store.js';
4
+ export { FileSyncStore, MemorySyncStore } from './store.js';
@@ -0,0 +1,2 @@
1
+ export { generateCheckpointKey, formatSinceDate, parseSinceDate, EPOCH, } from './state.js';
2
+ export { FileSyncStore, MemorySyncStore } from './store.js';
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Sync State - Tracks last sync timestamps for incremental sync
3
+ *
4
+ * Enables "only fetch changed since last run" patterns:
5
+ * - get "/invoices" { since: lastSync }
6
+ * - call Xero.getInvoices { since: lastSync("invoices") }
7
+ */
8
+ /**
9
+ * Sync checkpoint - records when a sync completed
10
+ */
11
+ export interface SyncCheckpoint {
12
+ /** Unique key (e.g., "Xero:getInvoices" or "invoices") */
13
+ key: string;
14
+ /** Last successful sync timestamp */
15
+ syncedAt: Date;
16
+ /** Number of records fetched in last sync */
17
+ recordCount?: number;
18
+ /** Optional cursor for cursor-based pagination resume */
19
+ cursor?: string;
20
+ /** Mission that performed the sync */
21
+ mission?: string;
22
+ /** Execution ID that performed the sync */
23
+ executionId?: string;
24
+ }
25
+ /**
26
+ * Sync state for a mission/source
27
+ */
28
+ export interface SyncState {
29
+ /** Mission name */
30
+ mission: string;
31
+ /** Source name */
32
+ source: string;
33
+ /** Checkpoints by key */
34
+ checkpoints: Map<string, SyncCheckpoint>;
35
+ /** When this state was last updated */
36
+ updatedAt: Date;
37
+ }
38
+ /**
39
+ * Options for resolving a "since" value
40
+ */
41
+ export interface SinceResolution {
42
+ /** The resolved timestamp */
43
+ timestamp: Date;
44
+ /** Whether this is a fresh sync (no previous checkpoint) */
45
+ isFreshSync: boolean;
46
+ /** The checkpoint key used */
47
+ key: string;
48
+ }
49
+ /**
50
+ * Generate a checkpoint key for a fetch operation
51
+ */
52
+ export declare function generateCheckpointKey(source: string, operationId?: string, endpoint?: string): string;
53
+ /**
54
+ * Default "since" value for fresh syncs
55
+ * Uses Unix epoch (1970-01-01) to fetch all historical data
56
+ */
57
+ export declare const EPOCH: Date;
58
+ /**
59
+ * Common date formats for API "since" parameters
60
+ */
61
+ export type SinceDateFormat = 'iso' | 'unix' | 'unix-ms' | 'date-only';
62
+ /**
63
+ * Format a date for use in API requests
64
+ */
65
+ export declare function formatSinceDate(date: Date, format?: SinceDateFormat): string;
66
+ /**
67
+ * Parse a date from various API response formats
68
+ */
69
+ export declare function parseSinceDate(value: unknown): Date | null;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Sync State - Tracks last sync timestamps for incremental sync
3
+ *
4
+ * Enables "only fetch changed since last run" patterns:
5
+ * - get "/invoices" { since: lastSync }
6
+ * - call Xero.getInvoices { since: lastSync("invoices") }
7
+ */
8
+ /**
9
+ * Generate a checkpoint key for a fetch operation
10
+ */
11
+ export function generateCheckpointKey(source, operationId, endpoint) {
12
+ if (operationId) {
13
+ return `${source}:${operationId}`;
14
+ }
15
+ if (endpoint) {
16
+ // Normalize endpoint (remove query params, trailing slashes)
17
+ const normalized = endpoint.split('?')[0].replace(/\/+$/, '');
18
+ return `${source}:${normalized}`;
19
+ }
20
+ return source;
21
+ }
22
+ /**
23
+ * Default "since" value for fresh syncs
24
+ * Uses Unix epoch (1970-01-01) to fetch all historical data
25
+ */
26
+ export const EPOCH = new Date(0);
27
+ /**
28
+ * Format a date for use in API requests
29
+ */
30
+ export function formatSinceDate(date, format = 'iso') {
31
+ switch (format) {
32
+ case 'iso':
33
+ return date.toISOString();
34
+ case 'unix':
35
+ return Math.floor(date.getTime() / 1000).toString();
36
+ case 'unix-ms':
37
+ return date.getTime().toString();
38
+ case 'date-only':
39
+ return date.toISOString().split('T')[0];
40
+ default:
41
+ return date.toISOString();
42
+ }
43
+ }
44
+ /**
45
+ * Parse a date from various API response formats
46
+ */
47
+ export function parseSinceDate(value) {
48
+ if (value instanceof Date) {
49
+ return value;
50
+ }
51
+ if (typeof value === 'string') {
52
+ const parsed = new Date(value);
53
+ if (!isNaN(parsed.getTime())) {
54
+ return parsed;
55
+ }
56
+ }
57
+ if (typeof value === 'number') {
58
+ // Assume Unix timestamp in seconds if < year 3000 in seconds
59
+ if (value < 32503680000) {
60
+ return new Date(value * 1000);
61
+ }
62
+ // Otherwise assume milliseconds
63
+ return new Date(value);
64
+ }
65
+ return null;
66
+ }
@@ -0,0 +1,49 @@
1
+ import type { SyncCheckpoint } from './state.js';
2
+ /**
3
+ * Sync store interface - persists sync checkpoints
4
+ */
5
+ export interface SyncStore {
6
+ /** Get last sync timestamp for a key */
7
+ getLastSync(key: string): Promise<Date>;
8
+ /** Get checkpoint details for a key */
9
+ getCheckpoint(key: string): Promise<SyncCheckpoint | null>;
10
+ /** Record a successful sync */
11
+ recordSync(checkpoint: SyncCheckpoint): Promise<void>;
12
+ /** List all checkpoints */
13
+ list(): Promise<SyncCheckpoint[]>;
14
+ /** Clear a specific checkpoint */
15
+ clear(key: string): Promise<void>;
16
+ /** Clear all checkpoints */
17
+ clearAll(): Promise<void>;
18
+ }
19
+ /**
20
+ * File-based sync store
21
+ * Stores sync state in .reqon-data/sync/{mission}.json
22
+ */
23
+ export declare class FileSyncStore implements SyncStore {
24
+ private filePath;
25
+ private checkpoints;
26
+ private initialized;
27
+ constructor(mission: string, baseDir?: string);
28
+ private init;
29
+ private load;
30
+ private persist;
31
+ getLastSync(key: string): Promise<Date>;
32
+ getCheckpoint(key: string): Promise<SyncCheckpoint | null>;
33
+ recordSync(checkpoint: SyncCheckpoint): Promise<void>;
34
+ list(): Promise<SyncCheckpoint[]>;
35
+ clear(key: string): Promise<void>;
36
+ clearAll(): Promise<void>;
37
+ }
38
+ /**
39
+ * In-memory sync store (for testing)
40
+ */
41
+ export declare class MemorySyncStore implements SyncStore {
42
+ private checkpoints;
43
+ getLastSync(key: string): Promise<Date>;
44
+ getCheckpoint(key: string): Promise<SyncCheckpoint | null>;
45
+ recordSync(checkpoint: SyncCheckpoint): Promise<void>;
46
+ list(): Promise<SyncCheckpoint[]>;
47
+ clear(key: string): Promise<void>;
48
+ clearAll(): Promise<void>;
49
+ }