@absolutejs/artifacts 0.0.5 → 0.1.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/README.md CHANGED
@@ -87,6 +87,27 @@ const history = await artifacts.listRevisions("owner-123", page.id);
87
87
  const restored = await artifacts.restore("owner-123", page.id, 1);
88
88
  ```
89
89
 
90
+ ## Production persistence
91
+
92
+ Use the package-owned Drizzle schema on PostgreSQL, including Neon. The store
93
+ atomically writes the current artifact, immutable revision, and lifecycle
94
+ outbox events. It also persists per-revision indexing state and fences every
95
+ artifact read and mutation by owner.
96
+
97
+ ```ts
98
+ import {
99
+ artifactDrizzleSchema,
100
+ createDrizzleArtifactStore,
101
+ } from "@absolutejs/artifacts/drizzle";
102
+
103
+ const store = createDrizzleArtifactStore({ db });
104
+ ```
105
+
106
+ Export `artifactDrizzleSchema` from your application's Drizzle schema so its
107
+ normal migration workflow owns the four tables. Insert and select TypeBoxes
108
+ generated directly from those tables are exported from the same entry point;
109
+ hosts should reuse them instead of redefining database row schemas.
110
+
90
111
  ## File-backed artifact kinds
91
112
 
92
113
  Use the bundled definitions directly or compose them with application-specific
@@ -0,0 +1,245 @@
1
+ // @bun
2
+ // src/drizzle.ts
3
+ import { and, asc, desc, eq, isNotNull, isNull } from "drizzle-orm";
4
+ import {
5
+ customType,
6
+ index,
7
+ integer,
8
+ pgTable,
9
+ primaryKey,
10
+ text,
11
+ timestamp
12
+ } from "drizzle-orm/pg-core";
13
+ import { createInsertSchema, createSelectSchema } from "drizzle-typebox";
14
+ var recordJsonb = customType({
15
+ dataType: () => "jsonb",
16
+ fromDriver: (value) => typeof value === "string" ? JSON.parse(value) : value,
17
+ toDriver: (value) => JSON.stringify(value)
18
+ });
19
+ var revisionJsonb = customType({
20
+ dataType: () => "jsonb",
21
+ fromDriver: (value) => typeof value === "string" ? JSON.parse(value) : value,
22
+ toDriver: (value) => JSON.stringify(value)
23
+ });
24
+ var eventJsonb = customType({
25
+ dataType: () => "jsonb",
26
+ fromDriver: (value) => typeof value === "string" ? JSON.parse(value) : value,
27
+ toDriver: (value) => JSON.stringify(value)
28
+ });
29
+ var indexingJsonb = customType({
30
+ dataType: () => "jsonb",
31
+ fromDriver: (value) => typeof value === "string" ? JSON.parse(value) : value,
32
+ toDriver: (value) => JSON.stringify(value)
33
+ });
34
+ var artifactRecords = pgTable("artifact_records", {
35
+ document: recordJsonb().notNull(),
36
+ id: text().primaryKey(),
37
+ kind: text().notNull(),
38
+ ownerId: text("owner_id").notNull(),
39
+ revision: integer().notNull(),
40
+ status: text().notNull(),
41
+ updatedAt: timestamp("updated_at", {
42
+ mode: "string",
43
+ withTimezone: true
44
+ }).notNull()
45
+ }, (table) => [
46
+ index("artifact_records_owner_updated_idx").on(table.ownerId, table.updatedAt.desc()),
47
+ index("artifact_records_owner_kind_idx").on(table.ownerId, table.kind)
48
+ ]);
49
+ var artifactRevisions = pgTable("artifact_revisions", {
50
+ artifactId: text("artifact_id").notNull().references(() => artifactRecords.id),
51
+ document: revisionJsonb().notNull(),
52
+ ownerId: text("owner_id").notNull(),
53
+ revision: integer().notNull()
54
+ }, (table) => [
55
+ primaryKey({
56
+ columns: [table.artifactId, table.revision],
57
+ name: "artifact_revisions_pkey"
58
+ }),
59
+ index("artifact_revisions_owner_idx").on(table.ownerId, table.artifactId, table.revision.desc())
60
+ ]);
61
+ var artifactEvents = pgTable("artifact_events", {
62
+ artifactId: text("artifact_id").notNull().references(() => artifactRecords.id),
63
+ createdAt: timestamp("created_at", {
64
+ mode: "string",
65
+ withTimezone: true
66
+ }).notNull(),
67
+ document: eventJsonb().notNull(),
68
+ id: text().primaryKey(),
69
+ ownerId: text("owner_id").notNull(),
70
+ processedAt: timestamp("processed_at", {
71
+ mode: "string",
72
+ withTimezone: true
73
+ }),
74
+ type: text().notNull()
75
+ }, (table) => [
76
+ index("artifact_events_outbox_idx").on(table.processedAt, table.createdAt),
77
+ index("artifact_events_artifact_idx").on(table.ownerId, table.artifactId, table.createdAt)
78
+ ]);
79
+ var artifactIndexingStates = pgTable("artifact_indexing_states", {
80
+ artifactId: text("artifact_id").primaryKey().references(() => artifactRecords.id),
81
+ document: indexingJsonb().notNull(),
82
+ ownerId: text("owner_id").notNull(),
83
+ revision: integer().notNull(),
84
+ status: text().notNull(),
85
+ updatedAt: timestamp("updated_at", {
86
+ mode: "string",
87
+ withTimezone: true
88
+ }).notNull()
89
+ });
90
+ var artifactDrizzleSchema = {
91
+ artifactEvents,
92
+ artifactIndexingStates,
93
+ artifactRecords,
94
+ artifactRevisions
95
+ };
96
+ var ArtifactRecordInsertSchema = createInsertSchema(artifactRecords);
97
+ var ArtifactRecordSelectSchema = createSelectSchema(artifactRecords);
98
+ var ArtifactRevisionInsertSchema = createInsertSchema(artifactRevisions);
99
+ var ArtifactRevisionSelectSchema = createSelectSchema(artifactRevisions);
100
+ var ArtifactEventInsertSchema = createInsertSchema(artifactEvents);
101
+ var ArtifactEventSelectSchema = createSelectSchema(artifactEvents);
102
+ var ArtifactIndexingStateInsertSchema = createInsertSchema(artifactIndexingStates);
103
+ var ArtifactIndexingStateSelectSchema = createSelectSchema(artifactIndexingStates);
104
+ var eventRows = (events) => events.map((event) => ({
105
+ artifactId: event.artifactId,
106
+ createdAt: event.createdAt,
107
+ document: event,
108
+ id: event.id,
109
+ ownerId: event.ownerId,
110
+ processedAt: event.processedAt,
111
+ type: event.type
112
+ }));
113
+ var assertEventsBelongToArtifact = (record, events) => {
114
+ if (events.some((event) => event.artifactId !== record.id || event.ownerId !== record.ownerId))
115
+ throw new Error("Artifact events must belong to the persisted artifact");
116
+ };
117
+ var recordRow = (record) => ({
118
+ document: record,
119
+ id: record.id,
120
+ kind: record.kind,
121
+ ownerId: record.ownerId,
122
+ revision: record.revision,
123
+ status: record.status,
124
+ updatedAt: record.updatedAt
125
+ });
126
+ var recordUpdate = (record) => ({
127
+ document: record,
128
+ kind: record.kind,
129
+ ownerId: record.ownerId,
130
+ revision: record.revision,
131
+ status: record.status,
132
+ updatedAt: record.updatedAt
133
+ });
134
+ var revisionRow = (record) => ({
135
+ artifactId: record.id,
136
+ document: record,
137
+ ownerId: record.ownerId,
138
+ revision: record.revision
139
+ });
140
+ var createDrizzleArtifactStore = (options) => ({
141
+ create: (record, events = []) => options.db.transaction(async (transaction) => {
142
+ assertEventsBelongToArtifact(record, events);
143
+ await transaction.insert(artifactRecords).values(recordRow(record));
144
+ await transaction.insert(artifactRevisions).values(revisionRow(record));
145
+ if (events.length > 0)
146
+ await transaction.insert(artifactEvents).values(eventRows(events));
147
+ }),
148
+ get: async (ownerId, artifactId) => {
149
+ const [row] = await options.db.select({ document: artifactRecords.document }).from(artifactRecords).where(and(eq(artifactRecords.id, artifactId), eq(artifactRecords.ownerId, ownerId))).limit(1);
150
+ return row?.document ?? null;
151
+ },
152
+ getIndexingState: async (ownerId, artifactId) => {
153
+ const [row] = await options.db.select({ document: artifactIndexingStates.document }).from(artifactIndexingStates).where(and(eq(artifactIndexingStates.artifactId, artifactId), eq(artifactIndexingStates.ownerId, ownerId))).limit(1);
154
+ return row?.document ?? null;
155
+ },
156
+ getRevision: async (ownerId, artifactId, revision) => {
157
+ const [row] = await options.db.select({ document: artifactRevisions.document }).from(artifactRevisions).where(and(eq(artifactRevisions.artifactId, artifactId), eq(artifactRevisions.ownerId, ownerId), eq(artifactRevisions.revision, revision))).limit(1);
158
+ return row?.document ?? null;
159
+ },
160
+ list: async (ownerId, query = {}) => {
161
+ const conditions = [eq(artifactRecords.ownerId, ownerId)];
162
+ if (query.kind)
163
+ conditions.push(eq(artifactRecords.kind, query.kind));
164
+ if (query.status)
165
+ conditions.push(eq(artifactRecords.status, query.status));
166
+ const statement = options.db.select({ document: artifactRecords.document }).from(artifactRecords).where(and(...conditions)).orderBy(desc(artifactRecords.updatedAt));
167
+ const rows = query.limit === undefined ? await statement : await statement.limit(query.limit);
168
+ return rows.map(({ document }) => document);
169
+ },
170
+ listEvents: async (query = {}) => {
171
+ const conditions = [];
172
+ if (query.processed !== undefined)
173
+ conditions.push(query.processed ? isNotNull(artifactEvents.processedAt) : isNull(artifactEvents.processedAt));
174
+ if (query.type)
175
+ conditions.push(eq(artifactEvents.type, query.type));
176
+ const statement = options.db.select({
177
+ document: artifactEvents.document,
178
+ processedAt: artifactEvents.processedAt
179
+ }).from(artifactEvents).where(conditions.length > 0 ? and(...conditions) : undefined).orderBy(asc(artifactEvents.createdAt));
180
+ const rows = query.limit === undefined ? await statement : await statement.limit(query.limit);
181
+ return rows.map(({ document, processedAt }) => ({
182
+ ...document,
183
+ ...processedAt ? { processedAt: new Date(processedAt).toISOString() } : {}
184
+ }));
185
+ },
186
+ listReferencedAssetIds: async () => {
187
+ const rows = await options.db.select({ document: artifactRevisions.document }).from(artifactRevisions);
188
+ return [
189
+ ...new Set(rows.flatMap(({ document }) => document.assets.map(({ id }) => id)))
190
+ ];
191
+ },
192
+ listRevisions: async (ownerId, artifactId) => (await options.db.select({ document: artifactRevisions.document }).from(artifactRevisions).where(and(eq(artifactRevisions.artifactId, artifactId), eq(artifactRevisions.ownerId, ownerId))).orderBy(desc(artifactRevisions.revision))).map(({ document }) => document),
193
+ markEventProcessed: async (eventId, processedAt) => (await options.db.update(artifactEvents).set({ processedAt }).where(eq(artifactEvents.id, eventId)).returning({ id: artifactEvents.id })).length === 1,
194
+ putIndexingState: (ownerId, state, events = []) => options.db.transaction(async (transaction) => {
195
+ assertEventsBelongToArtifact({ id: state.artifactId, ownerId }, events);
196
+ const [artifact] = await transaction.select({ id: artifactRecords.id }).from(artifactRecords).where(and(eq(artifactRecords.id, state.artifactId), eq(artifactRecords.ownerId, ownerId))).limit(1);
197
+ if (!artifact)
198
+ throw new Error("Artifact not found");
199
+ await transaction.insert(artifactIndexingStates).values({
200
+ artifactId: state.artifactId,
201
+ document: state,
202
+ ownerId,
203
+ revision: state.revision,
204
+ status: state.status,
205
+ updatedAt: state.updatedAt
206
+ }).onConflictDoUpdate({
207
+ set: {
208
+ document: state,
209
+ ownerId,
210
+ revision: state.revision,
211
+ status: state.status,
212
+ updatedAt: state.updatedAt
213
+ },
214
+ target: artifactIndexingStates.artifactId
215
+ });
216
+ if (events.length > 0)
217
+ await transaction.insert(artifactEvents).values(eventRows(events));
218
+ }),
219
+ save: (record, expectedRevision, events = []) => options.db.transaction(async (transaction) => {
220
+ assertEventsBelongToArtifact(record, events);
221
+ const updated = await transaction.update(artifactRecords).set(recordUpdate(record)).where(and(eq(artifactRecords.id, record.id), eq(artifactRecords.ownerId, record.ownerId), eq(artifactRecords.revision, expectedRevision))).returning({ id: artifactRecords.id });
222
+ if (updated.length !== 1)
223
+ return false;
224
+ await transaction.insert(artifactRevisions).values(revisionRow(record));
225
+ if (events.length > 0)
226
+ await transaction.insert(artifactEvents).values(eventRows(events));
227
+ return true;
228
+ })
229
+ });
230
+ export {
231
+ createDrizzleArtifactStore,
232
+ artifactRevisions,
233
+ artifactRecords,
234
+ artifactIndexingStates,
235
+ artifactEvents,
236
+ artifactDrizzleSchema,
237
+ ArtifactRevisionSelectSchema,
238
+ ArtifactRevisionInsertSchema,
239
+ ArtifactRecordSelectSchema,
240
+ ArtifactRecordInsertSchema,
241
+ ArtifactIndexingStateSelectSchema,
242
+ ArtifactIndexingStateInsertSchema,
243
+ ArtifactEventSelectSchema,
244
+ ArtifactEventInsertSchema
245
+ };
package/dist/manifest.js CHANGED
@@ -14,6 +14,10 @@ var __export = (target, all) => {
14
14
  set: __exportSetter.bind(all, name)
15
15
  });
16
16
  };
17
+ var defineImplementation = () => (implementation) => {
18
+ const defined = implementation;
19
+ return defined;
20
+ };
17
21
  var defineManifest = () => (manifest) => manifest;
18
22
  var literal = (value) => value;
19
23
  var RUNTIME_KIND = literal("runtime");
@@ -5986,13 +5990,63 @@ var manifest = defineManifest()({
5986
5990
  name: "@absolutejs/artifacts",
5987
5991
  tagline: "Give everything your AI makes a real lifecycle."
5988
5992
  },
5993
+ implements: [
5994
+ defineImplementation()({
5995
+ contract: "artifacts/store",
5996
+ factory: "createMemoryArtifactStore",
5997
+ from: "@absolutejs/artifacts",
5998
+ title: "In memory (development only \u2014 history resets on restart)",
5999
+ wiring: {
6000
+ code: "createMemoryArtifactStore()",
6001
+ imports: [
6002
+ {
6003
+ from: "@absolutejs/artifacts",
6004
+ names: ["createMemoryArtifactStore"]
6005
+ }
6006
+ ]
6007
+ }
6008
+ }),
6009
+ defineImplementation()({
6010
+ contract: "artifacts/store",
6011
+ factory: "createDrizzleArtifactStore",
6012
+ from: "@absolutejs/artifacts/drizzle",
6013
+ requires: {
6014
+ peers: [
6015
+ {
6016
+ name: "drizzle-orm",
6017
+ range: ">=1.0.0-rc.4 <2",
6018
+ reason: "Typed artifact revisions, indexing state, and transactional outbox persistence"
6019
+ }
6020
+ ],
6021
+ services: [
6022
+ {
6023
+ description: "Artifact lifecycle and revision database",
6024
+ id: "postgres"
6025
+ }
6026
+ ]
6027
+ },
6028
+ title: "Drizzle Postgres (production, including Neon)",
6029
+ wiring: {
6030
+ code: "createDrizzleArtifactStore({ db })",
6031
+ imports: [
6032
+ {
6033
+ from: "@absolutejs/artifacts/drizzle",
6034
+ names: ["createDrizzleArtifactStore"]
6035
+ }
6036
+ ]
6037
+ }
6038
+ })
6039
+ ],
5989
6040
  settings: Type2.Object({}),
5990
6041
  slots: {
5991
- service: {
6042
+ store: {
5992
6043
  configPath: "$self",
5993
- contract: "artifacts/service",
5994
- description: "The host-configured artifact service",
5995
- known: [],
6044
+ contract: "artifacts/store",
6045
+ description: "Where current artifacts, immutable revisions, indexing state, and transactional outbox events live",
6046
+ known: [
6047
+ "@absolutejs/artifacts#createMemoryArtifactStore",
6048
+ "@absolutejs/artifacts#drizzle"
6049
+ ],
5996
6050
  required: true
5997
6051
  }
5998
6052
  },
@@ -6090,7 +6144,7 @@ var manifest = defineManifest()({
6090
6144
  "\t}",
6091
6145
  "});",
6092
6146
  "",
6093
- "const artifactStore = createMemoryArtifactStore();",
6147
+ "const artifactStore = ${slot.store};",
6094
6148
  "const artifactService = createArtifactService({",
6095
6149
  "\tregistry: artifactRegistry,",
6096
6150
  "\tstore: artifactStore",
@@ -6100,11 +6154,7 @@ var manifest = defineManifest()({
6100
6154
  imports: [
6101
6155
  {
6102
6156
  from: "@absolutejs/artifacts",
6103
- names: [
6104
- "createArtifactService",
6105
- "createMemoryArtifactStore",
6106
- "defineArtifactRegistry"
6107
- ]
6157
+ names: ["createArtifactService", "defineArtifactRegistry"]
6108
6158
  },
6109
6159
  { from: "@sinclair/typebox", names: ["Type"] }
6110
6160
  ],
@@ -8,16 +8,70 @@
8
8
  "name": "@absolutejs/artifacts",
9
9
  "tagline": "Give everything your AI makes a real lifecycle."
10
10
  },
11
+ "implements": [
12
+ {
13
+ "contract": "artifacts/store",
14
+ "factory": "createMemoryArtifactStore",
15
+ "from": "@absolutejs/artifacts",
16
+ "title": "In memory (development only — history resets on restart)",
17
+ "wiring": {
18
+ "code": "createMemoryArtifactStore()",
19
+ "imports": [
20
+ {
21
+ "from": "@absolutejs/artifacts",
22
+ "names": [
23
+ "createMemoryArtifactStore"
24
+ ]
25
+ }
26
+ ]
27
+ }
28
+ },
29
+ {
30
+ "contract": "artifacts/store",
31
+ "factory": "createDrizzleArtifactStore",
32
+ "from": "@absolutejs/artifacts/drizzle",
33
+ "requires": {
34
+ "peers": [
35
+ {
36
+ "name": "drizzle-orm",
37
+ "range": ">=1.0.0-rc.4 <2",
38
+ "reason": "Typed artifact revisions, indexing state, and transactional outbox persistence"
39
+ }
40
+ ],
41
+ "services": [
42
+ {
43
+ "description": "Artifact lifecycle and revision database",
44
+ "id": "postgres"
45
+ }
46
+ ]
47
+ },
48
+ "title": "Drizzle Postgres (production, including Neon)",
49
+ "wiring": {
50
+ "code": "createDrizzleArtifactStore({ db })",
51
+ "imports": [
52
+ {
53
+ "from": "@absolutejs/artifacts/drizzle",
54
+ "names": [
55
+ "createDrizzleArtifactStore"
56
+ ]
57
+ }
58
+ ]
59
+ }
60
+ }
61
+ ],
11
62
  "settings": {
12
63
  "type": "object",
13
64
  "properties": {}
14
65
  },
15
66
  "slots": {
16
- "service": {
67
+ "store": {
17
68
  "configPath": "$self",
18
- "contract": "artifacts/service",
19
- "description": "The host-configured artifact service",
20
- "known": [],
69
+ "contract": "artifacts/store",
70
+ "description": "Where current artifacts, immutable revisions, indexing state, and transactional outbox events live",
71
+ "known": [
72
+ "@absolutejs/artifacts#createMemoryArtifactStore",
73
+ "@absolutejs/artifacts#drizzle"
74
+ ],
21
75
  "required": true
22
76
  }
23
77
  },
@@ -26,13 +80,12 @@
26
80
  "description": "Define structured artifact kinds, provide a store, and create the lifecycle service. Add a publisher only when your host supports public access.",
27
81
  "id": "default",
28
82
  "server": {
29
- "code": "const artifactRegistry = defineArtifactRegistry({\n\tpage: {\n\t\tcapabilities: ['archive', 'edit', 'preview', 'publish'],\n\t\tcontent: Type.Object({ blocks: Type.Array(Type.Unknown()) }),\n\t\tlabel: 'Page'\n\t}\n});\n\nconst artifactStore = createMemoryArtifactStore();\nconst artifactService = createArtifactService({\n\tregistry: artifactRegistry,\n\tstore: artifactStore\n});",
83
+ "code": "const artifactRegistry = defineArtifactRegistry({\n\tpage: {\n\t\tcapabilities: ['archive', 'edit', 'preview', 'publish'],\n\t\tcontent: Type.Object({ blocks: Type.Array(Type.Unknown()) }),\n\t\tlabel: 'Page'\n\t}\n});\n\nconst artifactStore = ${slot.store};\nconst artifactService = createArtifactService({\n\tregistry: artifactRegistry,\n\tstore: artifactStore\n});",
30
84
  "imports": [
31
85
  {
32
86
  "from": "@absolutejs/artifacts",
33
87
  "names": [
34
88
  "createArtifactService",
35
- "createMemoryArtifactStore",
36
89
  "defineArtifactRegistry"
37
90
  ]
38
91
  },