@gettersethya/mira-collection 0.1.0-alpha.2 → 0.1.0-alpha.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/README.md +164 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# @gettersethya/mira-collection
|
|
2
|
+
|
|
3
|
+
> **Early pre-alpha.** Breaking changes may occur without notice.
|
|
4
|
+
|
|
5
|
+
Collection definitions, filter DSL, and access rule builder for [Mira](https://github.com/gettersethya/mira). Shared between server and client — isomorphic, no runtime dependencies beyond `effect`.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @gettersethya/mira-collection effect
|
|
11
|
+
# or
|
|
12
|
+
pnpm add @gettersethya/mira-collection effect
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Collection types
|
|
16
|
+
|
|
17
|
+
### Base collection
|
|
18
|
+
|
|
19
|
+
Standard CRUD collection. System fields `id`, `seqId`, `created`, and `updated` are added automatically.
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { BaseCollection, Bytes, Field, Rule } from "@gettersethya/mira-collection"
|
|
23
|
+
|
|
24
|
+
const Posts = BaseCollection.define("posts", {
|
|
25
|
+
title: Field.text({ maxLength: 200 }),
|
|
26
|
+
slug: Field.text({ unique: true }),
|
|
27
|
+
content: Field.text(),
|
|
28
|
+
published: Field.boolean({ default: false }),
|
|
29
|
+
authorId: Field.relation(Users),
|
|
30
|
+
thumbnail: Field.file({ maxSize: Bytes.fromMB(5), mimeTypes: ["image/*"] }),
|
|
31
|
+
})
|
|
32
|
+
.indexes((I) => [
|
|
33
|
+
I.on("authorId"),
|
|
34
|
+
I.unique("slug"),
|
|
35
|
+
])
|
|
36
|
+
.rules((R) => ({
|
|
37
|
+
list: R.field("published").eq(R.literal(true)),
|
|
38
|
+
view: Rule.or(R.field("published").eq(R.literal(true)), R.field("authorId").eq(R.authId(Users))),
|
|
39
|
+
create: R.authId(Users).neq(R.literal(null)),
|
|
40
|
+
update: R.field("authorId").eq(R.authId(Users)),
|
|
41
|
+
delete: R.field("authorId").eq(R.authId(Users)),
|
|
42
|
+
}))
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Auth collection
|
|
46
|
+
|
|
47
|
+
Adds `email`, `password` (hashed), and `emailVerified` system fields. Exposes an `authWithPassword` endpoint.
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { AuthCollection, Field, Rule } from "@gettersethya/mira-collection"
|
|
51
|
+
|
|
52
|
+
const Users = AuthCollection.define("users", {
|
|
53
|
+
displayName: Field.text(),
|
|
54
|
+
username: Field.text({ unique: true }),
|
|
55
|
+
role: Field.text({ default: "user" }),
|
|
56
|
+
})
|
|
57
|
+
.rules((R) => ({
|
|
58
|
+
list: Rule.public(),
|
|
59
|
+
view: Rule.public(),
|
|
60
|
+
create: Rule.public(),
|
|
61
|
+
update: R.selfId().eq(R.authId(Users)), // users can only edit themselves
|
|
62
|
+
}))
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### View collection
|
|
66
|
+
|
|
67
|
+
Read-only, backed by a SQL `VIEW`. Supports `list` and `view` only. Every field must be marked `.view()`.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { Field, Rule, ViewCollection } from "@gettersethya/mira-collection"
|
|
71
|
+
|
|
72
|
+
const PostsWithAuthors = ViewCollection.define(
|
|
73
|
+
"posts_with_authors",
|
|
74
|
+
`SELECT p.id, p.seqId, p.title, u.displayName AS authorName
|
|
75
|
+
FROM posts p LEFT JOIN users u ON p.authorId = u.id`,
|
|
76
|
+
{
|
|
77
|
+
id: Field.text().view(),
|
|
78
|
+
seqId: Field.integer().view(),
|
|
79
|
+
title: Field.text().view(),
|
|
80
|
+
authorName: Field.text().view(),
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
.rules((R) => ({ list: Rule.public(), view: Rule.public() }))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Field types
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
Field.text({ minLength?, maxLength?, unique?, indexed?, required?, default? })
|
|
90
|
+
Field.email()
|
|
91
|
+
Field.integer({ min?, max?, unique?, indexed?, default? })
|
|
92
|
+
Field.number({ min?, max? })
|
|
93
|
+
Field.boolean({ default? })
|
|
94
|
+
Field.date()
|
|
95
|
+
Field.json()
|
|
96
|
+
Field.file({ maxSize?, mimeTypes?, protected? })
|
|
97
|
+
Field.relation(Collection)
|
|
98
|
+
|
|
99
|
+
// File size helpers
|
|
100
|
+
Bytes.fromKB(n)
|
|
101
|
+
Bytes.fromMB(n)
|
|
102
|
+
Bytes.fromGB(n)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Indexes
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// Field-level (single column)
|
|
109
|
+
Field.text({ unique: true }) // unique index
|
|
110
|
+
Field.text({ indexed: true }) // non-unique index
|
|
111
|
+
|
|
112
|
+
// Callback (composite or explicit)
|
|
113
|
+
.indexes((I) => [
|
|
114
|
+
I.on("authorId"), // non-unique
|
|
115
|
+
I.on("authorId", "published"), // composite
|
|
116
|
+
I.unique("slug"), // unique constraint
|
|
117
|
+
I.unique("teamId", "userId"), // composite unique
|
|
118
|
+
])
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Rules
|
|
122
|
+
|
|
123
|
+
Rules are evaluated server-side on every request. A missing key means **deny all** for that action.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
Rule.public() // allow all
|
|
127
|
+
R.authId(Users).neq(R.literal(null)) // logged-in users only
|
|
128
|
+
R.field("ownerId").eq(R.authId(Users)) // owner-only
|
|
129
|
+
R.field("role").in(R.literal(["admin", "mod"])) // role check
|
|
130
|
+
R.auth(Users, "role").eq(R.literal("admin")) // check a field on the auth record
|
|
131
|
+
Rule.not(R.field("archived").eq(R.literal(true))) // negation
|
|
132
|
+
Rule.or(R.field("isPublic").eq(R.literal(true)), R.field("ownerId").eq(R.authId(Users)))
|
|
133
|
+
Rule.and(R.field("published").eq(R.literal(true)), R.field("ownerId").eq(R.authId(Users)))
|
|
134
|
+
|
|
135
|
+
// Date arithmetic
|
|
136
|
+
R.field("created").gte(Rule.dateAdd(Rule.now(), -30, "day"))
|
|
137
|
+
|
|
138
|
+
// Request values (header, query param, or body field)
|
|
139
|
+
Rule.request("header", "x-api-key").eq(R.literal("secret"))
|
|
140
|
+
|
|
141
|
+
// Subquery membership check
|
|
142
|
+
Rule.subquery(Members, "teamId").where(R.field("userId").eq(R.authId(Users)))
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Filters
|
|
146
|
+
|
|
147
|
+
Used in client queries. Type-checked against the collection's field types.
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
f.field("published").eq(true)
|
|
151
|
+
f.field("viewCount").gte(100)
|
|
152
|
+
f.field("status").in(["draft", "published"])
|
|
153
|
+
f.field("title").like("%effect%")
|
|
154
|
+
f.field("deletedAt").null()
|
|
155
|
+
f.field("price").between(10, 100)
|
|
156
|
+
|
|
157
|
+
f.and(f.field("published").eq(true), f.field("viewCount").gte(100))
|
|
158
|
+
f.or(f.field("category").eq("tech"), f.field("category").eq("science"))
|
|
159
|
+
f.not(f.field("archived").eq(true))
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## More
|
|
163
|
+
|
|
164
|
+
See the [Mira root README](https://github.com/gettersethya/mira) for full documentation.
|