clivly 0.9.7 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +9 -9
  2. package/{adapter-C2eRbBfo.d.cts → adapter-1TN0zohk.d.cts} +1 -1
  3. package/{adapter-D5LRa-NA.d.mts → adapter-uxirB88i.d.mts} +1 -1
  4. package/{auth-adapter-Bjgo8LhN.d.mts → auth-adapter-CXTiW3Mb.d.cts} +1 -1
  5. package/{auth-adapter-DWzgl5Yq.d.cts → auth-adapter-DSuYwv1c.d.mts} +1 -1
  6. package/auth-better-auth.d.cts +1 -1
  7. package/auth-better-auth.d.mts +1 -1
  8. package/auth-clerk.d.cts +1 -1
  9. package/auth-clerk.d.mts +1 -1
  10. package/auth-workos.d.cts +1 -1
  11. package/auth-workos.d.mts +1 -1
  12. package/core-adapter.d.cts +1 -1
  13. package/core-adapter.d.mts +1 -1
  14. package/core-auth-adapter.d.cts +1 -1
  15. package/core-auth-adapter.d.mts +1 -1
  16. package/core-drizzle.d.cts +1 -1
  17. package/core-drizzle.d.mts +1 -1
  18. package/core-entity-config.cjs +1 -1
  19. package/core-entity-config.d.cts +1 -1
  20. package/core-entity-config.d.mts +1 -1
  21. package/core-entity-config.mjs +1 -1
  22. package/core-entity-heuristics.cjs +1 -0
  23. package/core-entity-heuristics.d.cts +3 -3
  24. package/core-entity-heuristics.d.mts +3 -3
  25. package/core-entity-heuristics.mjs +2 -2
  26. package/core-mapping-form.d.cts +1 -1
  27. package/core-mapping-form.d.mts +1 -1
  28. package/core-mappings-config.cjs +1 -1
  29. package/core-mappings-config.d.cts +1 -1
  30. package/core-mappings-config.d.mts +1 -1
  31. package/core-mappings-config.mjs +1 -1
  32. package/core-sync-engine.cjs +1 -1
  33. package/core-sync-engine.d.cts +1 -1
  34. package/core-sync-engine.d.mts +1 -1
  35. package/core-sync-engine.mjs +1 -1
  36. package/core-types.cjs +4 -0
  37. package/core-types.d.cts +2 -2
  38. package/core-types.d.mts +2 -2
  39. package/core-types.mjs +2 -1
  40. package/core-view-compiler.cjs +1 -1
  41. package/core-view-compiler.d.cts +1 -1
  42. package/core-view-compiler.d.mts +1 -1
  43. package/core-view-compiler.mjs +1 -1
  44. package/core.cjs +17 -15
  45. package/core.d.cts +10 -10
  46. package/core.d.mts +10 -10
  47. package/core.mjs +9 -8
  48. package/{drizzle-BmDDgAIF.d.mts → drizzle-DTOuGXEn.d.cts} +1 -1
  49. package/{drizzle-ixVV4pPN.d.cts → drizzle-SGapoly2.d.mts} +1 -1
  50. package/drizzle.cjs +2 -2
  51. package/drizzle.d.cts +3 -3
  52. package/drizzle.d.mts +3 -3
  53. package/drizzle.mjs +2 -2
  54. package/{entity-config-C8IIkrmX.mjs → entity-config-C4d3K9CP.mjs} +81 -4
  55. package/{entity-config-B2RA53nw.d.cts → entity-config-DNQAMbMS.d.cts} +27 -1
  56. package/{entity-config-B2RA53nw.d.mts → entity-config-DNQAMbMS.d.mts} +27 -1
  57. package/{entity-config-CPM-Tp-d.cjs → entity-config-DgAe0_YD.cjs} +86 -3
  58. package/index.cjs +24 -14
  59. package/index.mjs +24 -14
  60. package/internal-BAOG5eE4.d.mts +226 -0
  61. package/internal-BHJ0X3fC.d.cts +226 -0
  62. package/internal-Bgiimp5D.mjs +144 -0
  63. package/internal-Cz59xMj8.cjs +167 -0
  64. package/internal-adapter.d.cts +1 -1
  65. package/internal-adapter.d.mts +1 -1
  66. package/internal-auth-adapter.d.cts +1 -1
  67. package/internal-auth-adapter.d.mts +1 -1
  68. package/internal-drizzle.d.cts +1 -1
  69. package/internal-drizzle.d.mts +1 -1
  70. package/internal-entity-config.cjs +1 -1
  71. package/internal-entity-config.d.cts +1 -1
  72. package/internal-entity-config.d.mts +1 -1
  73. package/internal-entity-config.mjs +1 -1
  74. package/internal-entity-heuristics.cjs +34 -1
  75. package/internal-entity-heuristics.d.cts +8 -2
  76. package/internal-entity-heuristics.d.mts +8 -2
  77. package/internal-entity-heuristics.mjs +34 -2
  78. package/internal-mapping-form.d.cts +1 -1
  79. package/internal-mapping-form.d.mts +1 -1
  80. package/internal-mappings-config.cjs +1 -1
  81. package/internal-mappings-config.d.cts +1 -1
  82. package/internal-mappings-config.d.mts +1 -1
  83. package/internal-mappings-config.mjs +1 -1
  84. package/internal-sync-engine.cjs +1 -1
  85. package/internal-sync-engine.d.cts +1 -1
  86. package/internal-sync-engine.d.mts +1 -1
  87. package/internal-sync-engine.mjs +1 -1
  88. package/internal-types.cjs +4 -0
  89. package/internal-types.d.cts +2 -2
  90. package/internal-types.d.mts +2 -2
  91. package/internal-types.mjs +2 -1
  92. package/internal-view-compiler.cjs +1 -1
  93. package/internal-view-compiler.d.cts +1 -1
  94. package/internal-view-compiler.d.mts +1 -1
  95. package/internal-view-compiler.mjs +1 -1
  96. package/internal.cjs +17 -15
  97. package/internal.d.cts +10 -10
  98. package/internal.d.mts +10 -10
  99. package/internal.mjs +9 -8
  100. package/{mapping-form-DH77BXLv.d.mts → mapping-form-Cc7Ewv8c.d.mts} +1 -1
  101. package/{mapping-form-J0aU6O7l.d.cts → mapping-form-DwdbYAfR.d.cts} +1 -1
  102. package/{mappings-config-D9dsonoi.d.mts → mappings-config-Be9D43Np.d.mts} +1 -1
  103. package/{mappings-config-RQLGx8ub.d.cts → mappings-config-DUt4-9iO.d.cts} +1 -1
  104. package/{mappings-config-CBwL3FVh.cjs → mappings-config-Dx9ldgdf.cjs} +2 -16
  105. package/{mappings-config-DUKBe6OS.mjs → mappings-config-MoXBeI8p.mjs} +1 -15
  106. package/{namespace-probe-BQ4imnV7.d.mts → namespace-probe-9ScMDR0t.d.mts} +1 -1
  107. package/{namespace-probe-DZIGdvlm.d.cts → namespace-probe-DC4tbmLj.d.cts} +1 -1
  108. package/package.json +1 -1
  109. package/{projection-hnPSgH6G.mjs → projection-CSGJbZlZ.mjs} +1 -1
  110. package/{projection-DEwL-D8R.cjs → projection-Dz6_i1-w.cjs} +0 -6
  111. package/{projection-sql-iHy-Cx6V.cjs → projection-sql-CbXCKt3s.cjs} +1 -1
  112. package/{projection-sql-9zzTVGvf.mjs → projection-sql-iHoiYr1R.mjs} +1 -1
  113. package/{sdk-BL9BCzyD.cjs → sdk-B1vX4kUJ.cjs} +26 -6
  114. package/{sdk-CubnsyD9.mjs → sdk-D6gX0TXS.mjs} +26 -6
  115. package/sdk.cjs +2 -2
  116. package/sdk.d.cts +2 -2
  117. package/sdk.d.mts +2 -2
  118. package/sdk.mjs +2 -2
  119. package/{sync-engine-BLgwLpR6.cjs → sync-engine-B0FT1ax6.cjs} +1 -1
  120. package/{sync-engine-B_IzoUSx.mjs → sync-engine-CTSIcLlZ.mjs} +1 -1
  121. package/{sync-engine-BG2O1an2.d.cts → sync-engine-Df0kHAoJ.d.cts} +2 -2
  122. package/{sync-engine-DgQbrJON.d.mts → sync-engine-ugFacJ6G.d.mts} +2 -2
  123. package/{types-B1oRXdNP.d.mts → types-Cq1uHUKp.d.cts} +28 -2
  124. package/{types-B1oRXdNP.d.cts → types-Cq1uHUKp.d.mts} +28 -2
  125. package/types-Cq5gHCTq.cjs +20 -0
  126. package/types-YJTkXKzU.mjs +9 -0
  127. package/{view-compiler-D_6gfXFr.mjs → view-compiler-BzKk8nY3.mjs} +2 -2
  128. package/{view-compiler-Bo7pqXW3.d.mts → view-compiler-D3zp9S3K.d.cts} +1 -1
  129. package/{view-compiler-CmEdAMwj.d.cts → view-compiler-TpzDTu7z.d.mts} +1 -1
  130. package/{view-compiler-BHI5E4QI.cjs → view-compiler-ZKZbd3SK.cjs} +2 -2
  131. package/vite.d.cts +1 -1
  132. package/vite.d.mts +1 -1
  133. package/internal-BLKMnIIZ.cjs +0 -259
  134. package/internal-BpVh_J77.mjs +0 -212
  135. package/internal-CvlFk3KC.d.mts +0 -180
  136. package/internal-DGUOLQAq.d.cts +0 -180
