@everystack/model 0.4.2 → 0.4.3
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/package.json +1 -1
- package/src/index.ts +2 -2
- package/src/model.ts +73 -1
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -22,8 +22,8 @@ export type { Ability, AbilityCondition, Action } from './can';
|
|
|
22
22
|
export { belongsTo, hasMany } from './relations';
|
|
23
23
|
export type { Relation } from './relations';
|
|
24
24
|
|
|
25
|
-
export { defineModel } from './model';
|
|
26
|
-
export type { ModelDescriptor, ModelDefinition, ModelHooks, WrittenBy } from './model';
|
|
25
|
+
export { defineModel, defineMaterializedTable } from './model';
|
|
26
|
+
export type { ModelDescriptor, ModelDefinition, ModelHooks, WrittenBy, MaterializedTableMarker, MaterializedTableDefinition } from './model';
|
|
27
27
|
|
|
28
28
|
export { defineModule, moduleModels, moduleExtensions, moduleFunctionSql, moduleSequences, moduleDerived } from './module';
|
|
29
29
|
export type { Module, ModuleDefinition } from './module';
|
package/src/model.ts
CHANGED
|
@@ -15,7 +15,8 @@ import type { FieldBuilder } from './field';
|
|
|
15
15
|
import type { Ability } from './can';
|
|
16
16
|
import type { Relation } from './relations';
|
|
17
17
|
import type { TableConstraint } from './constraints';
|
|
18
|
-
import {
|
|
18
|
+
import type { RawSql } from './raw-sql';
|
|
19
|
+
import { validateTriggersFor, type TriggerSpec, type DependsOnRef } from './derived';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Per-table mutation hooks. Imperative function bodies the framework cannot
|
|
@@ -112,6 +113,24 @@ export interface ModelDescriptor {
|
|
|
112
113
|
renamedFrom?: string;
|
|
113
114
|
/** Triggers on this table (compute — reconciled, never migrated; excluded from the base fingerprint). */
|
|
114
115
|
triggers: TriggerSpec[];
|
|
116
|
+
/**
|
|
117
|
+
* Set by {@link defineMaterializedTable}: this table is a materialized cache — served as a
|
|
118
|
+
* TABLE (migrated, fingerprinted, authz'd like any table), but its CONTENTS are pipeline-owned.
|
|
119
|
+
* everystack NEVER reconciles or refreshes it; the declared query and deps are metadata for the
|
|
120
|
+
* pipeline, codegen, and build ordering. Excluded from the base fingerprint (the query is not a
|
|
121
|
+
* database fact — only the columns are), so it hashes identically to the equivalent plain table.
|
|
122
|
+
*/
|
|
123
|
+
materialized?: MaterializedTableMarker;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The declared-query metadata a {@link defineMaterializedTable} carries — never executed by everystack. */
|
|
127
|
+
export interface MaterializedTableMarker {
|
|
128
|
+
/** The defining query (deparsed from `sql\`\``). The pipeline computes the contents FROM it;
|
|
129
|
+
* everystack keeps it for round-trip, codegen, and build ordering — it does not run it. */
|
|
130
|
+
as: string;
|
|
131
|
+
/** Objects this table's contents are computed from — build-order edges for the pipeline. */
|
|
132
|
+
dependsOn: DependsOnRef[];
|
|
133
|
+
comment?: string;
|
|
115
134
|
}
|
|
116
135
|
|
|
117
136
|
export function defineModel(table: string, def: ModelDefinition): ModelDescriptor {
|
|
@@ -157,3 +176,56 @@ export function defineModel(table: string, def: ModelDefinition): ModelDescripto
|
|
|
157
176
|
triggers: def.triggers ?? [],
|
|
158
177
|
};
|
|
159
178
|
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* A MATERIALIZED TABLE — a matview that is shipped and served as a plain table. A refresh-free
|
|
182
|
+
* hot-swap (docs/plans/canonical-sync-artifacts.md) cannot populate a materialized VIEW without
|
|
183
|
+
* REFRESH, so a canonical dataset that must serve exact, pre-computed bytes lives in a table. This
|
|
184
|
+
* declares that table: real columns (fingerprinted, migrated, authz'd like any table, FK-targetable
|
|
185
|
+
* by other schemas), plus the defining query as METADATA. everystack never runs, reconciles, or
|
|
186
|
+
* refreshes it — the pipeline computes the contents locally and ships them in the artifact. The
|
|
187
|
+
* query and deps round-trip through codegen and drive the pipeline's build order.
|
|
188
|
+
*
|
|
189
|
+
* Contrast: {@link defineMaterializedView} is COMPUTE (reconciled + refreshed by everystack);
|
|
190
|
+
* this is STATE (a table everystack migrates, whose data an external pipeline owns).
|
|
191
|
+
*/
|
|
192
|
+
export interface MaterializedTableDefinition {
|
|
193
|
+
fields: Record<string, FieldBuilder>;
|
|
194
|
+
abilities?: Ability[];
|
|
195
|
+
schema?: string;
|
|
196
|
+
constraints?: TableConstraint[];
|
|
197
|
+
/** Build-order edges — the objects the pipeline computes this table's contents FROM. */
|
|
198
|
+
dependsOn?: DependsOnRef[];
|
|
199
|
+
/** The defining query. everystack keeps it (round-trip, codegen, ordering) but NEVER runs it. */
|
|
200
|
+
as: RawSql;
|
|
201
|
+
comment?: string;
|
|
202
|
+
/** Write principal — default `'worker'` (the pipeline loads it on the owner connection,
|
|
203
|
+
* bypassing RLS), so RLS is ENABLE-not-FORCE, matching how the swap loads the rows. */
|
|
204
|
+
writtenBy?: WrittenBy;
|
|
205
|
+
private?: boolean;
|
|
206
|
+
renamedFrom?: string;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function defineMaterializedTable(table: string, def: MaterializedTableDefinition): ModelDescriptor {
|
|
210
|
+
if (def.as == null) {
|
|
211
|
+
throw new Error(`materialized table '${table}': as (the defining query) is ${def.as === undefined ? 'undefined' : 'null'} — declare as: sql\`SELECT …\`.`);
|
|
212
|
+
}
|
|
213
|
+
for (const [i, d] of (def.dependsOn ?? []).entries()) {
|
|
214
|
+
if (d == null) throw new Error(`materialized table '${table}': dependsOn[${i}] is ${d === undefined ? 'undefined' : 'null'} — renamed or missing import?`);
|
|
215
|
+
}
|
|
216
|
+
const model = defineModel(table, {
|
|
217
|
+
fields: def.fields,
|
|
218
|
+
abilities: def.abilities,
|
|
219
|
+
schema: def.schema,
|
|
220
|
+
constraints: def.constraints,
|
|
221
|
+
writtenBy: def.writtenBy ?? 'worker',
|
|
222
|
+
private: def.private,
|
|
223
|
+
...(def.renamedFrom ? { renamedFrom: def.renamedFrom } : {}),
|
|
224
|
+
});
|
|
225
|
+
model.materialized = {
|
|
226
|
+
as: def.as.sql,
|
|
227
|
+
dependsOn: def.dependsOn ?? [],
|
|
228
|
+
...(def.comment ? { comment: def.comment } : {}),
|
|
229
|
+
};
|
|
230
|
+
return model;
|
|
231
|
+
}
|