@@ -0,0 +1,226 @@
1
+ import { F as ProjectionMapping, P as ProjectionJoin, h as FilterExpr, l as ConfigValidationError, m as DiscoveredSchemaTable, p as DiscoveredColumnMeta } from "./entity-config-DNQAMbMS.mjs";
2
+ //#region ../core/src/projection-cardinality.d.ts
3
+ declare function joinFansOut(join: ProjectionJoin, joinTable: DiscoveredSchemaTable): boolean;
4
+ /** Every join in the projection that can multiply rows. */
5
+ declare function fanOutJoins(projection: ProjectionMapping, tablesByName: Map<string, DiscoveredSchemaTable>): ProjectionJoin[];
6
+ //#endregion
7
+ //#region ../core/src/projection-sql.d.ts
8
+ /**
9
+ * SQL generation for projection-backed entities.
10
+ *
11
+ * Base and joins are aliased by their ref key (`base`, then each join's `key`),
12
+ * which is what allows the same table to be joined twice and keeps generated
13
+ * SQL independent of host table names.
14
+ *
15
+ * Pure string generation — no DB access. Identifiers are escaped so output is
16
+ * deterministic and injection-safe.
17
+ */
18
+ /** Quote a single identifier part, escaping embedded double-quotes. */
19
+ declare function quoteIdent(name: string): string;
20
+ /**
21
+ * The identity expression. Composite identities concatenate their parts with
22
+ * `::`; validation guarantees every part is NOT NULL and non-text, which is
23
+ * what makes the concatenation collision-free.
24
+ *
25
+ * Exported unaliased because two paths need the same expression under different
26
+ * names: the view compiler aliases it `id`, and the SDK's projection push source
27
+ * also orders and keyset-paginates by it. Sharing one definition is what keeps
28
+ * a pushed row's `id` identical to the one the view would have produced.
29
+ */
30
+ declare function compileProjectionIdentity(projection: ProjectionMapping, basePkColumn: string): string;
31
+ declare function compileProjectionSelect(projection: ProjectionMapping, basePkColumn: string): {
32
+ columns: string[];
33
+ selectItems: string[];
34
+ };
35
+ declare function compileProjectionJoins(projection: ProjectionMapping): string[];
36
+ /** `FROM <baseTable> AS "base"` — the alias every ref resolves against. */
37
+ declare function compileProjectionFrom(projection: ProjectionMapping): string;
38
+ /**
39
+ * Hard ceiling on preview rows, regardless of what a caller asks for.
40
+ *
41
+ * A preview runs against a production database from a button a user can click
42
+ * as often as they like, so the cap belongs beside the query rather than in
43
+ * whichever caller happens to be passing a limit today.
44
+ */
45
+ declare const PREVIEW_MAX_ROWS = 50;
46
+ /**
47
+ * One SELECT reading a few rows through an UNSAVED projection, for the derived
48
+ * object builder's preview.
49
+ *
50
+ * Lives here, beside the view compiler, because both sides of the round trip
51
+ * need the identical statement: an embedded org runs it against its own
52
+ * database from `@clivly.com/api`, and a cloud org's host app runs it from
53
+ * `@clivly/sdk`. Two copies would be free to disagree about what the user is
54
+ * being shown, which is the whole value of a preview.
55
+ *
56
+ * Columns are aliased by FIELD NAME, not by ref token. That is the deliberate
57
+ * difference from the push source, which aliases by token because cloud ingest
58
+ * reads `row[token]`; a preview is read by a person, in a table whose headers
59
+ * are the field names they just typed.
60
+ */
61
+ declare function compileProjectionPreview(projection: ProjectionMapping, limit: number): string;
62
+ //#endregion
63
+ //#region ../core/src/projection-validate.d.ts
64
+ /**
65
+ * A derived object's filter is evaluated in the cloud against pushed rows, so
66
+ * it can only name columns of the base table.
67
+ *
68
+ * Every rejected shape here fails *silently* today, which is the whole reason
69
+ * the check exists — a filter that cannot be honoured must error, never quietly
70
+ * return the wrong rows. The three shapes fail in different directions:
71
+ *
72
+ * - `$raw` is opaque SQL meant for the embedded model. The cloud path cannot
73
+ * evaluate it and passes every row, so the filter is ignored and the object
74
+ * syncs UNFILTERED.
75
+ * - A joined-table column resolves to nothing on a pushed row, so under a
76
+ * positive operator (`eq`, `in`) no row matches and the object syncs EMPTY.
77
+ * - The same unresolvable column under a NEGATIVE operator (`ne`, `isNull`,
78
+ * `notIn`) is the dangerous one: the missing cell reads as null, so the
79
+ * predicate is true for every row and a filter meant to narrow the sync
80
+ * ADMITS EVERYTHING. Nobody notices extra rows arriving.
81
+ *
82
+ * Rejecting an unresolvable column closes the last two together.
83
+ */
84
+ declare function validateProjectionFilter(filter: FilterExpr | undefined, projection: ProjectionMapping, schema: DiscoveredSchemaTable[], base: string): ConfigValidationError[];
85
+ /**
86
+ * Validate a projection against the discovered schema. Returns every error
87
+ * found (not just the first) so a builder UI can show them all at once.
88
+ */
89
+ declare function validateProjection(projection: ProjectionMapping, schema: DiscoveredSchemaTable[], base: string): ConfigValidationError[];
90
+ //#endregion
91
+ //#region ../core/src/schema-meta.d.ts
92
+ /**
93
+ * Accessors over a discovered table's schema-v2 metadata.
94
+ *
95
+ * Every helper treats *absent* metadata as **unknown**, never as a negative
96
+ * fact. `coversUniqueKey` returning false on a v1 payload is what makes
97
+ * projection validation fail closed: we cannot prove a join is many-to-one, so
98
+ * we refuse to compile it rather than guess.
99
+ */
100
+ declare function columnMetaFor(table: DiscoveredSchemaTable, column: string): DiscoveredColumnMeta | undefined;
101
+ /**
102
+ * Every primary-key column. More than one entry means a composite PK, which is
103
+ * why `base_pk` identity is rejected for such tables.
104
+ */
105
+ declare function primaryKeyColumns(table: DiscoveredSchemaTable): string[];
106
+ /**
107
+ * Do `columns` contain every column of at least one unique key on `table`?
108
+ *
109
+ * This is the fan-out test. A join whose ON columns cover a unique key of the
110
+ * joined table matches at most one row per base row; one that does not can
111
+ * match many.
112
+ */
113
+ declare function coversUniqueKey(table: DiscoveredSchemaTable, columns: string[]): boolean;
114
+ /**
115
+ * A discovered table whose enrichment metadata is known to be present.
116
+ *
117
+ * This narrows only the *presence* of the two optional fields. It says nothing
118
+ * about their contents — an empty `uniqueConstraints` is a known-empty fact
119
+ * (the table genuinely has no unique keys), which is exactly the distinction
120
+ * `hasRichMeta` has always drawn and must keep drawing.
121
+ */
122
+ type RichMetaTable = DiscoveredSchemaTable & {
123
+ columnsMeta: DiscoveredColumnMeta[];
124
+ uniqueConstraints: string[][];
125
+ };
126
+ /**
127
+ * Is this table's payload rich enough to validate a projection against?
128
+ *
129
+ * A type predicate rather than a plain boolean so the narrowing is visible to
130
+ * the compiler: callers that guard on this can iterate `columnsMeta` directly
131
+ * instead of casting, and a cast that outlives its guard becomes a type error
132
+ * rather than a silent walk over `undefined`.
133
+ *
134
+ * The invariant is unchanged: this reports whether the metadata is *known*,
135
+ * never whether the table has keys. Absent metadata stays UNKNOWN.
136
+ */
137
+ declare function hasRichMeta(table: DiscoveredSchemaTable): table is RichMetaTable;
138
+ //#endregion
139
+ //#region ../core/src/slug.d.ts
140
+ /**
141
+ * The one `slugify`.
142
+ *
143
+ * Three byte-identical copies existed before this module — in `@clivly/api`
144
+ * (the custom object-type slug), in `apps/web`'s onboarding route (the
145
+ * organisation slug), and in `@clivly/sdk`. The first two now import this; the
146
+ * SDK's copy stays where it is on purpose, because `@clivly/sdk` cannot depend
147
+ * on this package and its own comment records that its pattern is deliberately
148
+ * kept in step with the server's by hand.
149
+ *
150
+ * Note that the two callers this replaces slug *different things* — an
151
+ * object type and an organisation. They share a transform, not a namespace.
152
+ * Sharing the function stops the transform drifting; it does not make those two
153
+ * identifiers the same identifier.
154
+ */
155
+ /**
156
+ * Lowercase, trim, collapse every run of non-alphanumerics to one hyphen, and
157
+ * drop leading/trailing hyphens. `"Participant Info"` becomes
158
+ * `"participant-info"`.
159
+ *
160
+ * The result is NOT guaranteed to be an acceptable slug. Both
161
+ * `OBJECT_TYPE_SLUG_PATTERN` (server) and `PROJECTION_KEY_PATTERN` (this
162
+ * package) require a leading letter, and this function happily returns
163
+ * `"2024-invoices"` for `"2024 Invoices"`. Callers that need an
164
+ * acceptance-guaranteed identifier want `toObjectTypeSlug`.
165
+ */
166
+ declare function slugify(value: string): string;
167
+ /**
168
+ * A slug the server will accept, derived from a display name.
169
+ *
170
+ * `slugify` alone is not enough: the server's `OBJECT_TYPE_SLUG_PATTERN`
171
+ * (`apps/server/src/index.ts`) demands a leading letter, so a name beginning
172
+ * with a digit slugs to something the platform rejects at push time — long
173
+ * after the object was authored. `@clivly/sdk`'s `custom-slug.ts` already knows
174
+ * this case and deliberately withholds `"2024-invoices"` as a suggestion
175
+ * because it would be rejected too.
176
+ *
177
+ * The prefix is applied ONLY when the derived slug does not already start with
178
+ * a letter, so the overwhelmingly common name is untouched.
179
+ *
180
+ * This value must be shown to whoever authored the name. It is the identifier
181
+ * the host app declares as its `objectType`, so a slug derived silently is a
182
+ * slug the developer cannot copy into their config.
183
+ *
184
+ * Returns an empty string for a name with no alphanumerics at all — the caller
185
+ * decides whether that is "keep typing" or an error, since only it knows
186
+ * whether the field is still being edited.
187
+ */
188
+ declare function toObjectTypeSlug(displayName: string): string;
189
+ //#endregion
190
+ //#region ../core/src/stage-rules.d.ts
191
+ type ConditionOp = "is_empty" | "is_not_empty" | "equals" | "not_equals" | "greater_than" | "less_than" | "before" | "after";
192
+ interface Condition {
193
+ field: string;
194
+ op: ConditionOp;
195
+ value?: string;
196
+ }
197
+ interface EvaluableRule {
198
+ conditions: Condition[];
199
+ id: string;
200
+ }
201
+ /**
202
+ * Whether one condition holds for a host row.
203
+ *
204
+ * `mappedFields` is the set of columns the organization actually exposed. A
205
+ * condition naming anything outside it never matches, whatever the operator: a
206
+ * rule referencing a dropped column must fail safe, because treating the field
207
+ * as null would make `is_empty` match every deal.
208
+ */
209
+ declare function matchesCondition(condition: Condition, fields: Record<string, string | null>, mappedFields: Set<string>): boolean;
210
+ /**
211
+ * The id of the first rule whose conditions all hold, or null when none do.
212
+ *
213
+ * Returns a rule id rather than a stage key because the caller needs the
214
+ * matched rule's `allowClose` before it may move a deal into a closed stage.
215
+ *
216
+ * `rules` must arrive already ordered — stage position first, then rule
217
+ * position within a stage. Sorting needs stage rows this module deliberately
218
+ * does not see.
219
+ *
220
+ * A rule with no conditions never matches: an empty list would otherwise
221
+ * vacuously AND to true and capture every deal, which is the opposite of what
222
+ * a half-written rule should do.
223
+ */
224
+ declare function evaluateStage(rules: EvaluableRule[], fields: Record<string, string | null>, mappedFields: Set<string>): string | null;
225
+ //#endregion
226
+ export { joinFansOut as S, compileProjectionJoins as _, matchesCondition as a, quoteIdent as b, columnMetaFor as c, primaryKeyColumns as d, validateProjection as f, compileProjectionIdentity as g, compileProjectionFrom as h, evaluateStage as i, coversUniqueKey as l, PREVIEW_MAX_ROWS as m, ConditionOp as n, slugify as o, validateProjectionFilter as p, EvaluableRule as r, toObjectTypeSlug as s, Condition as t, hasRichMeta as u, compileProjectionPreview as v, fanOutJoins as x, compileProjectionSelect as y };
@@ -0,0 +1,226 @@
1
+ import { F as ProjectionMapping, P as ProjectionJoin, h as FilterExpr, l as ConfigValidationError, m as DiscoveredSchemaTable, p as DiscoveredColumnMeta } from "./entity-config-DNQAMbMS.cjs";
2
+ //#region ../core/src/projection-cardinality.d.ts
3
+ declare function joinFansOut(join: ProjectionJoin, joinTable: DiscoveredSchemaTable): boolean;
4
+ /** Every join in the projection that can multiply rows. */
5
+ declare function fanOutJoins(projection: ProjectionMapping, tablesByName: Map<string, DiscoveredSchemaTable>): ProjectionJoin[];
6
+ //#endregion
7
+ //#region ../core/src/projection-sql.d.ts
8
+ /**
9
+ * SQL generation for projection-backed entities.
10
+ *
11
+ * Base and joins are aliased by their ref key (`base`, then each join's `key`),
12
+ * which is what allows the same table to be joined twice and keeps generated
13
+ * SQL independent of host table names.
14
+ *
15
+ * Pure string generation — no DB access. Identifiers are escaped so output is
16
+ * deterministic and injection-safe.
17
+ */
18
+ /** Quote a single identifier part, escaping embedded double-quotes. */
19
+ declare function quoteIdent(name: string): string;
20
+ /**
21
+ * The identity expression. Composite identities concatenate their parts with
22
+ * `::`; validation guarantees every part is NOT NULL and non-text, which is
23
+ * what makes the concatenation collision-free.
24
+ *
25
+ * Exported unaliased because two paths need the same expression under different
26
+ * names: the view compiler aliases it `id`, and the SDK's projection push source
27
+ * also orders and keyset-paginates by it. Sharing one definition is what keeps
28
+ * a pushed row's `id` identical to the one the view would have produced.
29
+ */
30
+ declare function compileProjectionIdentity(projection: ProjectionMapping, basePkColumn: string): string;
31
+ declare function compileProjectionSelect(projection: ProjectionMapping, basePkColumn: string): {
32
+ columns: string[];
33
+ selectItems: string[];
34
+ };
35
+ declare function compileProjectionJoins(projection: ProjectionMapping): string[];
36
+ /** `FROM <baseTable> AS "base"` — the alias every ref resolves against. */
37
+ declare function compileProjectionFrom(projection: ProjectionMapping): string;
38
+ /**
39
+ * Hard ceiling on preview rows, regardless of what a caller asks for.
40
+ *
41
+ * A preview runs against a production database from a button a user can click
42
+ * as often as they like, so the cap belongs beside the query rather than in
43
+ * whichever caller happens to be passing a limit today.
44
+ */
45
+ declare const PREVIEW_MAX_ROWS = 50;
46
+ /**
47
+ * One SELECT reading a few rows through an UNSAVED projection, for the derived
48
+ * object builder's preview.
49
+ *
50
+ * Lives here, beside the view compiler, because both sides of the round trip
51
+ * need the identical statement: an embedded org runs it against its own
52
+ * database from `@clivly.com/api`, and a cloud org's host app runs it from
53
+ * `@clivly/sdk`. Two copies would be free to disagree about what the user is
54
+ * being shown, which is the whole value of a preview.
55
+ *
56
+ * Columns are aliased by FIELD NAME, not by ref token. That is the deliberate
57
+ * difference from the push source, which aliases by token because cloud ingest
58
+ * reads `row[token]`; a preview is read by a person, in a table whose headers
59
+ * are the field names they just typed.
60
+ */
61
+ declare function compileProjectionPreview(projection: ProjectionMapping, limit: number): string;
62
+ //#endregion
63
+ //#region ../core/src/projection-validate.d.ts
64
+ /**
65
+ * A derived object's filter is evaluated in the cloud against pushed rows, so
66
+ * it can only name columns of the base table.
67
+ *
68
+ * Every rejected shape here fails *silently* today, which is the whole reason
69
+ * the check exists — a filter that cannot be honoured must error, never quietly
70
+ * return the wrong rows. The three shapes fail in different directions:
71
+ *
72
+ * - `$raw` is opaque SQL meant for the embedded model. The cloud path cannot
73
+ * evaluate it and passes every row, so the filter is ignored and the object
74
+ * syncs UNFILTERED.
75
+ * - A joined-table column resolves to nothing on a pushed row, so under a
76
+ * positive operator (`eq`, `in`) no row matches and the object syncs EMPTY.
77
+ * - The same unresolvable column under a NEGATIVE operator (`ne`, `isNull`,
78
+ * `notIn`) is the dangerous one: the missing cell reads as null, so the
79
+ * predicate is true for every row and a filter meant to narrow the sync
80
+ * ADMITS EVERYTHING. Nobody notices extra rows arriving.
81
+ *
82
+ * Rejecting an unresolvable column closes the last two together.
83
+ */
84
+ declare function validateProjectionFilter(filter: FilterExpr | undefined, projection: ProjectionMapping, schema: DiscoveredSchemaTable[], base: string): ConfigValidationError[];
85
+ /**
86
+ * Validate a projection against the discovered schema. Returns every error
87
+ * found (not just the first) so a builder UI can show them all at once.
88
+ */
89
+ declare function validateProjection(projection: ProjectionMapping, schema: DiscoveredSchemaTable[], base: string): ConfigValidationError[];
90
+ //#endregion
91
+ //#region ../core/src/schema-meta.d.ts
92
+ /**
93
+ * Accessors over a discovered table's schema-v2 metadata.
94
+ *
95
+ * Every helper treats *absent* metadata as **unknown**, never as a negative
96
+ * fact. `coversUniqueKey` returning false on a v1 payload is what makes
97
+ * projection validation fail closed: we cannot prove a join is many-to-one, so
98
+ * we refuse to compile it rather than guess.
99
+ */
100
+ declare function columnMetaFor(table: DiscoveredSchemaTable, column: string): DiscoveredColumnMeta | undefined;
101
+ /**
102
+ * Every primary-key column. More than one entry means a composite PK, which is
103
+ * why `base_pk` identity is rejected for such tables.
104
+ */
105
+ declare function primaryKeyColumns(table: DiscoveredSchemaTable): string[];
106
+ /**
107
+ * Do `columns` contain every column of at least one unique key on `table`?
108
+ *
109
+ * This is the fan-out test. A join whose ON columns cover a unique key of the
110
+ * joined table matches at most one row per base row; one that does not can
111
+ * match many.
112
+ */
113
+ declare function coversUniqueKey(table: DiscoveredSchemaTable, columns: string[]): boolean;
114
+ /**
115
+ * A discovered table whose enrichment metadata is known to be present.
116
+ *
117
+ * This narrows only the *presence* of the two optional fields. It says nothing
118
+ * about their contents — an empty `uniqueConstraints` is a known-empty fact
119
+ * (the table genuinely has no unique keys), which is exactly the distinction
120
+ * `hasRichMeta` has always drawn and must keep drawing.
121
+ */
122
+ type RichMetaTable = DiscoveredSchemaTable & {
123
+ columnsMeta: DiscoveredColumnMeta[];
124
+ uniqueConstraints: string[][];
125
+ };
126
+ /**
127
+ * Is this table's payload rich enough to validate a projection against?
128
+ *
129
+ * A type predicate rather than a plain boolean so the narrowing is visible to
130
+ * the compiler: callers that guard on this can iterate `columnsMeta` directly
131
+ * instead of casting, and a cast that outlives its guard becomes a type error
132
+ * rather than a silent walk over `undefined`.
133
+ *
134
+ * The invariant is unchanged: this reports whether the metadata is *known*,
135
+ * never whether the table has keys. Absent metadata stays UNKNOWN.
136
+ */
137
+ declare function hasRichMeta(table: DiscoveredSchemaTable): table is RichMetaTable;
138
+ //#endregion
139
+ //#region ../core/src/slug.d.ts
140
+ /**
141
+ * The one `slugify`.
142
+ *
143
+ * Three byte-identical copies existed before this module — in `@clivly/api`
144
+ * (the custom object-type slug), in `apps/web`'s onboarding route (the
145
+ * organisation slug), and in `@clivly/sdk`. The first two now import this; the
146
+ * SDK's copy stays where it is on purpose, because `@clivly/sdk` cannot depend
147
+ * on this package and its own comment records that its pattern is deliberately
148
+ * kept in step with the server's by hand.
149
+ *
150
+ * Note that the two callers this replaces slug *different things* — an
151
+ * object type and an organisation. They share a transform, not a namespace.
152
+ * Sharing the function stops the transform drifting; it does not make those two
153
+ * identifiers the same identifier.
154
+ */
155
+ /**
156
+ * Lowercase, trim, collapse every run of non-alphanumerics to one hyphen, and
157
+ * drop leading/trailing hyphens. `"Participant Info"` becomes
158
+ * `"participant-info"`.
159
+ *
160
+ * The result is NOT guaranteed to be an acceptable slug. Both
161
+ * `OBJECT_TYPE_SLUG_PATTERN` (server) and `PROJECTION_KEY_PATTERN` (this
162
+ * package) require a leading letter, and this function happily returns
163
+ * `"2024-invoices"` for `"2024 Invoices"`. Callers that need an
164
+ * acceptance-guaranteed identifier want `toObjectTypeSlug`.
165
+ */
166
+ declare function slugify(value: string): string;
167
+ /**
168
+ * A slug the server will accept, derived from a display name.
169
+ *
170
+ * `slugify` alone is not enough: the server's `OBJECT_TYPE_SLUG_PATTERN`
171
+ * (`apps/server/src/index.ts`) demands a leading letter, so a name beginning
172
+ * with a digit slugs to something the platform rejects at push time — long
173
+ * after the object was authored. `@clivly/sdk`'s `custom-slug.ts` already knows
174
+ * this case and deliberately withholds `"2024-invoices"` as a suggestion
175
+ * because it would be rejected too.
176
+ *
177
+ * The prefix is applied ONLY when the derived slug does not already start with
178
+ * a letter, so the overwhelmingly common name is untouched.
179
+ *
180
+ * This value must be shown to whoever authored the name. It is the identifier
181
+ * the host app declares as its `objectType`, so a slug derived silently is a
182
+ * slug the developer cannot copy into their config.
183
+ *
184
+ * Returns an empty string for a name with no alphanumerics at all — the caller
185
+ * decides whether that is "keep typing" or an error, since only it knows
186
+ * whether the field is still being edited.
187
+ */
188
+ declare function toObjectTypeSlug(displayName: string): string;
189
+ //#endregion
190
+ //#region ../core/src/stage-rules.d.ts
191
+ type ConditionOp = "is_empty" | "is_not_empty" | "equals" | "not_equals" | "greater_than" | "less_than" | "before" | "after";
192
+ interface Condition {
193
+ field: string;
194
+ op: ConditionOp;
195
+ value?: string;
196
+ }
197
+ interface EvaluableRule {
198
+ conditions: Condition[];
199
+ id: string;
200
+ }
201
+ /**
202
+ * Whether one condition holds for a host row.
203
+ *
204
+ * `mappedFields` is the set of columns the organization actually exposed. A
205
+ * condition naming anything outside it never matches, whatever the operator: a
206
+ * rule referencing a dropped column must fail safe, because treating the field
207
+ * as null would make `is_empty` match every deal.
208
+ */
209
+ declare function matchesCondition(condition: Condition, fields: Record<string, string | null>, mappedFields: Set<string>): boolean;
210
+ /**
211
+ * The id of the first rule whose conditions all hold, or null when none do.
212
+ *
213
+ * Returns a rule id rather than a stage key because the caller needs the
214
+ * matched rule's `allowClose` before it may move a deal into a closed stage.
215
+ *
216
+ * `rules` must arrive already ordered — stage position first, then rule
217
+ * position within a stage. Sorting needs stage rows this module deliberately
218
+ * does not see.
219
+ *
220
+ * A rule with no conditions never matches: an empty list would otherwise
221
+ * vacuously AND to true and capture every deal, which is the opposite of what
222
+ * a half-written rule should do.
223
+ */
224
+ declare function evaluateStage(rules: EvaluableRule[], fields: Record<string, string | null>, mappedFields: Set<string>): string | null;
225
+ //#endregion
226
+ export { joinFansOut as S, compileProjectionJoins as _, matchesCondition as a, quoteIdent as b, columnMetaFor as c, primaryKeyColumns as d, validateProjection as f, compileProjectionIdentity as g, compileProjectionFrom as h, evaluateStage as i, coversUniqueKey as l, PREVIEW_MAX_ROWS as m, ConditionOp as n, slugify as o, validateProjectionFilter as p, EvaluableRule as r, toObjectTypeSlug as s, Condition as t, hasRichMeta as u, compileProjectionPreview as v, fanOutJoins as x, compileProjectionSelect as y };
@@ -0,0 +1,144 @@
1
+ //#region ../core/src/slug.ts
2
+ /**
3
+ * The one `slugify`.
4
+ *
5
+ * Three byte-identical copies existed before this module — in `@clivly/api`
6
+ * (the custom object-type slug), in `apps/web`'s onboarding route (the
7
+ * organisation slug), and in `@clivly/sdk`. The first two now import this; the
8
+ * SDK's copy stays where it is on purpose, because `@clivly/sdk` cannot depend
9
+ * on this package and its own comment records that its pattern is deliberately
10
+ * kept in step with the server's by hand.
11
+ *
12
+ * Note that the two callers this replaces slug *different things* — an
13
+ * object type and an organisation. They share a transform, not a namespace.
14
+ * Sharing the function stops the transform drifting; it does not make those two
15
+ * identifiers the same identifier.
16
+ */
17
+ const NON_ALNUM = /[^a-z0-9]+/g;
18
+ const EDGE_HYPHENS = /(^-|-$)/g;
19
+ /**
20
+ * Lowercase, trim, collapse every run of non-alphanumerics to one hyphen, and
21
+ * drop leading/trailing hyphens. `"Participant Info"` becomes
22
+ * `"participant-info"`.
23
+ *
24
+ * The result is NOT guaranteed to be an acceptable slug. Both
25
+ * `OBJECT_TYPE_SLUG_PATTERN` (server) and `PROJECTION_KEY_PATTERN` (this
26
+ * package) require a leading letter, and this function happily returns
27
+ * `"2024-invoices"` for `"2024 Invoices"`. Callers that need an
28
+ * acceptance-guaranteed identifier want `toObjectTypeSlug`.
29
+ */
30
+ function slugify(value) {
31
+ return value.toLowerCase().trim().replace(NON_ALNUM, "-").replace(EDGE_HYPHENS, "");
32
+ }
33
+ /**
34
+ * Prefixed onto a slug that would otherwise not start with a letter.
35
+ *
36
+ * Chosen over trimming the offending characters because dropping them loses
37
+ * information the user typed: `"2024 Invoices"` would become `"invoices"`,
38
+ * which collides with a genuinely different object of that name.
39
+ */
40
+ const LEADING_DIGIT_PREFIX = "obj-";
41
+ /** Mirrors `PROJECTION_KEY_PATTERN` / the server's `OBJECT_TYPE_SLUG_PATTERN`. */
42
+ const MUST_START_WITH_LETTER = /^[a-z]/;
43
+ /**
44
+ * A slug the server will accept, derived from a display name.
45
+ *
46
+ * `slugify` alone is not enough: the server's `OBJECT_TYPE_SLUG_PATTERN`
47
+ * (`apps/server/src/index.ts`) demands a leading letter, so a name beginning
48
+ * with a digit slugs to something the platform rejects at push time — long
49
+ * after the object was authored. `@clivly/sdk`'s `custom-slug.ts` already knows
50
+ * this case and deliberately withholds `"2024-invoices"` as a suggestion
51
+ * because it would be rejected too.
52
+ *
53
+ * The prefix is applied ONLY when the derived slug does not already start with
54
+ * a letter, so the overwhelmingly common name is untouched.
55
+ *
56
+ * This value must be shown to whoever authored the name. It is the identifier
57
+ * the host app declares as its `objectType`, so a slug derived silently is a
58
+ * slug the developer cannot copy into their config.
59
+ *
60
+ * Returns an empty string for a name with no alphanumerics at all — the caller
61
+ * decides whether that is "keep typing" or an error, since only it knows
62
+ * whether the field is still being edited.
63
+ */
64
+ function toObjectTypeSlug(displayName) {
65
+ const slug = slugify(displayName);
66
+ if (slug === "") return "";
67
+ return MUST_START_WITH_LETTER.test(slug) ? slug : `${LEADING_DIGIT_PREFIX}${slug}`;
68
+ }
69
+ //#endregion
70
+ //#region ../core/src/stage-rules.ts
71
+ const CONDITION_OPS = /* @__PURE__ */ new Set([
72
+ "is_empty",
73
+ "is_not_empty",
74
+ "equals",
75
+ "not_equals",
76
+ "greater_than",
77
+ "less_than",
78
+ "before",
79
+ "after"
80
+ ]);
81
+ function isCondition(value) {
82
+ if (!value || typeof value !== "object") return false;
83
+ const condition = value;
84
+ return typeof condition.field === "string" && condition.field.length > 0 && typeof condition.op === "string" && CONDITION_OPS.has(condition.op) && (condition.value === void 0 || typeof condition.value === "string");
85
+ }
86
+ /** Host fields arrive as strings; null and "" are both absent. */
87
+ function isEmpty(value) {
88
+ return value === null || value === void 0 || value === "";
89
+ }
90
+ function compareNumbers(raw, operand, op) {
91
+ const left = Number(raw);
92
+ const right = Number(operand);
93
+ if (Number.isNaN(left) || Number.isNaN(right)) return false;
94
+ return op === "greater_than" ? left > right : left < right;
95
+ }
96
+ function compareDates(raw, operand, op) {
97
+ const left = new Date(raw).getTime();
98
+ const right = new Date(operand ?? "").getTime();
99
+ if (Number.isNaN(left) || Number.isNaN(right)) return false;
100
+ return op === "before" ? left < right : left > right;
101
+ }
102
+ /**
103
+ * Whether one condition holds for a host row.
104
+ *
105
+ * `mappedFields` is the set of columns the organization actually exposed. A
106
+ * condition naming anything outside it never matches, whatever the operator: a
107
+ * rule referencing a dropped column must fail safe, because treating the field
108
+ * as null would make `is_empty` match every deal.
109
+ */
110
+ function matchesCondition(condition, fields, mappedFields) {
111
+ if (!mappedFields.has(condition.field)) return false;
112
+ const raw = fields[condition.field] ?? null;
113
+ if (condition.op === "is_empty") return isEmpty(raw);
114
+ if (condition.op === "is_not_empty") return !isEmpty(raw);
115
+ if (isEmpty(raw)) return false;
116
+ const value = raw;
117
+ if (condition.op === "equals") return value === condition.value;
118
+ if (condition.op === "not_equals") return value !== condition.value;
119
+ if (condition.op === "greater_than" || condition.op === "less_than") return compareNumbers(value, condition.value, condition.op);
120
+ return compareDates(value, condition.value, condition.op);
121
+ }
122
+ /**
123
+ * The id of the first rule whose conditions all hold, or null when none do.
124
+ *
125
+ * Returns a rule id rather than a stage key because the caller needs the
126
+ * matched rule's `allowClose` before it may move a deal into a closed stage.
127
+ *
128
+ * `rules` must arrive already ordered — stage position first, then rule
129
+ * position within a stage. Sorting needs stage rows this module deliberately
130
+ * does not see.
131
+ *
132
+ * A rule with no conditions never matches: an empty list would otherwise
133
+ * vacuously AND to true and capture every deal, which is the opposite of what
134
+ * a half-written rule should do.
135
+ */
136
+ function evaluateStage(rules, fields, mappedFields) {
137
+ for (const rule of rules) {
138
+ if (!Array.isArray(rule.conditions) || rule.conditions.length === 0 || !rule.conditions.every(isCondition)) continue;
139
+ if (rule.conditions.every((condition) => matchesCondition(condition, fields, mappedFields))) return rule.id;
140
+ }
141
+ return null;
142
+ }
143
+ //#endregion
144
+ export { toObjectTypeSlug as i, matchesCondition as n, slugify as r, evaluateStage as t };